roblox-opencode 1.0.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 +122 -0
- package/commands/setup-game.md +108 -0
- package/commands/sync-check.md +53 -0
- package/core/roblox-core.md +93 -0
- package/dist/server.js +167 -0
- package/package.json +35 -0
- package/skills/roblox-analytics/SKILL.md +277 -0
- package/skills/roblox-analytics/references/event-batcher.luau +75 -0
- package/skills/roblox-animation-vfx/SKILL.md +1325 -0
- package/skills/roblox-architecture/SKILL.md +863 -0
- package/skills/roblox-architecture/references/combat-systems.md +1381 -0
- package/skills/roblox-code-review/SKILL.md +687 -0
- package/skills/roblox-data/SKILL.md +889 -0
- package/skills/roblox-data/references/inventory-systems.md +1729 -0
- package/skills/roblox-debug/SKILL.md +99 -0
- package/skills/roblox-gui/SKILL.md +1103 -0
- package/skills/roblox-gui-fusion/SKILL.md +150 -0
- package/skills/roblox-gui-fusion/references/inventory.luau +427 -0
- package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -0
- package/skills/roblox-gui-fusion/references/shop.luau +411 -0
- package/skills/roblox-luau-mastery/SKILL.md +1519 -0
- package/skills/roblox-monetization/SKILL.md +1084 -0
- package/skills/roblox-monetization/references/process-receipt.luau +131 -0
- package/skills/roblox-networking/SKILL.md +669 -0
- package/skills/roblox-networking/references/remote-validator.luau +193 -0
- package/skills/roblox-publish-checklist/SKILL.md +128 -0
- package/skills/roblox-runtime/SKILL.md +753 -0
- package/skills/roblox-sharp-edges/SKILL.md +295 -0
- package/skills/roblox-sync/SKILL.md +126 -0
- package/skills/roblox-testing/SKILL.md +943 -0
- package/skills/roblox-tooling/SKILL.md +150 -0
- package/vendor/LICENSES/ProfileStore-LICENSE +201 -0
- package/vendor/LICENSES/RbxUtil-LICENSE +7 -0
- package/vendor/LICENSES/promise-LICENSE +21 -0
- package/vendor/LICENSES/t-LICENSE +21 -0
- package/vendor/LICENSES/testez-LICENSE +201 -0
- package/vendor/README.md +84 -0
- package/vendor/fusion/Animation/ExternalTime.luau +84 -0
- package/vendor/fusion/Animation/Spring.luau +322 -0
- package/vendor/fusion/Animation/Stopwatch.luau +128 -0
- package/vendor/fusion/Animation/Tween.luau +187 -0
- package/vendor/fusion/Animation/getTweenDuration.luau +27 -0
- package/vendor/fusion/Animation/getTweenRatio.luau +47 -0
- package/vendor/fusion/Animation/lerpType.luau +164 -0
- package/vendor/fusion/Animation/packType.luau +100 -0
- package/vendor/fusion/Animation/springCoefficients.luau +80 -0
- package/vendor/fusion/Animation/unpackType.luau +103 -0
- package/vendor/fusion/Colour/Oklab.luau +70 -0
- package/vendor/fusion/Colour/sRGB.luau +55 -0
- package/vendor/fusion/External.luau +168 -0
- package/vendor/fusion/ExternalDebug.luau +70 -0
- package/vendor/fusion/Graph/Observer.luau +114 -0
- package/vendor/fusion/Graph/castToGraph.luau +29 -0
- package/vendor/fusion/Graph/change.luau +81 -0
- package/vendor/fusion/Graph/depend.luau +33 -0
- package/vendor/fusion/Graph/evaluate.luau +56 -0
- package/vendor/fusion/Instances/Attribute.luau +58 -0
- package/vendor/fusion/Instances/AttributeChange.luau +47 -0
- package/vendor/fusion/Instances/AttributeOut.luau +63 -0
- package/vendor/fusion/Instances/Child.luau +21 -0
- package/vendor/fusion/Instances/Children.luau +148 -0
- package/vendor/fusion/Instances/Hydrate.luau +33 -0
- package/vendor/fusion/Instances/New.luau +53 -0
- package/vendor/fusion/Instances/OnChange.luau +50 -0
- package/vendor/fusion/Instances/OnEvent.luau +54 -0
- package/vendor/fusion/Instances/Out.luau +69 -0
- package/vendor/fusion/Instances/applyInstanceProps.luau +149 -0
- package/vendor/fusion/Instances/defaultProps.luau +194 -0
- package/vendor/fusion/LICENSE +21 -0
- package/vendor/fusion/Logging/formatError.luau +49 -0
- package/vendor/fusion/Logging/messages.luau +52 -0
- package/vendor/fusion/Logging/parseError.luau +25 -0
- package/vendor/fusion/Memory/checkLifetime.luau +134 -0
- package/vendor/fusion/Memory/deriveScope.luau +24 -0
- package/vendor/fusion/Memory/deriveScopeImpl.luau +45 -0
- package/vendor/fusion/Memory/doCleanup.luau +79 -0
- package/vendor/fusion/Memory/innerScope.luau +34 -0
- package/vendor/fusion/Memory/legacyCleanup.luau +18 -0
- package/vendor/fusion/Memory/needsDestruction.luau +17 -0
- package/vendor/fusion/Memory/poisonScope.luau +34 -0
- package/vendor/fusion/Memory/scopePool.luau +55 -0
- package/vendor/fusion/Memory/scoped.luau +27 -0
- package/vendor/fusion/Memory/whichLivesLonger.luau +75 -0
- package/vendor/fusion/RobloxExternal.luau +98 -0
- package/vendor/fusion/State/Computed.luau +139 -0
- package/vendor/fusion/State/For/Disassembly.luau +211 -0
- package/vendor/fusion/State/For/ForTypes.luau +30 -0
- package/vendor/fusion/State/For/init.luau +110 -0
- package/vendor/fusion/State/ForKeys.luau +94 -0
- package/vendor/fusion/State/ForPairs.luau +97 -0
- package/vendor/fusion/State/ForValues.luau +94 -0
- package/vendor/fusion/State/Value.luau +88 -0
- package/vendor/fusion/State/castToState.luau +26 -0
- package/vendor/fusion/State/peek.luau +31 -0
- package/vendor/fusion/State/updateAll.luau +1 -0
- package/vendor/fusion/Types.luau +314 -0
- package/vendor/fusion/Utility/Contextual.luau +91 -0
- package/vendor/fusion/Utility/Safe.luau +23 -0
- package/vendor/fusion/Utility/isSimilar.luau +29 -0
- package/vendor/fusion/Utility/merge.luau +35 -0
- package/vendor/fusion/Utility/nameOf.luau +35 -0
- package/vendor/fusion/Utility/never.luau +14 -0
- package/vendor/fusion/Utility/nicknames.luau +11 -0
- package/vendor/fusion/Utility/xtypeof.luau +27 -0
- package/vendor/fusion/init.luau +82 -0
- package/vendor/profilestore/init.luau +2243 -0
- package/vendor/promise/init.luau +1982 -0
- package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -0
- package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -0
- package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -0
- package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -0
- package/vendor/rbxutil/buffer-util/Types.luau +60 -0
- package/vendor/rbxutil/buffer-util/index.d.ts +153 -0
- package/vendor/rbxutil/buffer-util/init.luau +41 -0
- package/vendor/rbxutil/buffer-util/package.json +16 -0
- package/vendor/rbxutil/buffer-util/wally.toml +9 -0
- package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -0
- package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -0
- package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -0
- package/vendor/rbxutil/comm/Client/init.luau +135 -0
- package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -0
- package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -0
- package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -0
- package/vendor/rbxutil/comm/Server/init.luau +140 -0
- package/vendor/rbxutil/comm/Types.luau +18 -0
- package/vendor/rbxutil/comm/Util.luau +27 -0
- package/vendor/rbxutil/comm/init.luau +35 -0
- package/vendor/rbxutil/comm/wally.toml +13 -0
- package/vendor/rbxutil/component/init.luau +759 -0
- package/vendor/rbxutil/component/init.test.luau +311 -0
- package/vendor/rbxutil/component/wally.toml +14 -0
- package/vendor/rbxutil/concur/init.luau +542 -0
- package/vendor/rbxutil/concur/init.test.luau +364 -0
- package/vendor/rbxutil/concur/wally.toml +8 -0
- package/vendor/rbxutil/enum-list/init.luau +101 -0
- package/vendor/rbxutil/enum-list/init.test.luau +91 -0
- package/vendor/rbxutil/enum-list/wally.toml +8 -0
- package/vendor/rbxutil/find/index.d.ts +20 -0
- package/vendor/rbxutil/find/init.luau +44 -0
- package/vendor/rbxutil/find/package.json +17 -0
- package/vendor/rbxutil/find/wally.toml +8 -0
- package/vendor/rbxutil/input/Gamepad.luau +559 -0
- package/vendor/rbxutil/input/Keyboard.luau +124 -0
- package/vendor/rbxutil/input/Mouse.luau +278 -0
- package/vendor/rbxutil/input/PreferredInput.luau +91 -0
- package/vendor/rbxutil/input/Touch.luau +120 -0
- package/vendor/rbxutil/input/init.luau +33 -0
- package/vendor/rbxutil/input/wally.toml +12 -0
- package/vendor/rbxutil/loader/index.d.ts +15 -0
- package/vendor/rbxutil/loader/init.luau +137 -0
- package/vendor/rbxutil/loader/wally.toml +8 -0
- package/vendor/rbxutil/log/index.d.ts +38 -0
- package/vendor/rbxutil/log/init.luau +746 -0
- package/vendor/rbxutil/log/wally.toml +8 -0
- package/vendor/rbxutil/net/init.luau +190 -0
- package/vendor/rbxutil/net/wally.toml +8 -0
- package/vendor/rbxutil/option/index.d.ts +44 -0
- package/vendor/rbxutil/option/init.luau +489 -0
- package/vendor/rbxutil/option/init.test.luau +342 -0
- package/vendor/rbxutil/option/wally.toml +8 -0
- package/vendor/rbxutil/pid/index.d.ts +53 -0
- package/vendor/rbxutil/pid/init.luau +195 -0
- package/vendor/rbxutil/pid/package.json +16 -0
- package/vendor/rbxutil/pid/wally.toml +9 -0
- package/vendor/rbxutil/quaternion/index.d.ts +117 -0
- package/vendor/rbxutil/quaternion/init.luau +570 -0
- package/vendor/rbxutil/quaternion/package.json +16 -0
- package/vendor/rbxutil/quaternion/wally.toml +9 -0
- package/vendor/rbxutil/query/index.d.ts +43 -0
- package/vendor/rbxutil/query/init.luau +117 -0
- package/vendor/rbxutil/query/package.json +18 -0
- package/vendor/rbxutil/query/wally.toml +9 -0
- package/vendor/rbxutil/sequent/index.d.ts +28 -0
- package/vendor/rbxutil/sequent/init.luau +340 -0
- package/vendor/rbxutil/sequent/package.json +16 -0
- package/vendor/rbxutil/sequent/wally.toml +9 -0
- package/vendor/rbxutil/ser/init.luau +175 -0
- package/vendor/rbxutil/ser/init.test.luau +50 -0
- package/vendor/rbxutil/ser/wally.toml +11 -0
- package/vendor/rbxutil/shake/index.d.ts +36 -0
- package/vendor/rbxutil/shake/init.luau +532 -0
- package/vendor/rbxutil/shake/init.test.luau +267 -0
- package/vendor/rbxutil/shake/package.json +16 -0
- package/vendor/rbxutil/shake/wally.toml +9 -0
- package/vendor/rbxutil/signal/index.d.ts +100 -0
- package/vendor/rbxutil/signal/init.luau +432 -0
- package/vendor/rbxutil/signal/init.test.luau +190 -0
- package/vendor/rbxutil/signal/package.json +17 -0
- package/vendor/rbxutil/signal/wally.toml +9 -0
- package/vendor/rbxutil/silo/TableWatcher.luau +65 -0
- package/vendor/rbxutil/silo/Util.luau +55 -0
- package/vendor/rbxutil/silo/init.luau +338 -0
- package/vendor/rbxutil/silo/init.test.luau +215 -0
- package/vendor/rbxutil/silo/wally.toml +8 -0
- package/vendor/rbxutil/spring/index.d.ts +40 -0
- package/vendor/rbxutil/spring/init.luau +97 -0
- package/vendor/rbxutil/spring/package.json +17 -0
- package/vendor/rbxutil/spring/wally.toml +8 -0
- package/vendor/rbxutil/stream/index.d.ts +88 -0
- package/vendor/rbxutil/stream/init.luau +597 -0
- package/vendor/rbxutil/stream/package.json +18 -0
- package/vendor/rbxutil/stream/wally.toml +9 -0
- package/vendor/rbxutil/streamable/Streamable.luau +202 -0
- package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -0
- package/vendor/rbxutil/streamable/init.luau +8 -0
- package/vendor/rbxutil/streamable/wally.toml +12 -0
- package/vendor/rbxutil/symbol/init.luau +56 -0
- package/vendor/rbxutil/symbol/init.test.luau +37 -0
- package/vendor/rbxutil/symbol/wally.toml +8 -0
- package/vendor/rbxutil/table-util/init.luau +938 -0
- package/vendor/rbxutil/table-util/init.test.luau +439 -0
- package/vendor/rbxutil/table-util/wally.toml +8 -0
- package/vendor/rbxutil/task-queue/index.d.ts +27 -0
- package/vendor/rbxutil/task-queue/init.luau +97 -0
- package/vendor/rbxutil/task-queue/wally.toml +8 -0
- package/vendor/rbxutil/timer/index.d.ts +81 -0
- package/vendor/rbxutil/timer/init.luau +249 -0
- package/vendor/rbxutil/timer/init.test.luau +73 -0
- package/vendor/rbxutil/timer/wally.toml +11 -0
- package/vendor/rbxutil/tree/index.d.ts +15 -0
- package/vendor/rbxutil/tree/init.luau +137 -0
- package/vendor/rbxutil/tree/wally.toml +8 -0
- package/vendor/rbxutil/trove/index.d.ts +46 -0
- package/vendor/rbxutil/trove/init.luau +787 -0
- package/vendor/rbxutil/trove/init.test.luau +203 -0
- package/vendor/rbxutil/trove/wally.toml +8 -0
- package/vendor/rbxutil/typed-remote/init.luau +196 -0
- package/vendor/rbxutil/typed-remote/wally.toml +8 -0
- package/vendor/rbxutil/wait-for/index.d.ts +17 -0
- package/vendor/rbxutil/wait-for/init.luau +257 -0
- package/vendor/rbxutil/wait-for/init.test.luau +182 -0
- package/vendor/rbxutil/wait-for/wally.toml +11 -0
- package/vendor/t/t.lua +1350 -0
- package/vendor/testez/Context.lua +26 -0
- package/vendor/testez/Expectation.lua +311 -0
- package/vendor/testez/ExpectationContext.lua +38 -0
- package/vendor/testez/LifecycleHooks.lua +89 -0
- package/vendor/testez/Reporters/TeamCityReporter.lua +102 -0
- package/vendor/testez/Reporters/TextReporter.lua +106 -0
- package/vendor/testez/Reporters/TextReporterQuiet.lua +97 -0
- package/vendor/testez/TestBootstrap.lua +147 -0
- package/vendor/testez/TestEnum.lua +28 -0
- package/vendor/testez/TestPlan.lua +304 -0
- package/vendor/testez/TestPlanner.lua +40 -0
- package/vendor/testez/TestResults.lua +112 -0
- package/vendor/testez/TestRunner.lua +188 -0
- package/vendor/testez/TestSession.lua +243 -0
- package/vendor/testez/init.lua +40 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: roblox-gui-fusion
|
|
3
|
+
description: Fusion 0.3 game UI - shop, inventory, settings screens. Reactive declarative patterns for Roblox.
|
|
4
|
+
last_reviewed: 2026-05-24
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- Source patterns: VirtualButFake/fusion-components (MIT), dphfox/Fusion docs -->
|
|
8
|
+
|
|
9
|
+
# Roblox GUI with Fusion
|
|
10
|
+
|
|
11
|
+
## Quick Reference
|
|
12
|
+
|
|
13
|
+
**Framework:** Fusion 0.3 (dphfox/Fusion, MIT). Vendored at `.opencode/vendor/fusion/`.
|
|
14
|
+
|
|
15
|
+
**Require path:** The AI must resolve the Fusion require path based on the project:
|
|
16
|
+
- If `Packages/Fusion` exists (Wally) → `require(ReplicatedStorage.Packages.Fusion)`
|
|
17
|
+
- If `.opencode/vendor/fusion` exists (this plugin) → `require(ReplicatedStorage[".opencode"].vendor.fusion)` or wherever the project root maps in the DataModel
|
|
18
|
+
- If Fusion is elsewhere → match the existing require pattern in the project
|
|
19
|
+
|
|
20
|
+
**When to use this skill:**
|
|
21
|
+
- Building new game UI screens from scratch (shop, inventory, settings, etc.)
|
|
22
|
+
- Any UI with dynamic state (item lists, toggles, selections, animations)
|
|
23
|
+
|
|
24
|
+
**When NOT to use this skill:**
|
|
25
|
+
- Project already has established UI patterns (raw Instance, Roact, Vide) → follow existing patterns instead
|
|
26
|
+
- Simple static HUD with no state → use raw Instance (see `roblox-gui` skill)
|
|
27
|
+
|
|
28
|
+
**Routing logic for the AI:**
|
|
29
|
+
- If the project has existing Fusion code → match its patterns, use these references for architecture only
|
|
30
|
+
- If the project has existing non-Fusion UI → do NOT introduce Fusion, follow what's there
|
|
31
|
+
- If the project has no UI yet (greenfield) → use these references as starting templates, adapt colors/data
|
|
32
|
+
|
|
33
|
+
**Key design rules (framework-agnostic, always apply):**
|
|
34
|
+
- Mobile-first: design for phone, scale up. Touch targets minimum 48x48px.
|
|
35
|
+
- Scale (0-1 proportional) for position/size. Offset only for fixed padding/icons.
|
|
36
|
+
- Container Frame Rule: every logical group gets a Frame with layout modifier inside.
|
|
37
|
+
- UIListLayout/UIGridLayout for auto-arrangement. AutomaticSize on parent.
|
|
38
|
+
- ScreenGui.ResetOnSpawn = false for persistent UI. IgnoreGuiInset = true for fullscreen.
|
|
39
|
+
- Dark palette: off-white text on dark gray (never pure white on pure black).
|
|
40
|
+
- Never use absolute pixel sizes for main containers. UISizeConstraint for min/max bounds.
|
|
41
|
+
- ScrollingFrame: AutomaticCanvasSize. UIListLayout inside for content.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Fusion 0.3 Patterns
|
|
46
|
+
|
|
47
|
+
### Core idioms
|
|
48
|
+
|
|
49
|
+
```luau
|
|
50
|
+
local Fusion = require(path.to.Fusion)
|
|
51
|
+
local scoped = Fusion.scoped
|
|
52
|
+
local peek = Fusion.peek
|
|
53
|
+
local Children = Fusion.Children
|
|
54
|
+
local OnEvent = Fusion.OnEvent
|
|
55
|
+
|
|
56
|
+
-- Create a scope (manages cleanup of all objects created within it)
|
|
57
|
+
local scope = scoped(Fusion)
|
|
58
|
+
|
|
59
|
+
-- Reactive state
|
|
60
|
+
local count = scope:Value(0)
|
|
61
|
+
|
|
62
|
+
-- Derived state (re-computes when dependencies change)
|
|
63
|
+
local label = scope:Computed(function(use)
|
|
64
|
+
return "Count: " .. use(count)
|
|
65
|
+
end)
|
|
66
|
+
|
|
67
|
+
-- Create instances (reactive properties auto-update)
|
|
68
|
+
local gui = scope:New "ScreenGui" {
|
|
69
|
+
Parent = playerGui,
|
|
70
|
+
[Children] = {
|
|
71
|
+
scope:New "TextLabel" {
|
|
72
|
+
Text = label,
|
|
73
|
+
Size = UDim2.fromOffset(200, 50),
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
-- Animate any value
|
|
79
|
+
local smoothPos = scope:Spring(position, 25) -- speed 25
|
|
80
|
+
|
|
81
|
+
-- Read without subscribing (in callbacks)
|
|
82
|
+
local currentCount = peek(count)
|
|
83
|
+
|
|
84
|
+
-- Cleanup everything in the scope
|
|
85
|
+
scope:doCleanup()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Component pattern
|
|
89
|
+
|
|
90
|
+
```luau
|
|
91
|
+
local function Button(scope: Fusion.Scope, props: {
|
|
92
|
+
Text: Fusion.UsedAs<string>,
|
|
93
|
+
OnClick: () -> (),
|
|
94
|
+
Color: Fusion.UsedAs<Color3>?,
|
|
95
|
+
})
|
|
96
|
+
local isHovering = scope:Value(false)
|
|
97
|
+
|
|
98
|
+
return scope:New "TextButton" {
|
|
99
|
+
Text = props.Text,
|
|
100
|
+
BackgroundColor3 = scope:Spring(scope:Computed(function(use)
|
|
101
|
+
local base = if props.Color then use(props.Color) else Color3.fromRGB(80, 140, 255)
|
|
102
|
+
return if use(isHovering) then base:Lerp(Color3.new(1,1,1), 0.1) else base
|
|
103
|
+
end), 25),
|
|
104
|
+
[OnEvent "Activated"] = props.OnClick,
|
|
105
|
+
[OnEvent "MouseEnter"] = function() isHovering:set(true) end,
|
|
106
|
+
[OnEvent "MouseLeave"] = function() isHovering:set(false) end,
|
|
107
|
+
[Children] = { scope:New "UICorner" { CornerRadius = UDim.new(0, 6) } },
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### List rendering (ForPairs)
|
|
113
|
+
|
|
114
|
+
```luau
|
|
115
|
+
-- Renders a UI element for each item. Automatically adds/removes as data changes.
|
|
116
|
+
scope:ForPairs(items, function(use, itemScope, index, item)
|
|
117
|
+
return index, itemScope:New "TextLabel" {
|
|
118
|
+
Text = item.Name,
|
|
119
|
+
LayoutOrder = index,
|
|
120
|
+
}
|
|
121
|
+
end)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## References
|
|
127
|
+
|
|
128
|
+
Full production-quality screen implementations are in `references/`:
|
|
129
|
+
|
|
130
|
+
- **shop.luau** - Item grid, currency header, purchase confirmation modal, server-validated buy flow
|
|
131
|
+
- **inventory.luau** - Owned items grid with rarity borders, equip/unequip, detail panel, empty slots
|
|
132
|
+
- **settings-menu.luau** - Tabbed sections, sliders with drag, toggle switches, dropdown selects
|
|
133
|
+
|
|
134
|
+
Each reference is self-contained and demonstrates:
|
|
135
|
+
- Scope-based memory management
|
|
136
|
+
- Component composition (small functions returning instances)
|
|
137
|
+
- Reactive state driving UI updates
|
|
138
|
+
- Spring animations for smooth transitions
|
|
139
|
+
- ForPairs for dynamic lists
|
|
140
|
+
- Modal/overlay patterns
|
|
141
|
+
- Typed props for clear interfaces
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Sources
|
|
146
|
+
|
|
147
|
+
- Fusion: github.com/dphfox/Fusion (MIT, 764★)
|
|
148
|
+
- Fusion docs: elttob.uk/Fusion/latest
|
|
149
|
+
- Component patterns: VirtualButFake/fusion-components (MIT, 31 components)
|
|
150
|
+
- Fusion 0.3 release notes: github.com/dphfox/Fusion/releases/tag/v0.3-beta
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
Inventory Screen — 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 inventory interface with:
|
|
7
|
+
- Scrollable grid of owned items
|
|
8
|
+
- Item selection with detail panel
|
|
9
|
+
- Equip/unequip toggle
|
|
10
|
+
- Empty slot rendering
|
|
11
|
+
- Responsive grid (fills available width)
|
|
12
|
+
|
|
13
|
+
ADAPT THIS:
|
|
14
|
+
- Replace SLOT_COUNT and item data with your game's inventory system
|
|
15
|
+
- Replace equipRemote/dropRemote with your RemoteFunctions
|
|
16
|
+
- Adjust grid CellSize for your item icon dimensions
|
|
17
|
+
- Add drag-and-drop by tracking InputBegan/InputChanged on cards
|
|
18
|
+
|
|
19
|
+
Requires: Fusion 0.3 (wally: elttob/fusion@0.3.0)
|
|
20
|
+
]]
|
|
21
|
+
|
|
22
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
23
|
+
local Players = game:GetService("Players")
|
|
24
|
+
|
|
25
|
+
-- Adjust this path to match your project structure:
|
|
26
|
+
-- Wally: require(ReplicatedStorage.Packages.Fusion)
|
|
27
|
+
-- Custom: require(game.ReplicatedStorage.Fusion)
|
|
28
|
+
-- Vendored: (default, from roblox-opencode setup)
|
|
29
|
+
local Fusion = require(ReplicatedStorage[".opencode"].vendor.fusion)
|
|
30
|
+
local scoped = Fusion.scoped
|
|
31
|
+
local peek = Fusion.peek
|
|
32
|
+
local Children = Fusion.Children
|
|
33
|
+
local OnEvent = Fusion.OnEvent
|
|
34
|
+
|
|
35
|
+
--------------------------------------------------------------------------------
|
|
36
|
+
-- Types
|
|
37
|
+
--------------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export type InventoryItem = {
|
|
40
|
+
Id: string,
|
|
41
|
+
Name: string,
|
|
42
|
+
Icon: string?,
|
|
43
|
+
Rarity: "common" | "uncommon" | "rare" | "epic" | "legendary",
|
|
44
|
+
Equipped: boolean,
|
|
45
|
+
Description: string?,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type InventoryProps = {
|
|
49
|
+
Items: Fusion.Value<{ InventoryItem }>,
|
|
50
|
+
SlotCount: number,
|
|
51
|
+
Visible: Fusion.Value<boolean>,
|
|
52
|
+
OnEquip: (itemId: string) -> boolean,
|
|
53
|
+
OnUnequip: (itemId: string) -> boolean,
|
|
54
|
+
OnDrop: (itemId: string) -> boolean,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
--------------------------------------------------------------------------------
|
|
58
|
+
-- Theme
|
|
59
|
+
--------------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
local COLORS = {
|
|
62
|
+
Background = Color3.fromRGB(18, 18, 26),
|
|
63
|
+
Surface = Color3.fromRGB(28, 28, 40),
|
|
64
|
+
SurfaceHover = Color3.fromRGB(38, 38, 52),
|
|
65
|
+
SurfaceSelected = Color3.fromRGB(45, 45, 62),
|
|
66
|
+
EmptySlot = Color3.fromRGB(22, 22, 32),
|
|
67
|
+
Text = Color3.fromRGB(235, 235, 240),
|
|
68
|
+
TextSecondary = Color3.fromRGB(155, 155, 170),
|
|
69
|
+
Equipped = Color3.fromRGB(80, 200, 120),
|
|
70
|
+
Rarity = {
|
|
71
|
+
common = Color3.fromRGB(140, 140, 150),
|
|
72
|
+
uncommon = Color3.fromRGB(80, 200, 80),
|
|
73
|
+
rare = Color3.fromRGB(70, 140, 255),
|
|
74
|
+
epic = Color3.fromRGB(180, 70, 255),
|
|
75
|
+
legendary = Color3.fromRGB(255, 180, 40),
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
local FONT = {
|
|
80
|
+
Title = Font.new("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.Bold),
|
|
81
|
+
Body = Font.new("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.Medium),
|
|
82
|
+
Small = Font.new("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.Regular),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
--------------------------------------------------------------------------------
|
|
86
|
+
-- Item Slot component
|
|
87
|
+
--------------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
local function ItemSlot(
|
|
90
|
+
scope: Fusion.Scope,
|
|
91
|
+
item: InventoryItem?,
|
|
92
|
+
isSelected: Fusion.Computed<boolean>,
|
|
93
|
+
onSelect: () -> ()
|
|
94
|
+
)
|
|
95
|
+
if not item then
|
|
96
|
+
return scope:New "Frame" {
|
|
97
|
+
Name = "EmptySlot",
|
|
98
|
+
BackgroundColor3 = COLORS.EmptySlot,
|
|
99
|
+
[Children] = {
|
|
100
|
+
scope:New "UICorner" { CornerRadius = UDim.new(0, 6) },
|
|
101
|
+
scope:New "UIStroke" {
|
|
102
|
+
Color = Color3.fromRGB(40, 40, 50),
|
|
103
|
+
Thickness = 1,
|
|
104
|
+
Transparency = 0.5,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
local isHovering = scope:Value(false)
|
|
111
|
+
local rarityColor = COLORS.Rarity[item.Rarity] or COLORS.Rarity.common
|
|
112
|
+
|
|
113
|
+
return scope:New "TextButton" {
|
|
114
|
+
Name = "Slot_" .. item.Id,
|
|
115
|
+
BackgroundColor3 = scope:Spring(scope:Computed(function(use)
|
|
116
|
+
if use(isSelected) then
|
|
117
|
+
return COLORS.SurfaceSelected
|
|
118
|
+
elseif use(isHovering) then
|
|
119
|
+
return COLORS.SurfaceHover
|
|
120
|
+
end
|
|
121
|
+
return COLORS.Surface
|
|
122
|
+
end), 30),
|
|
123
|
+
AutoButtonColor = false,
|
|
124
|
+
Text = "",
|
|
125
|
+
|
|
126
|
+
[OnEvent "Activated"] = onSelect,
|
|
127
|
+
[OnEvent "MouseEnter"] = function() isHovering:set(true) end,
|
|
128
|
+
[OnEvent "MouseLeave"] = function() isHovering:set(false) end,
|
|
129
|
+
|
|
130
|
+
[Children] = {
|
|
131
|
+
scope:New "UICorner" { CornerRadius = UDim.new(0, 6) },
|
|
132
|
+
|
|
133
|
+
-- Rarity border
|
|
134
|
+
scope:New "UIStroke" {
|
|
135
|
+
Color = rarityColor,
|
|
136
|
+
Thickness = 1.5,
|
|
137
|
+
Transparency = scope:Spring(scope:Computed(function(use)
|
|
138
|
+
return if use(isSelected) then 0 else 0.6
|
|
139
|
+
end), 25),
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
-- Icon
|
|
143
|
+
if item.Icon then scope:New "ImageLabel" {
|
|
144
|
+
Name = "Icon",
|
|
145
|
+
BackgroundTransparency = 1,
|
|
146
|
+
Size = UDim2.new(1, -12, 1, -20),
|
|
147
|
+
Position = UDim2.fromOffset(6, 4),
|
|
148
|
+
Image = item.Icon,
|
|
149
|
+
ScaleType = Enum.ScaleType.Fit,
|
|
150
|
+
} else nil,
|
|
151
|
+
|
|
152
|
+
-- Equipped indicator
|
|
153
|
+
if item.Equipped then scope:New "Frame" {
|
|
154
|
+
Name = "EquippedDot",
|
|
155
|
+
AnchorPoint = Vector2.new(1, 0),
|
|
156
|
+
Position = UDim2.new(1, -4, 0, 4),
|
|
157
|
+
Size = UDim2.fromOffset(8, 8),
|
|
158
|
+
BackgroundColor3 = COLORS.Equipped,
|
|
159
|
+
[Children] = { scope:New "UICorner" { CornerRadius = UDim.new(1, 0) } },
|
|
160
|
+
} else nil,
|
|
161
|
+
|
|
162
|
+
-- Item name at bottom
|
|
163
|
+
scope:New "TextLabel" {
|
|
164
|
+
Name = "ItemName",
|
|
165
|
+
BackgroundTransparency = 1,
|
|
166
|
+
AnchorPoint = Vector2.new(0, 1),
|
|
167
|
+
Position = UDim2.new(0, 4, 1, -3),
|
|
168
|
+
Size = UDim2.new(1, -8, 0, 14),
|
|
169
|
+
FontFace = FONT.Small,
|
|
170
|
+
Text = item.Name,
|
|
171
|
+
TextColor3 = COLORS.TextSecondary,
|
|
172
|
+
TextSize = 10,
|
|
173
|
+
TextTruncate = Enum.TextTruncate.AtEnd,
|
|
174
|
+
TextXAlignment = Enum.TextXAlignment.Center,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
--------------------------------------------------------------------------------
|
|
181
|
+
-- Detail Panel component
|
|
182
|
+
--------------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
local function DetailPanel(
|
|
185
|
+
scope: Fusion.Scope,
|
|
186
|
+
selectedItem: Fusion.Computed<InventoryItem?>,
|
|
187
|
+
onEquipToggle: () -> (),
|
|
188
|
+
onDrop: () -> ()
|
|
189
|
+
)
|
|
190
|
+
return scope:New "Frame" {
|
|
191
|
+
Name = "DetailPanel",
|
|
192
|
+
BackgroundColor3 = COLORS.Surface,
|
|
193
|
+
Size = UDim2.new(1, 0, 0, 100),
|
|
194
|
+
Visible = scope:Computed(function(use)
|
|
195
|
+
return use(selectedItem) ~= nil
|
|
196
|
+
end),
|
|
197
|
+
|
|
198
|
+
[Children] = {
|
|
199
|
+
scope:New "UICorner" { CornerRadius = UDim.new(0, 8) },
|
|
200
|
+
scope:New "UIPadding" {
|
|
201
|
+
PaddingTop = UDim.new(0, 12),
|
|
202
|
+
PaddingBottom = UDim.new(0, 12),
|
|
203
|
+
PaddingLeft = UDim.new(0, 12),
|
|
204
|
+
PaddingRight = UDim.new(0, 12),
|
|
205
|
+
},
|
|
206
|
+
scope:New "UIListLayout" {
|
|
207
|
+
FillDirection = Enum.FillDirection.Vertical,
|
|
208
|
+
Padding = UDim.new(0, 8),
|
|
209
|
+
SortOrder = Enum.SortOrder.LayoutOrder,
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
-- Item name + rarity
|
|
213
|
+
scope:New "TextLabel" {
|
|
214
|
+
BackgroundTransparency = 1,
|
|
215
|
+
Size = UDim2.new(1, 0, 0, 18),
|
|
216
|
+
FontFace = FONT.Title,
|
|
217
|
+
Text = scope:Computed(function(use)
|
|
218
|
+
local item = use(selectedItem)
|
|
219
|
+
return if item then item.Name else ""
|
|
220
|
+
end),
|
|
221
|
+
TextColor3 = scope:Computed(function(use)
|
|
222
|
+
local item = use(selectedItem)
|
|
223
|
+
if item then
|
|
224
|
+
return COLORS.Rarity[item.Rarity] or COLORS.Text
|
|
225
|
+
end
|
|
226
|
+
return COLORS.Text
|
|
227
|
+
end),
|
|
228
|
+
TextSize = 15,
|
|
229
|
+
TextXAlignment = Enum.TextXAlignment.Left,
|
|
230
|
+
LayoutOrder = 1,
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
-- Description
|
|
234
|
+
scope:New "TextLabel" {
|
|
235
|
+
BackgroundTransparency = 1,
|
|
236
|
+
Size = UDim2.new(1, 0, 0, 14),
|
|
237
|
+
FontFace = FONT.Small,
|
|
238
|
+
Text = scope:Computed(function(use)
|
|
239
|
+
local item = use(selectedItem)
|
|
240
|
+
return if item and item.Description then item.Description else ""
|
|
241
|
+
end),
|
|
242
|
+
TextColor3 = COLORS.TextSecondary,
|
|
243
|
+
TextSize = 12,
|
|
244
|
+
TextWrapped = true,
|
|
245
|
+
TextXAlignment = Enum.TextXAlignment.Left,
|
|
246
|
+
LayoutOrder = 2,
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
-- Action buttons
|
|
250
|
+
scope:New "Frame" {
|
|
251
|
+
BackgroundTransparency = 1,
|
|
252
|
+
Size = UDim2.new(1, 0, 0, 28),
|
|
253
|
+
LayoutOrder = 3,
|
|
254
|
+
[Children] = {
|
|
255
|
+
scope:New "UIListLayout" {
|
|
256
|
+
FillDirection = Enum.FillDirection.Horizontal,
|
|
257
|
+
Padding = UDim.new(0, 8),
|
|
258
|
+
VerticalAlignment = Enum.VerticalAlignment.Center,
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
-- Equip/Unequip button
|
|
262
|
+
scope:New "TextButton" {
|
|
263
|
+
Name = "EquipBtn",
|
|
264
|
+
BackgroundColor3 = COLORS.Equipped,
|
|
265
|
+
Size = UDim2.new(0, 80, 0, 28),
|
|
266
|
+
FontFace = FONT.Body,
|
|
267
|
+
Text = scope:Computed(function(use)
|
|
268
|
+
local item = use(selectedItem)
|
|
269
|
+
return if item and item.Equipped then "Unequip" else "Equip"
|
|
270
|
+
end),
|
|
271
|
+
TextColor3 = COLORS.Text,
|
|
272
|
+
TextSize = 12,
|
|
273
|
+
AutoButtonColor = false,
|
|
274
|
+
[OnEvent "Activated"] = onEquipToggle,
|
|
275
|
+
[Children] = { scope:New "UICorner" { CornerRadius = UDim.new(0, 4) } },
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
-- Drop button
|
|
279
|
+
scope:New "TextButton" {
|
|
280
|
+
Name = "DropBtn",
|
|
281
|
+
BackgroundColor3 = Color3.fromRGB(60, 30, 30),
|
|
282
|
+
Size = UDim2.new(0, 60, 0, 28),
|
|
283
|
+
FontFace = FONT.Body,
|
|
284
|
+
Text = "Drop",
|
|
285
|
+
TextColor3 = Color3.fromRGB(220, 100, 100),
|
|
286
|
+
TextSize = 12,
|
|
287
|
+
AutoButtonColor = false,
|
|
288
|
+
[OnEvent "Activated"] = onDrop,
|
|
289
|
+
[Children] = { scope:New "UICorner" { CornerRadius = UDim.new(0, 4) } },
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
--------------------------------------------------------------------------------
|
|
298
|
+
-- Inventory Screen (main export)
|
|
299
|
+
--------------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
local function InventoryScreen(props: InventoryProps)
|
|
302
|
+
local scope = scoped(Fusion)
|
|
303
|
+
|
|
304
|
+
local selectedItemId: Fusion.Value<string?> = scope:Value(nil)
|
|
305
|
+
|
|
306
|
+
local selectedItem = scope:Computed(function(use)
|
|
307
|
+
local id = use(selectedItemId)
|
|
308
|
+
if not id then return nil end
|
|
309
|
+
for _, item in use(props.Items) do
|
|
310
|
+
if item.Id == id then return item end
|
|
311
|
+
end
|
|
312
|
+
return nil
|
|
313
|
+
end)
|
|
314
|
+
|
|
315
|
+
-- Build slot data: items + empty slots to fill SlotCount
|
|
316
|
+
local slotData = scope:Computed(function(use)
|
|
317
|
+
local items = use(props.Items)
|
|
318
|
+
local slots: { InventoryItem? } = {}
|
|
319
|
+
for i, item in items do
|
|
320
|
+
slots[i] = item
|
|
321
|
+
end
|
|
322
|
+
for i = #items + 1, props.SlotCount do
|
|
323
|
+
slots[i] = nil
|
|
324
|
+
end
|
|
325
|
+
return slots
|
|
326
|
+
end)
|
|
327
|
+
|
|
328
|
+
local screenGui = scope:New "ScreenGui" {
|
|
329
|
+
Name = "InventoryScreen",
|
|
330
|
+
ResetOnSpawn = false,
|
|
331
|
+
IgnoreGuiInset = true,
|
|
332
|
+
Enabled = props.Visible,
|
|
333
|
+
DisplayOrder = 10,
|
|
334
|
+
Parent = Players.LocalPlayer:WaitForChild("PlayerGui"),
|
|
335
|
+
|
|
336
|
+
[Children] = {
|
|
337
|
+
scope:New "Frame" {
|
|
338
|
+
Name = "Container",
|
|
339
|
+
BackgroundColor3 = COLORS.Background,
|
|
340
|
+
Size = UDim2.new(1, 0, 1, 0),
|
|
341
|
+
|
|
342
|
+
[Children] = {
|
|
343
|
+
scope:New "UIPadding" {
|
|
344
|
+
PaddingTop = UDim.new(0, 60),
|
|
345
|
+
PaddingBottom = UDim.new(0, 12),
|
|
346
|
+
PaddingLeft = UDim.new(0, 16),
|
|
347
|
+
PaddingRight = UDim.new(0, 16),
|
|
348
|
+
},
|
|
349
|
+
scope:New "UIListLayout" {
|
|
350
|
+
FillDirection = Enum.FillDirection.Vertical,
|
|
351
|
+
Padding = UDim.new(0, 12),
|
|
352
|
+
SortOrder = Enum.SortOrder.LayoutOrder,
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
-- Title
|
|
356
|
+
scope:New "TextLabel" {
|
|
357
|
+
Name = "Title",
|
|
358
|
+
BackgroundTransparency = 1,
|
|
359
|
+
Size = UDim2.new(1, 0, 0, 28),
|
|
360
|
+
FontFace = FONT.Title,
|
|
361
|
+
Text = "Inventory",
|
|
362
|
+
TextColor3 = COLORS.Text,
|
|
363
|
+
TextSize = 20,
|
|
364
|
+
TextXAlignment = Enum.TextXAlignment.Left,
|
|
365
|
+
LayoutOrder = 1,
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
-- Item grid (scrollable)
|
|
369
|
+
scope:New "ScrollingFrame" {
|
|
370
|
+
Name = "ItemGrid",
|
|
371
|
+
BackgroundTransparency = 1,
|
|
372
|
+
Size = UDim2.new(1, 0, 1, -150),
|
|
373
|
+
LayoutOrder = 2,
|
|
374
|
+
AutomaticCanvasSize = Enum.AutomaticSize.Y,
|
|
375
|
+
ScrollingDirection = Enum.ScrollingDirection.Y,
|
|
376
|
+
ScrollBarThickness = 4,
|
|
377
|
+
ScrollBarImageColor3 = COLORS.TextSecondary,
|
|
378
|
+
CanvasSize = UDim2.new(0, 0, 0, 0),
|
|
379
|
+
|
|
380
|
+
[Children] = {
|
|
381
|
+
scope:New "UIGridLayout" {
|
|
382
|
+
CellSize = UDim2.new(0, 72, 0, 80),
|
|
383
|
+
CellPadding = UDim2.new(0, 6, 0, 6),
|
|
384
|
+
FillDirection = Enum.FillDirection.Horizontal,
|
|
385
|
+
FillDirectionMaxCells = 0,
|
|
386
|
+
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
|
387
|
+
SortOrder = Enum.SortOrder.LayoutOrder,
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
scope:ForPairs(slotData, function(use, slotScope, index, item)
|
|
391
|
+
local isSelected = slotScope:Computed(function(use)
|
|
392
|
+
return item ~= nil and use(selectedItemId) == item.Id
|
|
393
|
+
end)
|
|
394
|
+
|
|
395
|
+
return index, ItemSlot(slotScope, item, isSelected, function()
|
|
396
|
+
if item then
|
|
397
|
+
selectedItemId:set(item.Id)
|
|
398
|
+
end
|
|
399
|
+
end)
|
|
400
|
+
end),
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
-- Detail panel at bottom
|
|
405
|
+
DetailPanel(scope, selectedItem, function()
|
|
406
|
+
local item = peek(selectedItem)
|
|
407
|
+
if not item then return end
|
|
408
|
+
if item.Equipped then
|
|
409
|
+
props.OnUnequip(item.Id)
|
|
410
|
+
else
|
|
411
|
+
props.OnEquip(item.Id)
|
|
412
|
+
end
|
|
413
|
+
end, function()
|
|
414
|
+
local item = peek(selectedItem)
|
|
415
|
+
if not item then return end
|
|
416
|
+
props.OnDrop(item.Id)
|
|
417
|
+
selectedItemId:set(nil)
|
|
418
|
+
end),
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return screenGui, scope
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
return InventoryScreen
|