diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 6ec304a6..9c108c86 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -40,7 +40,7 @@
},
// Use 'postCreateCommand' to run commands after the container is created.
- "postCreateCommand": "pip install mkdocs-material mike --break-system-packages && rokit install --no-trust-check && wally install && rojo sourcemap package.project.json --output sourcemap.json && wally-package-types --sourcemap sourcemap.json Packages/",
+ "postCreateCommand": "pip install mkdocs-material mike --break-system-packages && rokit install --no-trust-check && wally install && rojo sourcemap test.project.json --output sourcemap.json && wally-package-types --sourcemap sourcemap.json Packages/",
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
"remoteUser": "root"
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..873a5d37
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+*.rbxmx linguist-language=XML
+wally.lock linguist-language=TOML
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 52439489..d6b65295 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -14,7 +14,7 @@ permissions:
contents: write
env:
- VERSION: 1.x
+ VERSION: 2.x
jobs:
deploy:
diff --git a/.gitignore b/.gitignore
index ac03bdf3..00367eb5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,5 @@
# Rojo
sourcemap.json
-*.rbxl
-*.rbxlx
-*.rbxm
-*.rbxmx
# Wally
Packages/
@@ -11,3 +7,6 @@ DevPackages/
# MkDocs documentation
site/
+
+# Builds
+dist/
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index bc412731..fde8f0ab 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -10,5 +10,6 @@
"tag:yaml.org,2002:python/name:material.extensions.emoji.twemoji",
"tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format",
"tag:yaml.org,2002:python/object/apply:pymdownx.slugs.slugify mapping"
- ]
+ ],
+ "luau-lsp.sourcemap.rojoProjectFile": "test.project.json"
}
\ No newline at end of file
diff --git a/docs/alternatives.md b/docs/alternatives.md
deleted file mode 100644
index b74831cd..00000000
--- a/docs/alternatives.md
+++ /dev/null
@@ -1,55 +0,0 @@
-Satchel isn't the best backpack system out there or the only one but it does offer some advantages against others.
-
-## Purse
-
-[Purse] is a sub project of Satchel and a fork on the CoreGui backpack. It tries to be as close as possible to the default backpack while Satchel tries to be an improvement over it.
-
- [Purse]: https://purse.luau.page/
-
-### Pros
-
-- Easy to install, drag and drop installation
-- Well documented
-- Full platform support
-
-### Cons
-
-- Requires scripting knowledge to customize
-- No new features over default backpack
-
-## NeoHotbar
-
-[Neobar] is a modern hotbar-only system that acts as a great alternative to Satchel if you are looking for a hotbar only. Made on a strong foundation and well-built, [Neobar] is a powerful tool with unparalleled customization and API.
-
- [Neobar]: https://loneka.com/neohotbar/
-
-### Pros
-
-- Easy to install, drag and drop installation
-- Powerful and highly customizable interface
-- Well documented
-- Full platform support
-
-### Cons
-
-- Requires scripting knowledge to customize
-- No instance attributes
-- Hotbar system only
-
-## ReInvent
-
-[ReInvent] is an older-style hotbar and inventory system that is made completely separate from the backpack core scripts. [ReInvent] is no longer supported and lacks proper documentation and developer-facing APIs.
-
- [ReInvent]: https://devforum.roblox.com/t/1822656
-
-### Pros
-
-- Easy to install, drag and drop installation
-- Animated interface
-- Backpack and hotbar system
-
-### Cons
-
-- Computer and mobile platforms only
-- Poor documentation
-- Discontinued
diff --git a/docs/api-reference.md b/docs/api-reference.md
index af4f8f59..b3c5f9df 100644
--- a/docs/api-reference.md
+++ b/docs/api-reference.md
@@ -1,240 +1,125 @@
-
-
-Satchel is a reskin of the default BackpackGui located in [CoreGui]. Satchel acts very similar to the default backpack and is based on a fork on the default backpack. Behaviors between the two should remain the same with both of them managing the [Backpack].
-
- [CoreGui]: https://create.roblox.com/docs/reference/engine/classes/CoreGui
- [Backpack]: https://create.roblox.com/docs/reference/engine/classes/Backpack
-
-## Summary
-
-### Attributes
-
-| Attribute | Description | Default |
-| :--- | :--- | :--- |
-| BackgroundColor3: [`Color3`](https://create.roblox.com/docs/reference/engine/datatypes/Color3) | Determines the background color of the default inventory window and slots. | `[25, 27, 29]` |
-| BackgroundTransparency: [`number`](https://create.roblox.com/docs/scripting/luau/numbers) | Determines the background transparency of the default inventory window and slots. | 0.3 |
-| CornerRadius: [`UDim`](https://create.roblox.com/docs/reference/engine/datatypes/UDim) | Determines the radius, in pixels, of the default inventory window and slots. | `0, 8` |
-| EquipBorderColor3: [`Color3`](https://create.roblox.com/docs/reference/engine/datatypes/Color3) | Determines the color of the equip border when a slot is equipped. | `[255, 255, 255]` |
-| EquipBorderSizePixel: [`number`](https://create.roblox.com/docs/scripting/luau/numbers) | Determines the pixel width of the equip border when a slot is equipped. | `5` |
-| FontFace: [`Font`](https://create.roblox.com/docs/reference/engine/enums/Font) | Determines the font of the default inventory window and slots. | `Builder Sans` |
-| InsetIconPadding: [`boolean`](https://create.roblox.com/docs/scripting/luau/booleans) | Determines whether or not the tool icon is padded in the default inventory window and slots. | True |
-| OutlineEquipBorder: [`boolean`](https://create.roblox.com/docs/scripting/luau/booleans) | Determines whether or not the equip border is outline or inset when a slot is equipped. | True |
-| TextColor3: [`Color3`](https://create.roblox.com/docs/reference/engine/datatypes/Color3) | Determines the color of the text in default inventory window and slots. | `[255, 255, 255]` |
-| TextSize: [`number`](https://create.roblox.com/docs/scripting/luau/numbers) | Determines the size of the text in the default inventory window and slots. | `14` |
-| TextStrokeColor3: [`Color3`](https://create.roblox.com/docs/reference/engine/datatypes/Color3) | Determines the color of the text stroke of text in default inventory window and slots. | `[0, 0, 0]` |
-| TextStrokeTransparency: [`number`](https://create.roblox.com/docs/scripting/luau/numbers) | Determines the transparency of the text stroke of text in default chat window and slots. | 0.5 |
-
-### Methods
-
-| IsOpened(): [`boolean`](https://create.roblox.com/docs/scripting/luau/booleans) |
-| :--- |
-| Returns whether the inventory is opened or not. |
-
-| SetBackpackEnabled(enabled: boolean): `void` |
-| :--- |
-| Sets whether the backpack gui is enabled or disabled. |
-
-| GetBackpackEnabled(): [`boolean`](https://create.roblox.com/docs/scripting/luau/booleans) |
-| :--- |
-| Returns whether the backpack gui is enabled or disabled. |
-
-| GetStateChangedEvent(): [`RBXScriptSignal`](https://create.roblox.com/docs/reference/engine/datatypes/RBXScriptSignal) |
-| :--- |
-| Returns a signal that fires when the inventory is opened or closed. |
-
-## Attributes
-
-### BackgroundColor3
-
-[`Color3`](https://create.roblox.com/docs/reference/engine/datatypes/Color3)
-
-Determines the background color of the default inventory window and slots. Changing this will update the background color for all elements excluding the search box background for visibility purposes.
-
-### BackgroundTransparency
-
-[`number`](https://create.roblox.com/docs/luau/numbers)
-
-Determines the background transparency of the default inventory window and slots. This will change how the hot bar looks in its locked state and the inventory background.
-
-### CornerRadius
-
-[`UDim`](https://create.roblox.com/docs/reference/engine/datatypes/UDim)
-
-Determines the radius, in pixels, of the default inventory window and slots. This will affect all elements with a visible rounded corner. The corner radius for the search bar is calculated automatically based on this value.
-
-### EquipBorderColor3
-
-[`Color3`](https://create.roblox.com/docs/reference/engine/datatypes/Color3)
-
-Determines the color of the equip border when a slot is equipped. The drag outline color of the slot will not changed by this.
-
-### EquipBorderSizePixel
-
-[`number`](https://create.roblox.com/docs/luau/numbers)
-
-Determines the pixel width of the equip border when a slot is equipped. This additionally controls the padding of tool icons.
-
-### FontFace
-
-[`Enum.Font`](https://create.roblox.com/docs/reference/engine/enums/Font)
-
-Determines the font of the default inventory window and slots. This includes all text in the Satchel UI.
-
-!!! bug
-
- Rojo does not support the [Font](https://create.roblox.com/docs/reference/engine/datatypes/Font) instance attribute so the it will not be synced. You may add the attribute manually if you wish to adjust the font.
-
-### InsetIconPadding
-
-[`bool`](https://create.roblox.com/docs/luau/booleans)
+## Methods
-Determines whether or not the tool icon is padded in the default inventory window and slots. Changing this will change how the tool icon is padded in the slot or not.
+### getEnabled
-### OutlineEquipBorder
+```
+getEnabled(): boolean
+```
-[`bool`](https://create.roblox.com/docs/luau/booleans)
+Gets if the backpack is enabled.
-Determines whether or not the equip border is outline or inset when a slot is equipped. Changing this will make the equip border either border will outline or inset the slot.
+### setEnabled
-### TextColor3
+```
+setEnabled(enabled: boolean): ()
+```
-[`Color3`](https://create.roblox.com/docs/reference/engine/datatypes/Color3)
+Sets whether the backpack is enabled or not.
-Determines the color of the text in default inventory window and slots. This will change the color of all text.
+### getTheme
-### TextSize
+```
+getTheme(): StyleSheet
+```
-[`number`](https://create.roblox.com/docs/luau/numbers)
+Gets the active StyleSheet for the backpack theme.
-Determines the size of the text in the default inventory window and slots. This will change the text size of the tool names and will not change other text like search text, hotkey number, and gamepad hints.
+### setTheme
-### TextStrokeColor3
+```
+setTheme(theme: "DefaultTheme" | "LegacyTheme" | StyleSheet): ()
+```
-[`Color3`](https://create.roblox.com/docs/reference/engine/datatypes/Color3)
+Configures the backpack theme with a StyleSheet.
-Determines the color of the text stroke of text in default inventory window and slots. This will change the color of all text strokes which are visible.
+### getTopbarIcon
-### TextStrokeTransparency
+```
+getTopbarIcon(): TopbarPlus.Icon
+```
-[`number`](https://create.roblox.com/docs/luau/numbers)
+Gets the [TopbarPlus icon] used to toggle the backpack.
-Determines the transparency of the text stroke of text in default chat window and slots. This will change all text strokes in which text strokes are visible.
+ [TopbarPlus icon]: https://1foreverhd.github.io/TopbarPlus/api/
-## Methods
+### openInventory
-### IsOpened
+```
+openInventory(): ()
+```
-Returns whether the inventory is opened or not.
+Opens the inventory.
-#### Returns
+### closeInventory
-
+### inventoryOpened
-### GetStateChangedEvent
+```
+inventoryOpened(): BindableEvent
+```
-Returns a signal that fires when the inventory is opened or closed.
+Fires when the player opens the inventory or when [openInventory](#openinventory) is called.
-#### Code Samples
+### inventoryClosed
-This code sample detects when the inventory is opened or closed.
+```
+inventoryClosed(): BindableEvent
+```
-``` lua title="Detect Inventory State"
-local ReplicatedStorage = game:GetService("ReplicatedStorage")
+Fires when the player closes the inventory or when [closeInventory](#closeinventory) is called.
-local Satchel = require(ReplicatedStorage.Satchel)
+### themeChanged
-Satchel.GetStateChangedEvent():Connect(function(isOpened: boolean)
- if isOpened then
- print("Inventory opened")
- else
- print("Inventory closed")
- end
-end)
+```
+themeChanged(newTheme: StyleSheet, oldTheme: StyleSheet): BindableEvent
```
-#### Returns
-
-
+Fires when the backpack theme is changed.
diff --git a/docs/assets/favicon.svg b/docs/assets/favicon.svg
index 41701e53..562663dc 100644
--- a/docs/assets/favicon.svg
+++ b/docs/assets/favicon.svg
@@ -1,4 +1,4 @@
-
diff --git a/docs/index.md b/docs/index.md
index 162e07c4..303b5de2 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -3,87 +3,11 @@
margin: 4em 0;
text-align: center;
}
-
- /* Fix iframe background transparency */
- iframe {
- color-scheme: light;
- }
-Satchel is a modern open-source alternative to Roblox's default backpack.
-
-Satchel aims to be more customizable and easier to use than the default backpack while still having a "vanilla" feel. Installation of Satchel is as simple as dropping the module into your game and setting up a few properties if you like to customize it. It has a familiar feel and structure as to the default backpack for ease of use for both developers and players.
-
-This documentation will allow you to install Satchel and learn about how to script using Satchel.
-
-
-
-
-
-
-
----
-
-
-
-- :material-clock-fast:{ .lg .middle } __Fast and easy installation__
-
- ---
-
- Drag and drop installation from the [Creator Store] or [GitHub Releases]
-
- [:material-arrow-right: Installation](installation.md)
-
- [Creator Store]: https://create.roblox.com/store/asset/13947506401
- [GitHub Releases]: https://github.com/ryanlua/satchel/releases
-
-- :material-devices:{ .lg .middle } __Full device support__
-
- ---
-
- Compatible with computer, phone, tablet, console, and VR
-
- [:material-arrow-right: Platforms](platforms.md)
-
-- :material-toolbox-outline:{ .lg .middle } __Highly customizable__
-
- ---
-
- Change colors, fonts, and more using [instance attributes]
-
- [:material-arrow-right: Customization](usage.md#customization)
-
- [instance attributes]: https://create.roblox.com/docs/studio/instance-attributes
-
-- :material-scale-balance:{ .lg .middle } __Free and open-source__
-
- ---
-
- Open source for everyone to use and available on [GitHub]
-
- [:material-arrow-right: License](https://github.com/ryanlua/satchel#MPL-2.0-1-ov-file)
-
- [GitHub]: https://github.com/ryanlua/satchel
-
-
-
-
-
----
-
## Our Sponsors
-Special thanks for our sponsors for supporting Satchel and it's future development. We distribute Satchel and provide updates for free, for anyone to use or modify.
+Special thanks to our sponsors for supporting Satchel and its future development. We distribute Satchel and provide updates for free, for anyone to use or modify.
diff --git a/docs/installation.md b/docs/installation.md
index 51091bbc..bb3f8619 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -65,13 +65,6 @@ The Creator Store is the easiest way to install Satchel. It is a one-click insta
You are expected to already have Wally setup in your Rojo project and basic knowledge on how to use Wally packages.
-!!! warning
-
- Wally does not include the loader script so you need to [`#!lua require()`][require] Satchel to run:
- ``` lua title="Satchel Loader"
- require(script.Satchel)
- ```
-
1. Open your Rojo project in the code editor of your choice.
1. In the `wally.toml` file, add the [latest Wally version for Satchel][Wally]. Your dependencies should look similar to this:
diff --git a/docs/introduction.md b/docs/introduction.md
deleted file mode 100644
index ce5dfd1e..00000000
--- a/docs/introduction.md
+++ /dev/null
@@ -1,25 +0,0 @@
-Welcome to Satchel, a modern alternative to Roblox's default backpack.
-
-Satchel and its documentation are always a work in progress, but you can help too. See the [contributing guidelines](https://github.com/ryanlua/satchel/blob/main/.github/CONTRIBUTING.md) to find how you can improve Satchel.
-
-Just want to use Satchel? Check out [Installation].
-
- [Installation]: installation.md
-
-## Improvements
-
-* Modernized and refreshed UI
-* Customization using instance attributes
-* Methods and events, previously locked to CoreGui
-* Script readability and type improvements
-* Rojo sync and Wally support
-
-All open source and free for you to use in your own Roblox experiences.
-
-## Satchel over Default
-
-While the default backpack does its job, customizing the UI or editing the script is extremely difficult. Did you know that the backpack they are using today is from 2015? (With lots of bandaids and patches of course.) Satchel acts as a modernized version that aims to be much more friendly while still maintaining as many features and compatibility.
-
-## CoreGui Relation
-
-From a scripting perspective, Satchel is more of an advanced fork of the CoreGui with Satchel borrowing a majority of its codebase from the default. It's not entirely copy and paste job though. Type annotations and performance optimizations set Satchel apart along with its number of UI tweaks and refactors in place.
diff --git a/docs/overrides/main.html b/docs/overrides/main.html
deleted file mode 100644
index 243a9659..00000000
--- a/docs/overrides/main.html
+++ /dev/null
@@ -1,5 +0,0 @@
-{% extends "base.html" %}
-
-{% block announce %}
- Satchel v1 is unmaintained to focus on v2 development. For a maintained alternative, see Purse.
-{% endblock %}
\ No newline at end of file
diff --git a/docs/platforms.md b/docs/platforms.md
deleted file mode 100644
index ad5fc599..00000000
--- a/docs/platforms.md
+++ /dev/null
@@ -1,52 +0,0 @@
-We support all platforms that Roblox supports. Computers, phones, tablets, consoles, and VR are all supported by Satchel right out of the box. Where the default backpack should run, so should Satchel.
-
-!!! note
-
- Do you see a bug specific to a platform? [Open a bug report] we'll look into it.
-
- [Open a bug report]: https://github.com/ryanlua/satchel/issues/new
-
-## Current supported devices
-
-All platforms on Roblox are supported by Satchel, limited only by screen size. Below is a list of devices along with the accompanying interface and minimum screen size.
-
-### Computer
-
-Support for all computers with 1024 x 768px or larger.
-
-* 1024 x 768px minimum display size
-* Desktop interface
-* 10 hotbar slots
-
-### Phone
-
-Support for Apple iPhone 5 (568 x 320px) or newer.
-
-* 568 x 320px minimum display size
-* Mobile interface
-* 6 hotbar slots
-
-### Tablet
-
-Support for Apple iPad 2 (1024 x 768px) or newer.
-
-* 1024 x 768px minimum display size
-* Mobile interface
-* 10 or 6 hotbar slots (Depending on display size)
-
-### Console
-
-Support for Xbox and PlayStation. Specialized ten-foot interface and hint UI for controllers. Hint UI will automatically adapt to the correct controller buttons.
-
-* Ten-foot interface
-* Controller context hint UI
-* 10 hotbar slots
-
-### VR
-
-VR including Valve Index, Meta Quest 2 and above, and similar.
-
-* Adapted mobile interface
-* Controller context hint UI
-* Custom VR inventory controls
-* 6 hotbar slots
diff --git a/docs/usage.md b/docs/usage.md
deleted file mode 100644
index 5ab91b72..00000000
--- a/docs/usage.md
+++ /dev/null
@@ -1,49 +0,0 @@
-Use of Satchel after installation very easy. Just [publish your experience to Roblox] and see Satchel live in action.
-
-To learn how to install Satchel, see [Installation].
-
-!!! note
-
- Please see [API Reference] for more details on attributes, methods, and events for Satchel and how to use Satchel to it's full potential.
-
- [publish your experience to Roblox]: https://create.roblox.com/docs/production/publishing
- [Installation]: installation.md
- [API Reference]: api-reference.md
-
-### Customization
-
-Satchel is highly customizable & adjustable with [instance attributes] support allowing you to customize the behavior and appearance of over 10+ attributes.
-
-Some of the attributes include:
-
-* Text Color, Size, Stroke Color & Transparency
-* Background Color & Transparency
-* Equip Border Color & Thickness
-* Corner Radius
-* Font
-
-More attributes can be found in the [API Reference]. The list above is not exhaustive and there are may more attributes available for customization.
-
- [instance attributes]: https://create.roblox.com/docs/studio/instance-attributes
-
-
- 
- Example of customization using instance attributes
-
-
-### Scripting
-
-Satchel offers methods and events for scripting purposes. In the below code example we will use the `SetBackpackEnabled` method to disable the Satchel. The script expects the Satchel module to be in [`ReplicatedStorage`][ReplicatedStorage].
-
-``` lua title="Disable Backpack"
-local ReplicatedStorage = game:GetService("ReplicatedStorage")
-
-local Satchel = require(ReplicatedStorage.Satchel)
-
-Satchel.SetBackpackEnabled(false)
-```
-
-For the full API reference, see [API Reference] for more details on attributes, methods, and events for Satchel and how to use Satchel to it's full potential.
-
- [ReplicatedStorage]: https://create.roblox.com/docs/reference/engine/classes/ReplicatedStorage
- [SetBackpackEnabled]: api-reference.md#setbackpackenabled
diff --git a/mkdocs.yml b/mkdocs.yml
index 2ceb0d83..eeedd918 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -32,7 +32,6 @@ theme:
repo: fontawesome/brands/github
favicon: assets/favicon.svg
logo: assets/logo.svg
- custom_dir: docs/overrides
extra:
version:
@@ -60,18 +59,11 @@ markdown_extensions:
- pymdownx.superfences
- toc:
permalink: true
- toc_depth: 3
- - pymdownx.highlight:
- linenums: true
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
nav:
- Home: index.md
- - Introduction: introduction.md
- Installation: installation.md
- - Usage: usage.md
- - Platforms: platforms.md
- - Alternatives: alternatives.md
- API Reference: api-reference.md
diff --git a/models/Satchel/Packages.project.json b/models/Satchel/Packages.project.json
new file mode 100644
index 00000000..52d5154f
--- /dev/null
+++ b/models/Satchel/Packages.project.json
@@ -0,0 +1,9 @@
+{
+ "name": "Packages",
+ "tree": {
+ "$path": "../../Packages",
+ "satchel": {
+ "$path": "../../default.project.json"
+ }
+ }
+}
\ No newline at end of file
diff --git a/models/SatchelLoader/Satchel/init.luau b/models/Satchel/init.luau
similarity index 100%
rename from models/SatchelLoader/Satchel/init.luau
rename to models/Satchel/init.luau
diff --git a/models/SatchelLoader/Satchel/Packages.project.json b/models/SatchelLoader/Satchel/Packages.project.json
deleted file mode 100644
index 84cedaa9..00000000
--- a/models/SatchelLoader/Satchel/Packages.project.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "Packages",
- "tree": {
- "$path": "../../../Packages",
- "satchel": {
- "$path": "../../../src"
- }
- }
-}
\ No newline at end of file
diff --git a/models/SatchelLoader/init.client.luau b/models/SatchelLoader/init.client.luau
deleted file mode 100644
index 9600ce4d..00000000
--- a/models/SatchelLoader/init.client.luau
+++ /dev/null
@@ -1,11 +0,0 @@
---[[
- 💖 Thanks for using Satchel 💖
-
- Satchel is a modern open-source alternative to Roblox's default backpack 🎒
-
- 📰 DevForum: https://devforum.roblox.com/t/2451549
- 🛍️ Creator Store: https://create.roblox.com/store/asset/13947506401
- 🛝 Playground: https://www.roblox.com/join/bxsl5
-]]
-
-require(script.Satchel)
diff --git a/package.project.json b/package.project.json
index e1778089..bd882b22 100644
--- a/package.project.json
+++ b/package.project.json
@@ -2,6 +2,9 @@
"name": "Satchel",
"emitLegacyScripts": false,
"tree": {
- "$path": "models"
+ "$path": "models/Satchel",
+ "ThumbnailCamera": {
+ "$path": "models/ThumbnailCamera.model.json"
+ }
}
}
\ No newline at end of file
diff --git a/src/Api/closeInventory.luau b/src/Api/closeInventory.luau
new file mode 100644
index 00000000..40f5af60
--- /dev/null
+++ b/src/Api/closeInventory.luau
@@ -0,0 +1,13 @@
+--!strict
+
+const RunService = game:GetService("RunService")
+
+const bindableEvents = script.Parent.Parent.BindableEvents
+
+const function closeInventory(): ()
+ assert(RunService:IsClient(), "closeInventory can only be called on the client")
+
+ bindableEvents.InventoryClosed:Fire()
+end
+
+return closeInventory
diff --git a/src/Api/getEnabled.luau b/src/Api/getEnabled.luau
new file mode 100644
index 00000000..a430933a
--- /dev/null
+++ b/src/Api/getEnabled.luau
@@ -0,0 +1,16 @@
+--!strict
+
+const Players = game:GetService("Players")
+const RunService = game:GetService("RunService")
+
+const player = Players.LocalPlayer :: Player
+const playerGui = player:WaitForChild("PlayerGui")
+const screenGui = playerGui:WaitForChild("SatchelGui") :: ScreenGui
+
+const function getEnabled(): boolean
+ assert(RunService:IsClient(), "getEnabled can only be called on the client")
+
+ return screenGui.Enabled
+end
+
+return getEnabled
diff --git a/src/Api/getTheme.luau b/src/Api/getTheme.luau
new file mode 100644
index 00000000..4096389b
--- /dev/null
+++ b/src/Api/getTheme.luau
@@ -0,0 +1,13 @@
+--!strict
+
+const RunService = game:GetService("RunService")
+
+const currentStyleSheet = script.Parent.Parent.Design.SatchelStyleSheet
+
+const function getTheme(): StyleSheet
+ assert(RunService:IsClient(), "getTheme can only be called on the client")
+
+ return currentStyleSheet:FindFirstChildOfClass("StyleDerive").StyleSheet :: StyleSheet
+end
+
+return getTheme
diff --git a/src/Api/getTopbarIcon.luau b/src/Api/getTopbarIcon.luau
new file mode 100644
index 00000000..c3a62a98
--- /dev/null
+++ b/src/Api/getTopbarIcon.luau
@@ -0,0 +1,13 @@
+--!strict
+
+const RunService = game:GetService("RunService")
+
+const TopbarPlus = require("../../topbarplus")
+
+const function getTopbarIcon(): TopbarPlus.Icon
+ assert(RunService:IsClient(), "getTopbarIcon can only be called on the client")
+
+ return TopbarPlus.getIcon("SatchelInventory") :: TopbarPlus.Icon
+end
+
+return getTopbarIcon
diff --git a/src/Api/isInventoryOpen.luau b/src/Api/isInventoryOpen.luau
new file mode 100644
index 00000000..43bff2ce
--- /dev/null
+++ b/src/Api/isInventoryOpen.luau
@@ -0,0 +1,23 @@
+--!strict
+
+const RunService = game:GetService("RunService")
+
+const bindableEvents = script.Parent.Parent.BindableEvents
+
+local inventoryOpen = false
+
+bindableEvents.InventoryOpened.Event:Connect(function()
+ inventoryOpen = true
+end)
+
+bindableEvents.InventoryClosed.Event:Connect(function()
+ inventoryOpen = false
+end)
+
+const function isInventoryOpen(): boolean
+ assert(RunService:IsClient(), "isInventoryOpen can only be called on the client")
+
+ return inventoryOpen
+end
+
+return isInventoryOpen
diff --git a/src/Api/openInventory.luau b/src/Api/openInventory.luau
new file mode 100644
index 00000000..32ebc0a1
--- /dev/null
+++ b/src/Api/openInventory.luau
@@ -0,0 +1,13 @@
+--!strict
+
+const RunService = game:GetService("RunService")
+
+const bindableEvents = script.Parent.Parent.BindableEvents
+
+const function openInventory(): ()
+ assert(RunService:IsClient(), "openInventory can only be called on the client")
+
+ bindableEvents.InventoryOpened:Fire()
+end
+
+return openInventory
diff --git a/src/Api/setEnabled.luau b/src/Api/setEnabled.luau
new file mode 100644
index 00000000..cecffe53
--- /dev/null
+++ b/src/Api/setEnabled.luau
@@ -0,0 +1,23 @@
+--!strict
+
+const Players = game:GetService("Players")
+const RunService = game:GetService("RunService")
+
+const getTopbarIcon = require("./getTopbarIcon")
+
+const player = Players.LocalPlayer :: Player
+const playerGui = player:WaitForChild("PlayerGui")
+const screenGui = playerGui:WaitForChild("SatchelGui") :: ScreenGui
+
+const function setEnabled(enabled: boolean): ()
+ assert(RunService:IsClient(), "setEnabled can only be called on the client")
+
+ screenGui.Enabled = enabled
+
+ const icon = getTopbarIcon()
+ if icon then
+ icon:setEnabled(enabled)
+ end
+end
+
+return setEnabled
diff --git a/src/Api/setTheme.luau b/src/Api/setTheme.luau
new file mode 100644
index 00000000..dfbb4d9b
--- /dev/null
+++ b/src/Api/setTheme.luau
@@ -0,0 +1,20 @@
+--!strict
+
+const RunService = game:GetService("RunService")
+
+const bindableEvents = script.Parent.Parent.BindableEvents
+const styleSheets = script.Parent.Parent.Design
+const currentTheme = styleSheets.SatchelStyleSheet:FindFirstChildOfClass("StyleDerive") :: StyleDerive
+
+type DefaultThemes = "DefaultTheme" | "LegacyTheme"
+
+const function setTheme(theme: DefaultThemes | StyleSheet): ()
+ assert(RunService:IsClient(), "setTheme can only be called on the client")
+
+ const newTheme: StyleSheet = if typeof(theme) == "string" then styleSheets:WaitForChild(theme) :: StyleSheet else theme
+ currentTheme.StyleSheet = newTheme
+
+ bindableEvents.ThemeChanged:Fire(newTheme, currentTheme.StyleSheet)
+end
+
+return setTheme
diff --git a/src/Attribution.client.luau b/src/Attribution.client.luau
index a3e7ef45..e3c4d395 100644
--- a/src/Attribution.client.luau
+++ b/src/Attribution.client.luau
@@ -18,8 +18,29 @@
Thank you for supporting Satchel.
]]
-local RunService = game:GetService("RunService")
+const MarketplaceService = game:GetService("MarketplaceService")
+const RunService = game:GetService("RunService")
-if not RunService:IsStudio() then
- print("💼 Running Satchel v1.4.1 by @WinnersTakesAll")
+const VERSION = "2.0.0"
+
+-- Print attribution. Do not modify without reading above
+if not RunService:IsStudio() and RunService:IsClient() then
+ print(`💼 Running Satchel v{VERSION} by @WinnersTakesAll`)
+end
+
+-- Check for updates. You may modify the below
+local latestVersion: string?
+
+const success, result = pcall(function()
+ return MarketplaceService:GetProductInfoAsync(13947506401)
+end)
+
+if success then
+ latestVersion = string.match(result.Name, "v(%d+%.%d+%.%d+)")
+end
+
+if latestVersion and latestVersion ~= VERSION then
+ warn(
+ `A new version of Satchel (v{VERSION} -> v{latestVersion}) is available: https://create.roblox.com/store/asset/13947506401`
+ )
end
diff --git a/src/BindableEvents/BackpackItemAdded.model.json b/src/BindableEvents/BackpackItemAdded.model.json
new file mode 100644
index 00000000..1399ec04
--- /dev/null
+++ b/src/BindableEvents/BackpackItemAdded.model.json
@@ -0,0 +1,3 @@
+{
+ "ClassName": "BindableEvent"
+}
\ No newline at end of file
diff --git a/src/BindableEvents/BackpackItemEquipped.model.json b/src/BindableEvents/BackpackItemEquipped.model.json
new file mode 100644
index 00000000..1399ec04
--- /dev/null
+++ b/src/BindableEvents/BackpackItemEquipped.model.json
@@ -0,0 +1,3 @@
+{
+ "ClassName": "BindableEvent"
+}
\ No newline at end of file
diff --git a/src/BindableEvents/BackpackItemRemoved.model.json b/src/BindableEvents/BackpackItemRemoved.model.json
new file mode 100644
index 00000000..1399ec04
--- /dev/null
+++ b/src/BindableEvents/BackpackItemRemoved.model.json
@@ -0,0 +1,3 @@
+{
+ "ClassName": "BindableEvent"
+}
\ No newline at end of file
diff --git a/src/BindableEvents/BackpackItemUnequipped.model.json b/src/BindableEvents/BackpackItemUnequipped.model.json
new file mode 100644
index 00000000..1399ec04
--- /dev/null
+++ b/src/BindableEvents/BackpackItemUnequipped.model.json
@@ -0,0 +1,3 @@
+{
+ "ClassName": "BindableEvent"
+}
\ No newline at end of file
diff --git a/src/BindableEvents/InventoryClosed.model.json b/src/BindableEvents/InventoryClosed.model.json
new file mode 100644
index 00000000..1399ec04
--- /dev/null
+++ b/src/BindableEvents/InventoryClosed.model.json
@@ -0,0 +1,3 @@
+{
+ "ClassName": "BindableEvent"
+}
\ No newline at end of file
diff --git a/src/BindableEvents/InventoryOpened.model.json b/src/BindableEvents/InventoryOpened.model.json
new file mode 100644
index 00000000..1399ec04
--- /dev/null
+++ b/src/BindableEvents/InventoryOpened.model.json
@@ -0,0 +1,3 @@
+{
+ "ClassName": "BindableEvent"
+}
\ No newline at end of file
diff --git a/src/BindableEvents/ThemeChanged.model.json b/src/BindableEvents/ThemeChanged.model.json
new file mode 100644
index 00000000..1399ec04
--- /dev/null
+++ b/src/BindableEvents/ThemeChanged.model.json
@@ -0,0 +1,3 @@
+{
+ "ClassName": "BindableEvent"
+}
\ No newline at end of file
diff --git a/src/Bindings/HotbarContext.model.json b/src/Bindings/HotbarContext.model.json
new file mode 100644
index 00000000..165abab4
--- /dev/null
+++ b/src/Bindings/HotbarContext.model.json
@@ -0,0 +1,47 @@
+{
+ "ClassName": "InputContext",
+ "Properties": {
+ "Priority": 2000
+ },
+ "Children": [
+ {
+ "Name": "SlotLeftAction",
+ "ClassName": "InputAction",
+ "Children": [
+ {
+ "Name": "GamepadBinding",
+ "ClassName": "InputBinding",
+ "Properties": {
+ "KeyCode": "ButtonL1"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "SlotRightAction",
+ "ClassName": "InputAction",
+ "Children": [
+ {
+ "Name": "GamepadBinding",
+ "ClassName": "InputBinding",
+ "Properties": {
+ "KeyCode": "ButtonR1"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "ToggleInventoryAction",
+ "ClassName": "InputAction",
+ "Children": [
+ {
+ "Name": "KeyBinding",
+ "ClassName": "InputBinding",
+ "Properties": {
+ "KeyCode": "Backquote"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/Bindings/InventoryContext.model.json b/src/Bindings/InventoryContext.model.json
new file mode 100644
index 00000000..e84b8a4e
--- /dev/null
+++ b/src/Bindings/InventoryContext.model.json
@@ -0,0 +1,47 @@
+{
+ "ClassName": "InputContext",
+ "Properties": {
+ "Priority": 2000
+ },
+ "Children": [
+ {
+ "Name": "CloseInventoryAction",
+ "ClassName": "InputAction",
+ "Children": [
+ {
+ "Name": "GamepadBinding",
+ "ClassName": "InputBinding",
+ "Properties": {
+ "KeyCode": "ButtonB"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "RemoveFromHotbarAction",
+ "ClassName": "InputAction",
+ "Children": [
+ {
+ "Name": "GamepadBinding",
+ "ClassName": "InputBinding",
+ "Properties": {
+ "KeyCode": "ButtonX"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "SelectSwapAction",
+ "ClassName": "InputAction",
+ "Children": [
+ {
+ "Name": "GamepadBinding",
+ "ClassName": "InputBinding",
+ "Properties": {
+ "KeyCode": "ButtonA"
+ }
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Client.client.luau b/src/Client.client.luau
new file mode 100644
index 00000000..3b6754c5
--- /dev/null
+++ b/src/Client.client.luau
@@ -0,0 +1,119 @@
+const GuiService = game:GetService("GuiService")
+const Players = game:GetService("Players")
+const RunService = game:GetService("RunService")
+const StarterGui = game:GetService("StarterGui")
+const UserInputService = game:GetService("UserInputService")
+
+-- Enable Dev Mode and React DevTools in Studio
+if RunService:IsStudio() then
+ const ReactGlobals = require("../react-globals")
+ ReactGlobals.__DEV__ = true
+ ReactGlobals.__PROFILE__ = true
+ require("../react-devtools")
+end
+
+const React = require("../react")
+const ReactRoblox = require("../react-roblox")
+const closeInventory = require("./Api/closeInventory")
+
+const App = require("./Components/App")
+
+const DESIGN_SHEET = script.Parent.Design.SatchelStyleSheet
+
+const player = Players.LocalPlayer :: Player
+const playerGui = player:WaitForChild("PlayerGui") :: PlayerGui
+
+StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, false)
+
+const handle = Instance.new("ScreenGui")
+handle.Name = "SatchelGui"
+handle.ResetOnSpawn = false
+handle.Parent = playerGui
+
+const root = ReactRoblox.createRoot(handle)
+
+const function Satchel()
+ -- Open and close backpack based on bindable events
+ const opened, setOpened = React.useState(false)
+
+ React.useEffect(function()
+ const bindableEvents = script.Parent.BindableEvents
+
+ const inventoryOpenedSignal = bindableEvents.InventoryOpened.Event:Connect(function()
+ setOpened(true)
+ end)
+ const inventoryClosedSignal = bindableEvents.InventoryClosed.Event:Connect(function()
+ setOpened(false)
+ end)
+ const menuOpenedSignal = GuiService.MenuOpened:Connect(function()
+ closeInventory()
+ end)
+
+ return function()
+ inventoryOpenedSignal:Disconnect()
+ inventoryClosedSignal:Disconnect()
+ menuOpenedSignal:Disconnect()
+ end
+ end, {})
+
+ -- Close backpack when clicking outside of it
+ React.useEffect(function()
+ const inputSignal = UserInputService.InputBegan:Connect(function(input, gameProcessedEvent)
+ const inputType = input.UserInputType
+ if not gameProcessedEvent then
+ if inputType == Enum.UserInputType.MouseButton1 or inputType == Enum.UserInputType.Touch then
+ closeInventory()
+ end
+ end
+ end)
+
+ return function()
+ inputSignal:Disconnect()
+ end
+ end, {})
+
+ -- Change slots based on viewport display size
+ const slots, setSlots = React.useState(10)
+ const rows, setRows = React.useState(4)
+
+ React.useEffect(function()
+ const function updateBackpackSize()
+ const screenOrientation = playerGui.CurrentScreenOrientation
+ const viewportSize = GuiService.ViewportDisplaySize
+
+ if screenOrientation == Enum.ScreenOrientation.Portrait then
+ setSlots(3)
+ setRows(3)
+ elseif viewportSize == Enum.DisplaySize.Small then
+ setSlots(5)
+ setRows(2)
+ else
+ setSlots(10)
+ setRows(4)
+ end
+ end
+
+ updateBackpackSize()
+ const viewportSignal = GuiService:GetPropertyChangedSignal("ViewportDisplaySize"):Connect(updateBackpackSize)
+ const orientationSignal =
+ playerGui:GetPropertyChangedSignal("CurrentScreenOrientation"):Connect(updateBackpackSize)
+
+ return function()
+ viewportSignal:Disconnect()
+ orientationSignal:Disconnect()
+ end
+ end, {})
+
+ return React.createElement(App, {
+ slots = slots,
+ rows = rows,
+ opened = opened,
+ })
+end
+
+root:render(React.createElement(React.Fragment, nil, {
+ StyleLink = React.createElement("StyleLink", {
+ StyleSheet = DESIGN_SHEET,
+ }),
+ App = React.createElement(Satchel),
+}))
diff --git a/src/Components/App.luau b/src/Components/App.luau
new file mode 100644
index 00000000..624310a3
--- /dev/null
+++ b/src/Components/App.luau
@@ -0,0 +1,94 @@
+const React = require("../../react")
+
+const Backpack = require("./Backpack")
+const HotbarHint = require("./HotbarHint")
+const InventoryHint = require("./InventoryHint")
+
+const InventoryBindings = script.Parent.Parent.Bindings.InventoryContext
+const RemoveFromHotbarGamepadKeyCode = InventoryBindings.RemoveFromHotbarAction.GamepadBinding.KeyCode
+const SelectSwapGamepadKeyCode = InventoryBindings.SelectSwapAction.GamepadBinding.KeyCode
+const CloseInventoryGamepadKeyCode = InventoryBindings.CloseInventoryAction.GamepadBinding.KeyCode
+
+const HotbarBindings = script.Parent.Parent.Bindings.HotbarContext
+const SlotLeftGamepadKeyCode = HotbarBindings.SlotLeftAction.GamepadBinding.KeyCode
+const SlotRightGamepadKeyCode = HotbarBindings.SlotRightAction.GamepadBinding.KeyCode
+
+export type Props = {
+ forceGamepadHintVisible: boolean?,
+ forceKeyboardHintVisible: boolean?,
+ slots: number?,
+ rows: number?,
+ items: { Tool | HopperBin }?,
+ opened: boolean?,
+}
+
+const function App(props: Props)
+ const backpackSize, setBackpackSize = React.useState(Vector2.zero)
+ const slotCount = props.slots or 10
+
+ local hotbarVisible = props.opened or false
+
+ -- Don't show hotbar hints if hotbar has no slots
+ if not hotbarVisible and props.items then
+ for order = 1, slotCount do
+ if props.items[order] ~= nil then
+ hotbarVisible = true
+ break
+ end
+ end
+ end
+
+ return React.createElement("Frame", {
+ [React.Tag] = "HotbarHints",
+ }, {
+ SlotLeftHint = React.createElement(HotbarHint, {
+ keyCode = SlotLeftGamepadKeyCode,
+ forceVisible = props.forceGamepadHintVisible,
+ order = -1,
+ visible = hotbarVisible,
+ }),
+ SlotRightHint = React.createElement(HotbarHint, {
+ keyCode = SlotRightGamepadKeyCode,
+ forceVisible = props.forceGamepadHintVisible,
+ order = 1,
+ visible = hotbarVisible,
+ }),
+ InventoryHints = React.createElement("Frame", {
+ Size = UDim2.fromOffset(backpackSize.X, 0),
+ [React.Tag] = "InventoryHints",
+ }, {
+ Hints = React.createElement("Frame", {
+ Size = UDim2.fromOffset(backpackSize.X, 0),
+ Visible = props.opened,
+ LayoutOrder = -1,
+ [React.Tag] = "Hints",
+ }, {
+ RemoveFromHotbarHint = React.createElement(InventoryHint, {
+ keyCode = RemoveFromHotbarGamepadKeyCode,
+ forceVisible = props.forceGamepadHintVisible,
+ text = "Remove from hotbar",
+ }),
+ SelectSwapHint = React.createElement(InventoryHint, {
+ keyCode = SelectSwapGamepadKeyCode,
+ forceVisible = props.forceGamepadHintVisible,
+ text = "Select/Swap",
+ }),
+ CloseInventoryHint = React.createElement(InventoryHint, {
+ keyCode = CloseInventoryGamepadKeyCode,
+ forceVisible = props.forceGamepadHintVisible,
+ text = "Close inventory",
+ }),
+ }),
+ Backpack = React.createElement(Backpack, {
+ forceKeyboardHintVisible = props.forceKeyboardHintVisible,
+ slots = props.slots,
+ rows = props.rows,
+ items = props.items,
+ opened = props.opened,
+ onAbsoluteSizeChanged = setBackpackSize,
+ }),
+ }),
+ })
+end
+
+return App
diff --git a/src/Components/App.story.luau b/src/Components/App.story.luau
new file mode 100644
index 00000000..1aaabecf
--- /dev/null
+++ b/src/Components/App.story.luau
@@ -0,0 +1,37 @@
+const React = require("../../react")
+
+const App = require("./App")
+
+const controls = {
+ hintVisible = true,
+ slots = 10,
+ toolName = "Sword",
+ toolImage = "rbxasset://Textures/Sword128.png",
+ toolTooltip = "A classic sword",
+ toolSlot = 1,
+ opened = true,
+ rows = 4,
+}
+
+type Props = {
+ controls: typeof(controls),
+}
+
+return {
+ summary = "Hotbar and inventory, including hints for gamepad controls.",
+ controls = controls,
+ story = function(props: Props)
+ const tool = Instance.new("Tool")
+ tool.Name = props.controls.toolName
+ tool.TextureId = props.controls.toolImage
+ tool.ToolTip = props.controls.toolTooltip
+
+ return React.createElement(App, {
+ forceGamepadHintVisible = props.controls.hintVisible,
+ slots = props.controls.slots,
+ items = { [props.controls.toolSlot] = tool },
+ opened = props.controls.opened,
+ rows = props.controls.rows,
+ })
+ end,
+}
diff --git a/src/Components/Backpack.luau b/src/Components/Backpack.luau
new file mode 100644
index 00000000..6b2c770e
--- /dev/null
+++ b/src/Components/Backpack.luau
@@ -0,0 +1,55 @@
+const React = require("../../react")
+
+const Hotbar = require("./Hotbar")
+const Inventory = require("./Inventory")
+
+export type Props = {
+ forceKeyboardHintVisible: boolean?,
+ slots: number?,
+ rows: number?, -- TODO: Add rows for inventory height
+ items: { Tool | HopperBin }?,
+ opened: boolean?,
+ onAbsoluteSizeChanged: ((Vector2) -> ())?,
+}
+
+const function Backpack(props: Props)
+ const opened = props.opened or false
+ const slots = props.slots or 10
+
+ -- Items at indices 1..slots are hotbar; items above slots are inventory.
+ const hotbarItems: { [number]: Tool | HopperBin } = {}
+ const inventoryItems: { [number]: Tool | HopperBin } = {}
+ if props.items then
+ for index, item in props.items do
+ if item and index > 0 and index <= slots then
+ hotbarItems[index] = item
+ elseif item and index > slots then
+ inventoryItems[index - slots] = item
+ end
+ end
+ end
+
+ return React.createElement("Frame", {
+ [React.Tag] = "Backpack",
+ [React.Change.AbsoluteSize] = function(rbx)
+ if props.onAbsoluteSizeChanged then
+ props.onAbsoluteSizeChanged(rbx.AbsoluteSize)
+ end
+ end :: any,
+ }, {
+ Hotbar = React.createElement(Hotbar, {
+ forceKeyboardHintVisible = props.forceKeyboardHintVisible,
+ slots = slots,
+ items = hotbarItems,
+ opened = opened,
+ }),
+ Inventory = React.createElement(Inventory, {
+ slots = slots,
+ rows = props.rows,
+ items = inventoryItems,
+ opened = opened,
+ }),
+ })
+end
+
+return Backpack
diff --git a/src/Components/Backpack.story.luau b/src/Components/Backpack.story.luau
new file mode 100644
index 00000000..4d37936a
--- /dev/null
+++ b/src/Components/Backpack.story.luau
@@ -0,0 +1,35 @@
+const React = require("../../react")
+
+const Backpack = require("./Backpack")
+
+const controls = {
+ slots = 10,
+ toolName = "Sword",
+ toolImage = "rbxasset://Textures/Sword128.png",
+ toolTooltip = "A classic sword",
+ toolSlot = 1,
+ opened = true,
+ rows = 4,
+}
+
+type Props = {
+ controls: typeof(controls),
+}
+
+return {
+ summary = "Backpack that holds the hotbar and inventory",
+ controls = controls,
+ story = function(props: Props)
+ const tool = Instance.new("Tool")
+ tool.Name = props.controls.toolName
+ tool.TextureId = props.controls.toolImage
+ tool.ToolTip = props.controls.toolTooltip
+
+ return React.createElement(Backpack, {
+ slots = props.controls.slots,
+ items = { [props.controls.toolSlot] = tool },
+ opened = props.controls.opened,
+ rows = props.controls.rows,
+ })
+ end,
+}
diff --git a/src/Components/Hotbar.luau b/src/Components/Hotbar.luau
new file mode 100644
index 00000000..da9acf2c
--- /dev/null
+++ b/src/Components/Hotbar.luau
@@ -0,0 +1,39 @@
+const React = require("../../react")
+
+const Slot = require("./Slot")
+
+export type Props = {
+ forceKeyboardHintVisible: boolean?,
+ slots: number?,
+ items: { Tool | HopperBin }?,
+ opened: boolean?,
+}
+
+const function Hotbar(props: Props)
+ const slotCount = props.slots or 10
+
+ const children: any = {}
+
+ -- Create hotbar slots
+ for order = 1, slotCount do
+ const item = props.items and props.items[order]
+ const slotUnlocked = props.opened and item ~= nil
+
+ -- Only show slots if the hotbar is opened or if there is a tool in the slot
+ if props.opened or item then
+ children[tostring(order)] = React.createElement(Slot, {
+ order = order,
+ item = item,
+ hint = true,
+ forceHintVisible = props.forceKeyboardHintVisible,
+ unlocked = slotUnlocked,
+ })
+ end
+ end
+
+ return React.createElement("Frame", {
+ [React.Tag] = "Hotbar",
+ }, children)
+end
+
+return Hotbar
diff --git a/src/Components/Hotbar.story.luau b/src/Components/Hotbar.story.luau
new file mode 100644
index 00000000..27910a56
--- /dev/null
+++ b/src/Components/Hotbar.story.luau
@@ -0,0 +1,33 @@
+const React = require("../../react")
+
+const Hotbar = require("./Hotbar")
+
+const controls = {
+ slots = 10,
+ toolName = "Sword",
+ toolImage = "rbxasset://Textures/Sword128.png",
+ tooltipText = "A classic sword",
+ toolSlot = 1,
+ opened = true,
+}
+
+type Props = {
+ controls: typeof(controls),
+}
+
+return {
+ summary = "Hotbar on the bottom of the screen that shows equipped tools and hints for equipping",
+ controls = controls,
+ story = function(props: Props)
+ const tool = Instance.new("Tool")
+ tool.Name = props.controls.toolName
+ tool.TextureId = props.controls.toolImage
+ tool.ToolTip = props.controls.tooltipText
+
+ return React.createElement(Hotbar, {
+ slots = props.controls.slots,
+ items = { [props.controls.toolSlot] = tool },
+ opened = props.controls.opened,
+ })
+ end,
+}
diff --git a/src/Components/HotbarHint.luau b/src/Components/HotbarHint.luau
new file mode 100644
index 00000000..e3e82ae8
--- /dev/null
+++ b/src/Components/HotbarHint.luau
@@ -0,0 +1,29 @@
+const UserInputService = game:GetService("UserInputService")
+
+const React = require("../../react")
+
+export type Props = {
+ keyCode: Enum.KeyCode,
+ forceVisible: boolean?,
+ order: number?,
+ visible: boolean?,
+}
+
+const function HotbarHint(props: Props)
+ local tags = "HotbarHint"
+ if props.forceVisible then
+ tags = tags .. " Visible"
+ end
+
+ return React.createElement("Frame", {
+ LayoutOrder = props.order,
+ Visible = props.visible,
+ [React.Tag] = tags,
+ }, {
+ KeyCode = React.createElement("ImageLabel", {
+ Image = UserInputService:GetImageForKeyCode(props.keyCode),
+ }),
+ })
+end
+
+return HotbarHint
diff --git a/src/Components/HotbarHint.story.luau b/src/Components/HotbarHint.story.luau
new file mode 100644
index 00000000..9e49c66b
--- /dev/null
+++ b/src/Components/HotbarHint.story.luau
@@ -0,0 +1,14 @@
+const React = require("../../react")
+
+const HotbarHint = require("./HotbarHint")
+
+return {
+ name = "Hotbar Hint",
+ summary = "Hint shown in the hotbar to show how to equip slots",
+ story = function()
+ return React.createElement(HotbarHint, {
+ keyCode = Enum.KeyCode.ButtonX,
+ forceVisible = true,
+ })
+ end,
+}
diff --git a/src/Components/Inventory.luau b/src/Components/Inventory.luau
new file mode 100644
index 00000000..3204590f
--- /dev/null
+++ b/src/Components/Inventory.luau
@@ -0,0 +1,45 @@
+const React = require("../../react")
+
+const Searchbar = require("./Searchbar")
+const Slot = require("./Slot")
+
+export type Props = {
+ width: number?,
+ rows: number?,
+ items: { Tool | HopperBin }?,
+ opened: boolean?,
+}
+
+const function Inventory(props: Props)
+ const width = props.width or 0
+ const rows = props.rows or 4
+ const height = rows * 65 - 5
+
+ const children: any = {}
+
+ -- Create inventory slots
+ if props.items then
+ for order, item in props.items do
+ if item then
+ children[tostring(order)] = React.createElement(Slot, {
+ unlocked = true,
+ order = order,
+ item = item,
+ })
+ end
+ end
+ end
+
+ return React.createElement("Frame", {
+ Size = UDim2.fromOffset(width, 0),
+ Visible = props.opened,
+ [React.Tag] = "Inventory",
+ }, {
+ SearchBox = React.createElement(Searchbar),
+ SlotFrame = React.createElement("ScrollingFrame", {
+ Size = UDim2.new(1, 0, 0, height),
+ }, children),
+ })
+end
+
+return Inventory
diff --git a/src/Components/Inventory.story.luau b/src/Components/Inventory.story.luau
new file mode 100644
index 00000000..3757072b
--- /dev/null
+++ b/src/Components/Inventory.story.luau
@@ -0,0 +1,31 @@
+const React = require("../../react")
+
+const Inventory = require("./Inventory")
+
+const controls = {
+ toolName = "Sword",
+ toolImage = "rbxasset://Textures/Sword128.png",
+ tooltipText = "A classic sword",
+ rows = 4,
+}
+
+type Props = {
+ controls: typeof(controls),
+}
+
+return {
+ summary = "Toggleable inventory for displaying items not in the hotbar",
+ controls = controls,
+ story = function(props: Props)
+ const tool = Instance.new("Tool")
+ tool.Name = props.controls.toolName
+ tool.TextureId = props.controls.toolImage
+ tool.ToolTip = props.controls.tooltipText
+
+ return React.createElement(Inventory, {
+ rows = props.controls.rows,
+ width = 655,
+ items = { [1] = tool },
+ })
+ end,
+}
diff --git a/src/Components/InventoryHint.luau b/src/Components/InventoryHint.luau
new file mode 100644
index 00000000..b729ba5c
--- /dev/null
+++ b/src/Components/InventoryHint.luau
@@ -0,0 +1,31 @@
+const UserInputService = game:GetService("UserInputService")
+
+const React = require("../../react")
+
+export type Props = {
+ keyCode: Enum.KeyCode,
+ text: string,
+ forceVisible: boolean?,
+ order: number?,
+}
+
+const function InventoryHint(props: Props)
+ local tags = "InventoryHint"
+ if props.forceVisible then
+ tags = tags .. " Visible"
+ end
+
+ return React.createElement("Frame", {
+ LayoutOrder = props.order,
+ [React.Tag] = tags,
+ }, {
+ KeyCode = React.createElement("ImageLabel", {
+ Image = UserInputService:GetImageForKeyCode(props.keyCode),
+ }),
+ Action = React.createElement("TextLabel", {
+ Text = props.text,
+ }),
+ })
+end
+
+return InventoryHint
diff --git a/src/Components/InventoryHint.story.luau b/src/Components/InventoryHint.story.luau
new file mode 100644
index 00000000..215ffa3e
--- /dev/null
+++ b/src/Components/InventoryHint.story.luau
@@ -0,0 +1,24 @@
+const React = require("../../react")
+
+const InventoryHint = require("./InventoryHint")
+
+const controls = {
+ text = "Remove from Hotbar",
+}
+
+type Props = {
+ controls: typeof(controls),
+}
+
+return {
+ name = "Inventory Hint",
+ summary = "Hint shown in the inventory",
+ controls = controls,
+ story = function(props: Props)
+ return React.createElement(InventoryHint, {
+ keyCode = Enum.KeyCode.ButtonX,
+ forceVisible = true,
+ text = props.controls.text,
+ })
+ end,
+}
diff --git a/src/Components/Satchel.storybook.luau b/src/Components/Satchel.storybook.luau
new file mode 100644
index 00000000..eea63c37
--- /dev/null
+++ b/src/Components/Satchel.storybook.luau
@@ -0,0 +1,29 @@
+const React = require("../../react")
+const ReactRoblox = require("../../react-roblox")
+
+const DESIGN_SHEET = script.Parent.Parent.Design.SatchelStyleSheet
+
+return {
+ name = "Satchel",
+ mapStory = function(Story)
+ return function(storyProps)
+ return React.createElement(React.Fragment, nil, {
+ UIListLayout = React.createElement("UIListLayout", {
+ HorizontalAlignment = Enum.HorizontalAlignment.Center,
+ VerticalAlignment = Enum.VerticalAlignment.Center,
+ }),
+ StyleLink = React.createElement("StyleLink", {
+ StyleSheet = DESIGN_SHEET,
+ }),
+ Story = React.createElement(Story, storyProps),
+ })
+ end
+ end,
+ storyRoots = {
+ script.Parent,
+ },
+ packages = {
+ React = React,
+ ReactRoblox = ReactRoblox,
+ },
+}
diff --git a/src/Components/Searchbar.luau b/src/Components/Searchbar.luau
new file mode 100644
index 00000000..172b3b75
--- /dev/null
+++ b/src/Components/Searchbar.luau
@@ -0,0 +1,27 @@
+const React = require("../../react")
+
+export type Props = {
+ size: UDim2?,
+}
+
+const function Searchbar(props: Props)
+ const text, setText = React.useState("")
+
+ return React.createElement("TextBox", {
+ Size = props.size,
+ Text = text,
+ [React.Tag] = "Searchbar",
+ [React.Change.Text] = function(rbx)
+ setText(rbx.Text)
+ end :: any,
+ }, {
+ Clear = React.createElement("ImageButton", {
+ Visible = text ~= "",
+ [React.Event.Activated] = function()
+ setText("")
+ end,
+ }),
+ })
+end
+
+return Searchbar
diff --git a/src/Components/Searchbar.story.luau b/src/Components/Searchbar.story.luau
new file mode 100644
index 00000000..5b7a2c8a
--- /dev/null
+++ b/src/Components/Searchbar.story.luau
@@ -0,0 +1,12 @@
+const React = require("../../react")
+
+const Searchbar = require("./Searchbar")
+
+return {
+ summary = "Search for items in the inventory",
+ story = function()
+ return React.createElement(Searchbar, {
+ size = UDim2.fromOffset(190, 30),
+ })
+ end,
+}
diff --git a/src/Components/Slot.luau b/src/Components/Slot.luau
new file mode 100644
index 00000000..3d01adff
--- /dev/null
+++ b/src/Components/Slot.luau
@@ -0,0 +1,72 @@
+const React = require("../../react")
+
+const Tooltip = require("./Tooltip")
+
+export type Props = {
+ item: (Tool | HopperBin)?,
+ equipped: boolean?,
+ unlocked: boolean?,
+ forceHintVisible: boolean?,
+ hint: boolean?,
+ order: number?,
+}
+
+const function Slot(props: Props)
+ const item = props.item
+ const itemName = item and item.Name
+ const itemImage = item and item.TextureId
+ const tooltipText = item and (item:IsA("Tool") and item.ToolTip or "")
+
+ local equipped, setEquipped = React.useState(props.equipped or false)
+
+ -- Only show numbers 1-10 for hints and show 0 for the 10th slot
+ const order = props.order or 1
+ local slotNumber = ""
+ if order >= 1 and order < 10 then
+ slotNumber = tostring(order)
+ elseif order == 10 then
+ slotNumber = "0"
+ end
+
+ const hintVisible = props.hint == true and props.forceHintVisible
+
+ -- Generate tags based on state
+ local tags = "Slot"
+ if props.unlocked then
+ tags = tags .. " Unlocked"
+ end
+ if equipped then
+ tags = tags .. " Equipped"
+ end
+
+ -- Hide name if there is an image
+ local slotText = itemName
+ if itemImage ~= "" then
+ slotText = ""
+ end
+
+ return React.createElement("TextButton", {
+ Text = slotText,
+ LayoutOrder = props.order,
+ [React.Tag] = tags,
+ [React.Event.Activated] = function()
+ if props.item then
+ setEquipped(not equipped)
+ end
+ end :: any,
+ }, {
+ NumberHint = React.createElement("TextLabel", {
+ Text = slotNumber,
+ Visible = hintVisible,
+ [React.Tag] = "SlotNumber",
+ }),
+ TextureIcon = React.createElement("ImageLabel", {
+ Image = itemImage or "",
+ }),
+ ToolTip = React.createElement(Tooltip, {
+ text = tooltipText,
+ }),
+ })
+end
+
+return Slot
diff --git a/src/Components/Slot.story.luau b/src/Components/Slot.story.luau
new file mode 100644
index 00000000..95962cec
--- /dev/null
+++ b/src/Components/Slot.story.luau
@@ -0,0 +1,36 @@
+const React = require("../../react")
+
+const Slot = require("./Slot")
+
+const controls = {
+ toolName = "Sword",
+ toolImage = "rbxasset://Textures/Sword128.png",
+ toolTooltip = "A classic sword",
+ equipped = false,
+ unlocked = false,
+ hint = true,
+ order = 1,
+}
+
+type Props = {
+ controls: typeof(controls),
+}
+
+return {
+ summary = "Slot representing a single item in the hotbar or inventory, showing the tool and hints for equipping",
+ controls = controls,
+ story = function(props: Props)
+ const tool = Instance.new("Tool")
+ tool.Name = props.controls.toolName
+ tool.TextureId = props.controls.toolImage
+ tool.ToolTip = props.controls.toolTooltip
+
+ return React.createElement(Slot, {
+ item = tool,
+ equipped = props.controls.equipped,
+ unlocked = props.controls.unlocked,
+ hint = props.controls.hint,
+ order = props.controls.order,
+ })
+ end,
+}
diff --git a/src/Components/Tooltip.luau b/src/Components/Tooltip.luau
new file mode 100644
index 00000000..9fa9d57f
--- /dev/null
+++ b/src/Components/Tooltip.luau
@@ -0,0 +1,19 @@
+const React = require("../../react")
+
+export type Props = {
+ text: string?,
+}
+
+const function Tooltip(props: Props)
+ -- Hide tooltip if there is no text
+ if not props.text or props.text == "" then
+ return
+ end
+
+ return React.createElement("TextLabel", {
+ Text = props.text,
+ [React.Tag] = "Tooltip",
+ })
+end
+
+return Tooltip
diff --git a/src/Components/Tooltip.story.luau b/src/Components/Tooltip.story.luau
new file mode 100644
index 00000000..caea0653
--- /dev/null
+++ b/src/Components/Tooltip.story.luau
@@ -0,0 +1,21 @@
+const React = require("../../react")
+
+const Tooltip = require("./Tooltip")
+
+const controls = {
+ text = "I'm a tooltip!",
+}
+
+type Props = {
+ controls: typeof(controls),
+}
+
+return {
+ summary = "Message displayed when hovering over a slot",
+ controls = controls,
+ story = function(props: Props)
+ return React.createElement(Tooltip, {
+ text = props.controls.text,
+ })
+ end,
+}
diff --git a/src/CoreGuiWarn.client.luau b/src/CoreGuiWarn.client.luau
new file mode 100644
index 00000000..b56acf88
--- /dev/null
+++ b/src/CoreGuiWarn.client.luau
@@ -0,0 +1,15 @@
+--!strict
+
+const StarterGui = game:GetService("StarterGui")
+
+const Satchel = require("./")
+
+task.spawn(function()
+ while task.wait(1) do
+ if StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Backpack) and Satchel.getEnabled() then
+ warn("[Satchel] CoreGui backpack detected. Disabling Satchel to prevent conflicts.")
+ Satchel.setEnabled(false)
+ break
+ end
+ end
+end)
diff --git a/src/Design.rbxmx b/src/Design.rbxmx
new file mode 100644
index 00000000..1bdd9ba6
--- /dev/null
+++ b/src/Design.rbxmx
@@ -0,0 +1,1001 @@
+
+ true
+ null
+ nil
+
+
+
+ 0
+ false
+ Design
+ -1
+
+
+
+
+
+ 0
+ false
+ SatchelStyleSheet
+ -1
+
+
+
+
+ 0
+
+
+ .Inventory
+
+ 0
+ false
+ .Inventory
+ -1
+
+
+
+
+ 0
+
+
+ ::UIPadding
+
+ 0
+ false
+ ::UIPadding
+ -1
+
+
+
+
+
+ 0
+
+
+ >ScrollingFrame
+
+ 0
+ false
+ >ScrollingFrame
+ -1
+
+
+
+
+ 0
+
+
+ ::UIListLayout
+
+ 0
+ false
+ ::UIListLayout
+ -1
+
+
+
+
+
+
+ 0
+ AQAAAAwAAABDb3JuZXJSYWRpdXMCFAAAACRTdXJmYWNlQ29ybmVyUmFkaXVz
+
+ ::UICorner
+
+ 0
+ false
+ ::UICorner
+ -1
+
+
+
+
+
+ 0
+ AQAAAAcAAABQYWRkaW5nCQAAAAAFAAAA
+
+ ::UIListLayout
+
+ 0
+ false
+ ::UIListLayout
+ -1
+
+
+
+
+
+
+ 0
+
+
+ .Hints
+
+ 0
+ false
+ .Hints
+ -1
+
+
+
+
+ 0
+
+
+ ::UIListLayout
+
+ 0
+ false
+ ::UIListLayout
+ -1
+
+
+
+
+
+
+ 0
+
+
+ .HotbarHints
+
+ 0
+ false
+ .HotbarHints
+ -1
+
+
+
+
+ 0
+
+
+ ::UIListLayout
+
+ 0
+ false
+ ::UIListLayout
+ -1
+
+
+
+
+
+
+ 0
+
+
+ .HotbarHint
+
+ 0
+ false
+ .HotbarHint
+ -1
+
+
+
+
+ 0
+
+
+ >ImageLabel
+
+ 0
+ false
+ >ImageLabel
+ -1
+
+
+
+
+ 0
+ AQAAAAsAAABBc3BlY3RSYXRpbwYAAAAAAAAAQA==
+
+ ::UIAspectRatioConstraint
+
+ 0
+ false
+ ::UIAspectRatioConstraint
+ -1
+
+
+
+
+
+
+ 0
+ AQAAAAcAAABWaXNpYmxlAwE=
+
+ @PreferredInputGamepad,.Visible
+
+ 0
+ false
+ @PreferredInputGamepad,.Visible
+ -1
+
+
+
+
+
+
+ 0
+
+
+ TextBox.Searchbar
+
+ 0
+ false
+ TextBox.Searchbar
+ -1
+
+
+
+
+ 0
+
+
+ ::UIStroke
+
+ 0
+ false
+ ::UIStroke
+ -1
+
+
+
+
+
+ 0
+
+
+ >ImageButton
+
+ 0
+ false
+ >ImageButton
+ -1
+
+
+
+
+ 0
+ AAAAAA==
+
+ ::UIAspectRatioConstraint
+
+ 0
+ false
+ ::UIAspectRatioConstraint
+ -1
+
+
+
+
+
+
+ 0
+
+
+ ::UIPadding
+
+ 0
+ false
+ ::UIPadding
+ -1
+
+
+
+
+
+ 0
+ AQAAAAwAAABDb3JuZXJSYWRpdXMCFgAAACRDb250YWluZXJDb3JuZXJSYWRpdXM=
+
+ ::UICorner
+
+ 0
+ false
+ ::UICorner
+ -1
+
+
+
+
+
+
+ 0
+
+
+ TextButton.Slot
+
+ 0
+ false
+ TextButton.Slot
+ -1
+
+
+
+
+ 0
+
+
+ ::UIPadding
+
+ 0
+ false
+ ::UIPadding
+ -1
+
+
+
+
+
+ 0
+
+
+ >ImageLabel
+
+ 0
+ false
+ >ImageLabel
+ -1
+
+
+
+
+ 0
+ AQAAAAwAAABDb3JuZXJSYWRpdXMCFQAAACRTbG90SWNvbkNvcm5lclJhZGl1cw==
+
+ ::UICorner
+
+ 0
+ false
+ ::UICorner
+ -1
+
+
+
+
+
+
+ 0
+
+
+ .Unlocked
+
+ 0
+ false
+ .Unlocked
+ -1
+
+
+
+
+ 0
+ AAAAAA==
+
+ :Press
+
+ 0
+ false
+ :Press
+ -1
+
+
+
+
+ 0
+
+
+ ::UIStroke
+
+ 0
+ false
+ ::UIStroke
+ -1
+
+
+
+
+
+
+
+ 0
+ AgAAAAQAAABTaXplCgAAAAAAAAAAAAAAAAAAAAAHAAAAVmlzaWJsZQMA
+
+ >TextLabel
+
+ 0
+ false
+ >TextLabel
+ -1
+
+
+
+
+ 0
+
+
+ .SlotNumber
+
+ 0
+ false
+ .SlotNumber
+ -1
+
+
+
+
+ 0
+ AQAAAAcAAABWaXNpYmxlAwE=
+
+ @PreferredInputKeyboardAndMouse
+
+ 0
+ false
+ @PreferredInputKeyboardAndMouse
+ -1
+
+
+
+
+
+
+ 0
+ AQAAAAcAAABWaXNpYmxlAwA=
+
+ .Tooltip
+
+ 0
+ false
+ .Tooltip
+ -1
+
+
+
+
+
+
+ 0
+ AQAAAAwAAABDb3JuZXJSYWRpdXMCEQAAACRTbG90Q29ybmVyUmFkaXVz
+
+ ::UICorner
+
+ 0
+ false
+ ::UICorner
+ -1
+
+
+
+
+
+ 0
+ AAAAAA==
+
+ :Press
+
+ 0
+ false
+ :Press
+ -1
+
+
+
+
+ 0
+ AQAAAAcAAABWaXNpYmxlAwE=
+
+ >.Tooltip
+
+ 0
+ false
+ >.Tooltip
+ -1
+
+
+
+
+
+
+ 0
+ AAAAAA==
+
+ .Equipped
+
+ 0
+ false
+ .Equipped
+ -1
+
+
+
+
+ 0
+
+
+ ::UIStroke
+
+ 0
+ false
+ ::UIStroke
+ -1
+
+
+
+
+
+
+ 0
+
+
+ ::UITextSizeConstraint
+
+ 0
+ false
+ ::UITextSizeConstraint
+ -1
+
+
+
+
+
+ 0
+ AAAAAA==
+
+ :Hover
+
+ 0
+ false
+ :Hover
+ -1
+
+
+
+
+ 0
+ AQAAAAcAAABWaXNpYmxlAwE=
+
+ >.Tooltip
+
+ 0
+ false
+ >.Tooltip
+ -1
+
+
+
+
+
+
+
+ 0
+
+
+ .Hotbar
+
+ 0
+ false
+ .Hotbar
+ -1
+
+
+
+
+ 0
+
+
+ ::UIListLayout
+
+ 0
+ false
+ ::UIListLayout
+ -1
+
+
+
+
+
+ 0
+ AgAAAAsAAABQYWRkaW5nTGVmdAkAAAAABQAAAAwAAABQYWRkaW5nUmlnaHQJAAAAAAUAAAA=
+
+ ::UIPadding
+
+ 0
+ false
+ ::UIPadding
+ -1
+
+
+
+
+
+
+ 0
+ RBX4FFE90AE43704D19A9B55E33BC2823A0
+
+ 0
+ false
+ Derive from DefaultTheme
+ -1
+
+
+
+
+
+ 0
+
+
+ TextLabel.Tooltip
+
+ 0
+ false
+ TextLabel.Tooltip
+ -1
+
+
+
+
+ 0
+ AQAAAAwAAABDb3JuZXJSYWRpdXMCFAAAACRUb29sVGlwQ29ybmVyUmFkaXVz
+
+ ::UICorner
+
+ 0
+ false
+ ::UICorner
+ -1
+
+
+
+
+
+ 0
+
+
+ ::UIPadding
+
+ 0
+ false
+ ::UIPadding
+ -1
+
+
+
+
+
+
+ 0
+
+
+ .InventoryHint
+
+ 0
+ false
+ .InventoryHint
+ -1
+
+
+
+
+ 0
+
+
+ >TextLabel
+
+ 0
+ false
+ >TextLabel
+ -1
+
+
+
+
+
+ 0
+
+
+ >ImageLabel
+
+ 0
+ false
+ >ImageLabel
+ -1
+
+
+
+
+ 0
+ AQAAAAsAAABBc3BlY3RSYXRpbwYAAAAAAAD4Pw==
+
+ ::UIAspectRatioConstraint
+
+ 0
+ false
+ ::UIAspectRatioConstraint
+ -1
+
+
+
+
+
+
+ 0
+ AQAAAAcAAABWaXNpYmxlAwE=
+
+ @PreferredInputGamepad,.Visible
+
+ 0
+ false
+ @PreferredInputGamepad,.Visible
+ -1
+
+
+
+
+
+
+ 0
+
+
+ .Backpack
+
+ 0
+ false
+ .Backpack
+ -1
+
+
+
+
+ 0
+ AQAAAAkAAABTb3J0T3JkZXIVCQAAAFNvcnRPcmRlcgIAAAA=
+
+ ::UIListLayout
+
+ 0
+ false
+ ::UIListLayout
+ -1
+
+
+
+
+
+
+ 0
+
+
+ .InventoryHints
+
+ 0
+ false
+ .InventoryHints
+ -1
+
+
+
+
+ 0
+ AQAAAAkAAABTb3J0T3JkZXIVCQAAAFNvcnRPcmRlcgIAAAA=
+
+ ::UIListLayout
+
+ 0
+ false
+ ::UIListLayout
+ -1
+
+
+
+
+
+
+
+
+ 0
+ false
+ BaseTokens
+ -1
+
+
+
+
+ 0
+ RBX45EBE9EB3D7C433CA888B0E29B9FC464
+
+ 0
+ false
+ Derive from ThemeTokens
+ -1
+
+
+
+
+
+
+
+ 0
+ false
+ ThemeTokens
+ -1
+
+
+
+
+
+ AQAAAA0AAABTdHlsZUNhdGVnb3J5AgYAAABUaGVtZXM=
+ 0
+ false
+ DefaultTheme
+ -1
+
+
+
+
+ 0
+ RBXA96A2BC50C474CD9929B9019E117AD67
+
+ 0
+ false
+ Derive from BaseTokens
+ -1
+
+
+
+
+
+
+
+ 0
+ false
+ LegacyTheme
+ -1
+
+
+
+
+ 0
+ RBXA96A2BC50C474CD9929B9019E117AD67
+
+ 0
+ false
+ Derive from BaseTokens
+ -1
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/TopbarIcon.client.luau b/src/TopbarIcon.client.luau
new file mode 100644
index 00000000..000ec9db
--- /dev/null
+++ b/src/TopbarIcon.client.luau
@@ -0,0 +1,47 @@
+--!strict
+
+const Icon = require("../topbarplus")
+const Satchel = require("./")
+
+const icon = Icon.new()
+icon:setName("SatchelInventory")
+icon:modifyTheme({
+ { "IconLabelContainer", "TargetWidth", 0 }, -- Force minimum width
+ { "IconLabel", "AutoLocalize", false }, -- Don't translate font icon
+})
+icon:setLabel("backpack")
+icon:setOrder(-1)
+icon:setTextSize(24)
+icon:setTextFont(
+ "rbxasset://LuaPackages/Packages/_Index/BuilderIcons/BuilderIcons/BuilderIcons.json",
+ Enum.FontWeight.Bold,
+ Enum.FontStyle.Normal,
+ "Selected"
+)
+icon:setTextFont(
+ "rbxasset://LuaPackages/Packages/_Index/BuilderIcons/BuilderIcons/BuilderIcons.json",
+ Enum.FontWeight.Regular,
+ Enum.FontStyle.Normal,
+ "Deselected"
+)
+icon:bindToggleKey(Enum.KeyCode.Backquote)
+icon:autoDeselect(false)
+icon:setCaption("Inventory")
+
+icon.toggled:Connect(function(isSelected, fromSource)
+ if fromSource == "User" then
+ if isSelected then
+ Satchel.openInventory()
+ else
+ Satchel.closeInventory()
+ end
+ end
+end)
+
+Satchel.inventoryOpened:Connect(function()
+ icon:select()
+end)
+
+Satchel.inventoryClosed:Connect(function()
+ icon:deselect()
+end)
diff --git a/src/init.luau b/src/init.luau
index 6ed1e1b3..8c623fdf 100644
--- a/src/init.luau
+++ b/src/init.luau
@@ -1,2167 +1,25 @@
---!nolint DeprecatedApi
-
---[[
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-]]
-
-local ContextActionService = game:GetService("ContextActionService")
-local GuiService = game:GetService("GuiService")
-local Players = game:GetService("Players")
-local RunService = game:GetService("RunService")
-local StarterGui = game:GetService("StarterGui")
-local TextChatService = game:GetService("TextChatService")
-local UserInputService = game:GetService("UserInputService")
-local VRService = game:GetService("VRService")
-local PlayerGui: Instance = Players.LocalPlayer:WaitForChild("PlayerGui")
-
-local BackpackScript = {}
-
-BackpackScript.OpenClose = nil :: any -- Function to toggle open/close
-BackpackScript.IsOpen = false :: boolean
-BackpackScript.StateChanged = Instance.new("BindableEvent") :: BindableEvent -- Fires after any open/close, passes IsNowOpen
-
-BackpackScript.ModuleName = "Backpack" :: string
-BackpackScript.KeepVRTopbarOpen = true :: boolean
-BackpackScript.VRIsExclusive = true :: boolean
-BackpackScript.VRClosesNonExclusive = true :: boolean
-
-BackpackScript.BackpackEmpty = Instance.new("BindableEvent") :: BindableEvent -- Fires when the backpack is empty (no tools
-BackpackScript.BackpackEmpty.Name = "BackpackEmpty"
-
-BackpackScript.BackpackItemAdded = Instance.new("BindableEvent") :: BindableEvent -- Fires when an item is added to the backpack
-BackpackScript.BackpackItemAdded.Name = "BackpackAdded"
-
-BackpackScript.BackpackItemRemoved = Instance.new("BindableEvent") :: BindableEvent -- Fires when an item is removed from the backpack
-BackpackScript.BackpackItemRemoved.Name = "BackpackRemoved"
-
-local targetScript: ModuleScript = script
-
-require(script.Attribution)
-
--- Constants --
-local PREFERRED_TRANSPARENCY: number = GuiService.PreferredTransparency or 1
-
--- Legacy behavior for backpack
-local LEGACY_EDGE_ENABLED: boolean = not targetScript:GetAttribute("OutlineEquipBorder") or false -- Instead of the edge selection being inset, it will be on the outlined. LEGACY_PADDING must be enabled for this to work or this will do nothing
-local LEGACY_PADDING_ENABLED: boolean = targetScript:GetAttribute("InsetIconPadding") -- Instead of the icon taking up the full slot, it will be padded on each side.
-
--- Background
-local BACKGROUND_TRANSPARENCY_DEFAULT: number = targetScript:GetAttribute("BackgroundTransparency") or 0.3
-local BACKGROUND_TRANSPARENCY: number = BACKGROUND_TRANSPARENCY_DEFAULT * PREFERRED_TRANSPARENCY
-local BACKGROUND_CORNER_RADIUS: UDim = UDim.new(0, 8)
-local BACKGROUND_COLOR: Color3 = targetScript:GetAttribute("BackgroundColor3")
- or Color3.new(25 / 255, 27 / 255, 29 / 255)
-
--- Slots
-local SLOT_EQUIP_COLOR: Color3 = targetScript:GetAttribute("EquipBorderColor3") or Color3.new(0 / 255, 162 / 255, 1)
-local SLOT_LOCKED_TRANSPARENCY_DEFAULT: number = targetScript:GetAttribute("BackgroundTransparency") or 0.3 -- Locked means undraggable
-local SLOT_LOCKED_TRANSPARENCY: number = SLOT_LOCKED_TRANSPARENCY_DEFAULT * PREFERRED_TRANSPARENCY
-local SLOT_EQUIP_THICKNESS: number = targetScript:GetAttribute("EquipBorderSizePixel") or 5 -- Relative
-local SLOT_CORNER_RADIUS: UDim = targetScript:GetAttribute("CornerRadius") or UDim.new(0, 8)
-local SLOT_BORDER_COLOR: Color3 = Color3.new(1, 1, 1) -- Appears when dragging
-
--- Tooltips
-local TOOLTIP_CORNER_RADIUS: UDim = SLOT_CORNER_RADIUS - UDim.new(0, 5) or UDim.new(0, 3)
-local TOOLTIP_BACKGROUND_COLOR: Color3 = targetScript:GetAttribute("BackgroundColor3")
- or Color3.new(25 / 255, 27 / 255, 29 / 255)
-local TOOLTIP_PADDING: number = 4
-local TOOLTIP_HEIGHT: number = 16
-local TOOLTIP_OFFSET: number = -5 -- From to
-
--- Topbar icons
-local ARROW_IMAGE_OPEN: string = "rbxasset://textures/ui/TopBar/inventoryOn.png"
-local ARROW_IMAGE_CLOSE: string = "rbxasset://textures/ui/TopBar/inventoryOff.png"
--- local ARROW_HOTKEY: { Enum.KeyCode } = { Enum.KeyCode.Backquote, Enum.KeyCode.DPadUp } --TODO: Hookup '~' too?
-
--- Hotbar slots
-local HOTBAR_SLOTS_FULL: number = 10 -- 10 is the max
-local HOTBAR_SLOTS_VR: number = 6
-local HOTBAR_SLOTS_MINI: number = 6 -- Mobile gets 6 slots instead of default 3 it had before
-local HOTBAR_SLOTS_WIDTH_CUTOFF: number = 1024 -- Anything smaller is MINI
-
-local INVENTORY_ROWS_FULL: number = 4
-local INVENTORY_ROWS_VR: number = 3
-local INVENTORY_ROWS_MINI: number = 2
-local INVENTORY_HEADER_SIZE: number = 40
-local INVENTORY_ARROWS_BUFFER_VR: number = 40
-
--- Text
-local TEXT_COLOR: Color3 = targetScript:GetAttribute("TextColor3") or Color3.new(1, 1, 1)
-local TEXT_STROKE_TRANSPARENCY: number = targetScript:GetAttribute("TextStrokeTransparency") or 0.5
-local TEXT_STROKE_COLOR: Color3 = targetScript:GetAttribute("TextStrokeColor3") or Color3.new(0, 0, 0)
-
--- Search
-local SEARCH_BACKGROUND_COLOR: Color3 = Color3.new(25 / 255, 27 / 255, 29 / 255)
-local SEARCH_BACKGROUND_TRANSPARENCY_DEFAULT: number = 0.2
-local SEARCH_BACKGROUND_TRANSPARENCY: number = SEARCH_BACKGROUND_TRANSPARENCY_DEFAULT * PREFERRED_TRANSPARENCY
-local SEARCH_BORDER_COLOR: Color3 = Color3.new(1, 1, 1)
-local SEARCH_BORDER_TRANSPARENCY: number = 0.8
-local SEARCH_BORDER_THICKNESS: number = 1
-local SEARCH_TEXT_PLACEHOLDER: string = "Search"
-local SEARCH_TEXT_OFFSET: number = 8
-local SEARCH_TEXT: string = ""
-local SEARCH_CORNER_RADIUS: UDim = UDim.new(0, 3)
-local SEARCH_IMAGE_X: string = "rbxasset://textures/ui/InspectMenu/x.png"
-local SEARCH_BUFFER_PIXELS: number = 5
-local SEARCH_WIDTH_PIXELS: number = 200
-
--- Misc
-local FONT_FAMILY: Font = targetScript:GetAttribute("FontFace")
- or Font.new("rbxasset://fonts/families/BuilderSans.json")
-local FONT_SIZE: number = targetScript:GetAttribute("TextSize") or 16
-local DROP_HOTKEY_VALUE: number = Enum.KeyCode.Backspace.Value
-local ZERO_KEY_VALUE: number = Enum.KeyCode.Zero.Value
-local DOUBLE_CLICK_TIME: number = 0.5
-local ICON_BUFFER_PIXELS: number = 5
-local ICON_SIZE_PIXELS: number = 60
-
-local MOUSE_INPUT_TYPES: { [Enum.UserInputType]: boolean } =
- { -- These are the input types that will be used for mouse -- [[ADDED]], Optional
- [Enum.UserInputType.MouseButton1] = true,
- [Enum.UserInputType.MouseButton2] = true,
- [Enum.UserInputType.MouseButton3] = true,
- [Enum.UserInputType.MouseMovement] = true,
- [Enum.UserInputType.MouseWheel] = true,
- }
-
-local GAMEPAD_INPUT_TYPES: { [Enum.UserInputType]: boolean } =
- { -- These are the input types that will be used for gamepad
- [Enum.UserInputType.Gamepad1] = true,
- [Enum.UserInputType.Gamepad2] = true,
- [Enum.UserInputType.Gamepad3] = true,
- [Enum.UserInputType.Gamepad4] = true,
- [Enum.UserInputType.Gamepad5] = true,
- [Enum.UserInputType.Gamepad6] = true,
- [Enum.UserInputType.Gamepad7] = true,
- [Enum.UserInputType.Gamepad8] = true,
- }
-
--- Topbar logic
-local BackpackEnabled: boolean = true
-
-local Icon: any = require(script.Parent.topbarplus)
-
-local inventoryIcon: any = Icon.new()
- :setName("Inventory")
- :setImage(ARROW_IMAGE_OPEN, "Selected")
- :setImage(ARROW_IMAGE_CLOSE, "Deselected")
- :setImageScale(1)
- :setCaption("Inventory")
- :bindToggleKey(Enum.KeyCode.Backquote)
- :autoDeselect(false)
- :setOrder(-1)
-
-inventoryIcon.toggled:Connect(function(): ()
- if not GuiService.MenuIsOpen then
- BackpackScript.OpenClose()
- end
-end)
-
-local BackpackGui: ScreenGui = Instance.new("ScreenGui")
-BackpackGui.DisplayOrder = 120
-BackpackGui.IgnoreGuiInset = true
-BackpackGui.ResetOnSpawn = false
-BackpackGui.Name = "BackpackGui"
-BackpackGui.Parent = PlayerGui
-
-local IsTenFootInterface: boolean = GuiService:IsTenFootInterface()
-if IsTenFootInterface then
- ICON_SIZE_PIXELS = 100
- FONT_SIZE = 24
-end
-
-local GamepadActionsBound: boolean = false
-
-local IS_PHONE: boolean = UserInputService.TouchEnabled
- and workspace.CurrentCamera.ViewportSize.X < HOTBAR_SLOTS_WIDTH_CUTOFF
-
-local Player: Player = Players.LocalPlayer
-
-local MainFrame: Frame = nil
-local HotbarFrame: Frame = nil
-local InventoryFrame: Frame = nil
-local VRInventorySelector: any = nil
-local ScrollingFrame: ScrollingFrame = nil
-local UIGridFrame: Frame = nil
-local UIGridLayout: UIGridLayout = nil
-local ScrollUpInventoryButton: any = nil
-local ScrollDownInventoryButton: any = nil
-local changeToolFunc: any = nil
-
-local Character: Model = Player.Character or Player.CharacterAdded:Wait()
-local Humanoid: any = Character:WaitForChild("Humanoid")
-local Backpack: Instance = Player:WaitForChild("Backpack")
-
-local Slots = {} -- List of all Slots by index
-local LowestEmptySlot: any = nil
-local SlotsByTool = {} -- Map of Tools to their assigned Slots
-local HotkeyFns = {} -- Map of KeyCode values to their assigned behaviors
-local Dragging: { boolean } = {} -- Only used to check if anything is being dragged, to disable other input
-local FullHotbarSlots: number = 0 -- Now being used to also determine whether or not LB and RB on the gamepad are enabled.
-local ActiveHopper = nil -- NOTE: HopperBin
-local StarterToolFound: boolean = false -- Special handling is required for the gear currently equipped on the site
-local WholeThingEnabled: boolean = false
-local TextBoxFocused: boolean = false -- ANY TextBox, not just the search box
-local ViewingSearchResults: boolean = false -- If the results of a search are currently being viewed
--- local HotkeyStrings = {} -- Used for eating/releasing hotkeys
-local CharConns: { RBXScriptConnection } = {} -- Holds character Connections to be cleared later
-local GamepadEnabled: boolean = false -- determines if our gui needs to be gamepad friendly
-
-local IsVR: boolean = VRService.VREnabled -- Are we currently using a VR device?
-local NumberOfHotbarSlots: number = IsVR and HOTBAR_SLOTS_VR or (IS_PHONE and HOTBAR_SLOTS_MINI or HOTBAR_SLOTS_FULL) -- Number of slots shown at the bottom
-local NumberOfInventoryRows: number = IsVR and INVENTORY_ROWS_VR
- or (IS_PHONE and INVENTORY_ROWS_MINI or INVENTORY_ROWS_FULL) -- How many rows in the popped-up inventory
-local BackpackPanel = nil
-local lastEquippedSlot: any = nil
-
-local function EvaluateBackpackPanelVisibility(enabled: boolean): boolean
- return enabled and inventoryIcon.enabled and BackpackEnabled and VRService.VREnabled
-end
-
-local function ShowVRBackpackPopup(): ()
- if BackpackPanel and EvaluateBackpackPanelVisibility(true) then
- BackpackPanel:ForceShowForSeconds(2)
- end
-end
-
-local function FindLowestEmpty(): number?
- for i: number = 1, NumberOfHotbarSlots do
- local slot: any = Slots[i]
- if not slot.Tool then
- return slot
- end
- end
- return nil
-end
-
-local function isInventoryEmpty(): boolean
- for i: number = NumberOfHotbarSlots + 1, #Slots do
- local slot: any = Slots[i]
- if slot and slot.Tool then
- return false
- end
- end
- return true
-end
-
-BackpackScript.IsInventoryEmpty = isInventoryEmpty
-
-local function UseGazeSelection(): boolean
- return false -- disabled in new VR system
-end
-
-local function AdjustHotbarFrames(): ()
- local inventoryOpen: boolean = InventoryFrame.Visible -- (Show all)
- local visualTotal: number = inventoryOpen and NumberOfHotbarSlots or FullHotbarSlots
- local visualIndex: number = 0
-
- for i: number = 1, NumberOfHotbarSlots do
- local slot: any = Slots[i]
- if slot.Tool or inventoryOpen then
- visualIndex = visualIndex + 1
- slot:Readjust(visualIndex, visualTotal)
- slot.Frame.Visible = true
- else
- slot.Frame.Visible = false
- end
- end
-end
-
-local function UpdateScrollingFrameCanvasSize(): ()
- local countX: number = math.floor(ScrollingFrame.AbsoluteSize.X / (ICON_SIZE_PIXELS + ICON_BUFFER_PIXELS))
- local maxRow: number = math.ceil((#UIGridFrame:GetChildren() - 1) / countX)
- local canvasSizeY: number = maxRow * (ICON_SIZE_PIXELS + ICON_BUFFER_PIXELS) + ICON_BUFFER_PIXELS
- ScrollingFrame.CanvasSize = UDim2.fromOffset(0, canvasSizeY)
-end
-
-local function AdjustInventoryFrames(): ()
- for i: number = NumberOfHotbarSlots + 1, #Slots do
- local slot: any = Slots[i]
- slot.Frame.LayoutOrder = slot.Index
- slot.Frame.Visible = (slot.Tool ~= nil)
- end
- UpdateScrollingFrameCanvasSize()
-end
-
-local function UpdateBackpackLayout(): ()
- HotbarFrame.Size = UDim2.new(
- 0,
- ICON_BUFFER_PIXELS + (NumberOfHotbarSlots * (ICON_SIZE_PIXELS + ICON_BUFFER_PIXELS)),
- 0,
- ICON_BUFFER_PIXELS + ICON_SIZE_PIXELS + ICON_BUFFER_PIXELS
- )
- HotbarFrame.Position = UDim2.new(0.5, -HotbarFrame.Size.X.Offset / 2, 1, -HotbarFrame.Size.Y.Offset)
- InventoryFrame.Size = UDim2.new(
- 0,
- HotbarFrame.Size.X.Offset,
- 0,
- (HotbarFrame.Size.Y.Offset * NumberOfInventoryRows)
- + INVENTORY_HEADER_SIZE
- + (IsVR and 2 * INVENTORY_ARROWS_BUFFER_VR or 0)
- )
- InventoryFrame.Position = UDim2.new(
- 0.5,
- -InventoryFrame.Size.X.Offset / 2,
- 1,
- HotbarFrame.Position.Y.Offset - InventoryFrame.Size.Y.Offset
- )
-
- ScrollingFrame.Size = UDim2.new(
- 1,
- ScrollingFrame.ScrollBarThickness + 1,
- 1,
- -INVENTORY_HEADER_SIZE - (IsVR and 2 * INVENTORY_ARROWS_BUFFER_VR or 0)
- )
- ScrollingFrame.Position = UDim2.fromOffset(0, INVENTORY_HEADER_SIZE + (IsVR and INVENTORY_ARROWS_BUFFER_VR or 0))
- AdjustHotbarFrames()
- AdjustInventoryFrames()
-end
-
-local function Clamp(low: number, high: number, num: number): number
- return math.min(high, math.max(low, num))
-end
-
-local function CheckBounds(guiObject: GuiObject, x: number, y: number): boolean
- local pos: Vector2 = guiObject.AbsolutePosition
- local size: Vector2 = guiObject.AbsoluteSize
- return (x > pos.X and x <= pos.X + size.X and y > pos.Y and y <= pos.Y + size.Y)
-end
-
-local function GetOffset(guiObject: GuiObject, point: Vector2): number
- local centerPoint: Vector2 = guiObject.AbsolutePosition + (guiObject.AbsoluteSize / 2)
- return (centerPoint - point).Magnitude
-end
-
-local function DisableActiveHopper(): () --NOTE: HopperBin
- ActiveHopper:ToggleSelect()
- SlotsByTool[ActiveHopper]:UpdateEquipView()
- ActiveHopper = nil :: any
-end
-
-local function UnequipAllTools(): () --NOTE: HopperBin
- if Humanoid then
- Humanoid:UnequipTools()
- if ActiveHopper then
- DisableActiveHopper()
- end
- end
-end
-
-local function EquipNewTool(tool: Tool): () --NOTE: HopperBin
- UnequipAllTools()
- Humanoid:EquipTool(tool) --NOTE: This would also unequip current Tool
- --tool.Parent = Character --TODO: Switch back to above line after EquipTool is fixed!
-end
-
-local function IsEquipped(tool: Tool): boolean
- return tool and tool.Parent == Character --NOTE: HopperBin
-end
-
--- Create a slot
-local function MakeSlot(parent: Instance, initIndex: number?): GuiObject
- local index: number = initIndex or (#Slots + 1)
-
- -- Slot Definition --
-
- local slot: any = {}
- slot.Tool = nil :: any
- slot.Index = index :: number
- slot.Frame = nil :: any
-
- local SlotFrame: any = nil
- local FakeSlotFrame: Frame = nil
- local ToolIcon: ImageLabel = nil
- local ToolName: TextLabel = nil
- local ToolChangeConn: any = nil
- local HighlightFrame: any = nil -- UIStroke
- local SelectionObj: ImageLabel = nil
-
- --NOTE: The following are only defined for Hotbar Slots
- local ToolTip: TextLabel = nil
- local SlotNumber: TextLabel = nil
-
- -- Slot Functions --
-
- -- Update slot transparency
- local function UpdateSlotFading(): ()
- SlotFrame.SelectionImageObject = nil
- SlotFrame.BackgroundTransparency = SlotFrame.Draggable and 0 or SLOT_LOCKED_TRANSPARENCY
- end
-
- -- Adjust the slots to the centered
- function slot:Readjust(visualIndex: number, visualTotal: number): ...any --NOTE: Only used for Hotbar slots
- local centered: number = HotbarFrame.Size.X.Offset / 2
- local sizePlus: number = ICON_BUFFER_PIXELS + ICON_SIZE_PIXELS
- local midpointish: number = (visualTotal / 2) + 0.5
- local factor: number = visualIndex - midpointish
- SlotFrame.Position =
- UDim2.fromOffset(centered - (ICON_SIZE_PIXELS / 2) + (sizePlus * factor), ICON_BUFFER_PIXELS)
- end
-
- -- Fill the slot with a tool
- function slot:Fill(tool: Tool): ...any
- -- Clear slot if it has no tool else assign the tool
- if not tool then
- return self:Clear()
- end
-
- self.Tool = tool :: Tool
-
- -- Update the slot with tool data
- local function assignToolData(): ()
- local icon: string = tool.TextureId
- ToolIcon.Image = icon
-
- if icon ~= "" then
- -- Enable the tool name on the slot if there is no icon
- ToolName.Visible = false
- else
- ToolName.Visible = true
- end
-
- ToolName.Text = tool.Name
-
- -- If there is a tooltip, then show it
- if ToolTip and tool:IsA("Tool") then --NOTE: HopperBin
- ToolTip.Text = tool.ToolTip
- ToolTip.Size = UDim2.fromOffset(0, TOOLTIP_HEIGHT)
- ToolTip.Position = UDim2.new(0.5, 0, 0, TOOLTIP_OFFSET)
- end
- end
- assignToolData()
-
- -- Disconnect tool event if it exists
- if ToolChangeConn then
- ToolChangeConn:Disconnect()
- ToolChangeConn = nil
- end
-
- -- Update the slot with new tool data if the tool's properties changes
- ToolChangeConn = tool.Changed:Connect(function(property: string): ()
- if property == "TextureId" or property == "Name" or property == "ToolTip" then
- assignToolData()
- end
- end)
-
- local hotbarSlot: boolean = (self.Index <= NumberOfHotbarSlots)
- local inventoryOpen: boolean = InventoryFrame.Visible
-
- if (not hotbarSlot or inventoryOpen) and not UserInputService.VREnabled then
- SlotFrame.Draggable = true
- end
-
- self:UpdateEquipView()
-
- if hotbarSlot then
- FullHotbarSlots = FullHotbarSlots + 1
- -- If using a controller, determine whether or not we can enable BindCoreAction("BackpackHotbarEquip", etc)
- if WholeThingEnabled and FullHotbarSlots >= 1 and not GamepadActionsBound then
- -- Player added first item to a hotbar slot, enable BindCoreAction
- GamepadActionsBound = true
- ContextActionService:BindAction(
- "BackpackHotbarEquip",
- changeToolFunc,
- false,
- Enum.KeyCode.ButtonL1,
- Enum.KeyCode.ButtonR1
- )
- end
- end
-
- SlotsByTool[tool] = self
- LowestEmptySlot = FindLowestEmpty()
- end
-
- -- Empty the slot of any tool data
- function slot:Clear(): ...any
- if not self.Tool then
- return
- end
-
- -- Disconnect tool event if it exists
- if ToolChangeConn then
- ToolChangeConn:Disconnect()
- ToolChangeConn = nil
- end
-
- ToolIcon.Image = ""
- ToolName.Text = ""
- if ToolTip then
- ToolTip.Text = ""
- ToolTip.Visible = false
- end
- SlotFrame.Draggable = false
-
- self:UpdateEquipView(true) -- Show as unequipped
-
- if self.Index <= NumberOfHotbarSlots then
- FullHotbarSlots = FullHotbarSlots - 1
- if FullHotbarSlots < 1 then
- -- Player removed last item from hotbar; UnbindCoreAction("BackpackHotbarEquip"), allowing the developer to use LB and RB.
- GamepadActionsBound = false
- ContextActionService:UnbindAction("BackpackHotbarEquip")
- end
- end
-
- SlotsByTool[self.Tool] = nil
- self.Tool = nil
- LowestEmptySlot = FindLowestEmpty()
- end
-
- function slot:UpdateEquipView(unequippedOverride: boolean?): ...any
- local override = unequippedOverride or false
- if not override and IsEquipped(self.Tool) then -- Equipped
- lastEquippedSlot = slot
- if not HighlightFrame then
- HighlightFrame = Instance.new("UIStroke")
- HighlightFrame.Name = "Border"
- HighlightFrame.Thickness = SLOT_EQUIP_THICKNESS
- HighlightFrame.Color = SLOT_EQUIP_COLOR
- HighlightFrame.ApplyStrokeMode = Enum.ApplyStrokeMode.Border
- end
- if LEGACY_EDGE_ENABLED == true then
- HighlightFrame.Parent = ToolIcon
- else
- HighlightFrame.Parent = SlotFrame
- end
- else -- In the Backpack
- if HighlightFrame then
- HighlightFrame.Parent = nil
- end
- end
- UpdateSlotFading()
- end
-
- function slot:IsEquipped(): boolean
- return IsEquipped(self.Tool)
- end
-
- function slot:Delete(): ...any
- SlotFrame:Destroy() --NOTE: Also clears connections
- table.remove(Slots, self.Index)
- local newSize: number = #Slots
-
- -- Now adjust the rest (both visually and representationally)
- for slotIndex: number = self.Index :: number, newSize :: number do
- Slots[slotIndex]:SlideBack()
- end
-
- UpdateScrollingFrameCanvasSize()
- end
-
- function slot:Swap(targetSlot: any): ...any --NOTE: This slot (self) must not be empty!
- local myTool: any, otherTool: any = self.Tool, targetSlot.Tool
- self:Clear()
- if otherTool then -- (Target slot might be empty)
- targetSlot:Clear()
- self:Fill(otherTool)
- end
- if myTool then
- targetSlot:Fill(myTool)
- else
- targetSlot:Clear()
- end
- end
-
- function slot:SlideBack(): ...any -- For inventory slot shifting
- self.Index = self.Index - 1
- SlotFrame.Name = self.Index
- SlotFrame.LayoutOrder = self.Index
- end
-
- function slot:TurnNumber(on: boolean): ...any
- if SlotNumber then
- SlotNumber.Visible = on
- end
- end
-
- function slot:SetClickability(on: boolean): ...any -- (Happens on open/close arrow)
- if self.Tool then
- if UserInputService.VREnabled then
- SlotFrame.Draggable = false
- else
- SlotFrame.Draggable = not on
- end
- UpdateSlotFading()
- end
- end
-
- function slot:CheckTerms(terms: any): number
- local hits: number = 0
- local function checkEm(str: string, term: any): ()
- local _, n: number = str:lower():gsub(term, "")
- hits = hits + n
- end
- local tool: Tool = self.Tool
- if tool then
- for term: any in pairs(terms) do
- checkEm(ToolName.Text, term)
- if tool:IsA("Tool") then --NOTE: HopperBin
- local toolTipText: string = ToolTip and ToolTip.Text or ""
- checkEm(toolTipText, term)
- end
- end
- end
- return hits
- end
-
- -- Slot select logic, activated by clicking or pressing hotkey
- function slot:Select(): ...any
- local tool: Tool = slot.Tool
- if tool then
- if IsEquipped(tool) then --NOTE: HopperBin
- UnequipAllTools()
- elseif tool.Parent == Backpack then
- EquipNewTool(tool)
- end
- end
- end
-
- -- Slot Init Logic --
-
- SlotFrame = Instance.new("TextButton")
- SlotFrame.Name = tostring(index)
- SlotFrame.BackgroundColor3 = BACKGROUND_COLOR
- SlotFrame.BorderColor3 = SLOT_BORDER_COLOR
- SlotFrame.Text = ""
- SlotFrame.BorderSizePixel = 0
- SlotFrame.Size = UDim2.fromOffset(ICON_SIZE_PIXELS, ICON_SIZE_PIXELS)
- SlotFrame.Active = true
- SlotFrame.Draggable = false
- SlotFrame.BackgroundTransparency = SLOT_LOCKED_TRANSPARENCY
- SlotFrame.MouseButton1Click:Connect(function(): ()
- changeSlot(slot)
- end)
- local searchFrameCorner: UICorner = Instance.new("UICorner")
- searchFrameCorner.Name = "Corner"
- searchFrameCorner.CornerRadius = SLOT_CORNER_RADIUS
- searchFrameCorner.Parent = SlotFrame
- slot.Frame = SlotFrame
-
- do
- local selectionObjectClipper: Frame = Instance.new("Frame")
- selectionObjectClipper.Name = "SelectionObjectClipper"
- selectionObjectClipper.BackgroundTransparency = 1
- selectionObjectClipper.Visible = false
- selectionObjectClipper.Parent = SlotFrame
-
- SelectionObj = Instance.new("ImageLabel")
- SelectionObj.Name = "Selector"
- SelectionObj.BackgroundTransparency = 1
- SelectionObj.Size = UDim2.fromScale(1, 1)
- SelectionObj.Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png"
- SelectionObj.ScaleType = Enum.ScaleType.Slice
- SelectionObj.SliceCenter = Rect.new(12, 12, 52, 52)
- SelectionObj.Parent = selectionObjectClipper
- end
-
- ToolIcon = Instance.new("ImageLabel")
- ToolIcon.BackgroundTransparency = 1
- ToolIcon.Name = "Icon"
- ToolIcon.Size = UDim2.fromScale(1, 1)
- ToolIcon.Position = UDim2.fromScale(0.5, 0.5)
- ToolIcon.AnchorPoint = Vector2.new(0.5, 0.5)
- if LEGACY_PADDING_ENABLED == true then
- ToolIcon.Size = UDim2.new(1, -SLOT_EQUIP_THICKNESS * 2, 1, -SLOT_EQUIP_THICKNESS * 2)
- else
- ToolIcon.Size = UDim2.fromScale(1, 1)
- end
- ToolIcon.Parent = SlotFrame
-
- local ToolIconCorner: UICorner = Instance.new("UICorner")
- ToolIconCorner.Name = "Corner"
- if LEGACY_PADDING_ENABLED == true then
- ToolIconCorner.CornerRadius = SLOT_CORNER_RADIUS - UDim.new(0, SLOT_EQUIP_THICKNESS)
- else
- ToolIconCorner.CornerRadius = SLOT_CORNER_RADIUS
- end
- ToolIconCorner.Parent = ToolIcon
-
- ToolName = Instance.new("TextLabel")
- ToolName.BackgroundTransparency = 1
- ToolName.Name = "ToolName"
- ToolName.Text = ""
- ToolName.TextColor3 = TEXT_COLOR
- ToolName.TextStrokeTransparency = TEXT_STROKE_TRANSPARENCY
- ToolName.TextStrokeColor3 = TEXT_STROKE_COLOR
- ToolName.FontFace = Font.new(FONT_FAMILY.Family, Enum.FontWeight.Medium, Enum.FontStyle.Normal)
- ToolName.TextSize = FONT_SIZE
- ToolName.Size = UDim2.new(1, -SLOT_EQUIP_THICKNESS * 2, 1, -SLOT_EQUIP_THICKNESS * 2)
- ToolName.Position = UDim2.fromScale(0.5, 0.5)
- ToolName.AnchorPoint = Vector2.new(0.5, 0.5)
- ToolName.TextWrapped = true
- ToolName.TextTruncate = Enum.TextTruncate.AtEnd
- ToolName.Parent = SlotFrame
-
- slot.Frame.LayoutOrder = slot.Index
-
- if index <= NumberOfHotbarSlots then -- Hotbar-Specific Slot Stuff
- -- ToolTip stuff
- ToolTip = Instance.new("TextLabel")
- ToolTip.Name = "ToolTip"
- ToolTip.Text = ""
- ToolTip.Size = UDim2.fromScale(1, 1)
- ToolTip.TextColor3 = TEXT_COLOR
- ToolTip.TextStrokeTransparency = TEXT_STROKE_TRANSPARENCY
- ToolTip.TextStrokeColor3 = TEXT_STROKE_COLOR
- ToolTip.FontFace = Font.new(FONT_FAMILY.Family, Enum.FontWeight.Medium, Enum.FontStyle.Normal)
- ToolTip.TextSize = FONT_SIZE
- ToolTip.ZIndex = 2
- ToolTip.TextWrapped = false
- ToolTip.TextYAlignment = Enum.TextYAlignment.Center
- ToolTip.BackgroundColor3 = TOOLTIP_BACKGROUND_COLOR
- ToolTip.BackgroundTransparency = SLOT_LOCKED_TRANSPARENCY
- ToolTip.AnchorPoint = Vector2.new(0.5, 1)
- ToolTip.BorderSizePixel = 0
- ToolTip.Visible = false
- ToolTip.AutomaticSize = Enum.AutomaticSize.X
- ToolTip.Parent = SlotFrame
-
- local ToolTipCorner: UICorner = Instance.new("UICorner")
- ToolTipCorner.Name = "Corner"
- ToolTipCorner.CornerRadius = TOOLTIP_CORNER_RADIUS
- ToolTipCorner.Parent = ToolTip
-
- local ToolTipPadding: UIPadding = Instance.new("UIPadding")
- ToolTipPadding.PaddingLeft = UDim.new(0, TOOLTIP_PADDING)
- ToolTipPadding.PaddingRight = UDim.new(0, TOOLTIP_PADDING)
- ToolTipPadding.PaddingTop = UDim.new(0, TOOLTIP_PADDING)
- ToolTipPadding.PaddingBottom = UDim.new(0, TOOLTIP_PADDING)
- ToolTipPadding.Parent = ToolTip
- SlotFrame.MouseEnter:Connect(function(): ()
- if ToolTip.Text ~= "" then
- ToolTip.Visible = true
- end
- end)
- SlotFrame.MouseLeave:Connect(function(): ()
- ToolTip.Visible = false
- end)
-
- function slot:MoveToInventory(): ...any
- if slot.Index <= NumberOfHotbarSlots then -- From a Hotbar slot
- local tool: any = slot.Tool
- self:Clear() --NOTE: Order matters here
- local newSlot: any = MakeSlot(UIGridFrame)
- newSlot:Fill(tool)
- if IsEquipped(tool) then -- Also unequip it --NOTE: HopperBin
- UnequipAllTools()
- end
- -- Also hide the inventory slot if we're showing results right now
- if ViewingSearchResults then
- newSlot.Frame.Visible = false
- newSlot.Parent = InventoryFrame
- end
- end
- end
-
- -- Show label and assign hotkeys for 1-9 and 0 (zero is always last slot when > 10 total)
- if index < 10 or index == NumberOfHotbarSlots then -- NOTE: Hardcoded on purpose!
- local slotNum: number = (index < 10) and index or 0
- SlotNumber = Instance.new("TextLabel")
- SlotNumber.BackgroundTransparency = 1
- SlotNumber.Name = "Number"
- SlotNumber.TextColor3 = TEXT_COLOR
- SlotNumber.TextStrokeTransparency = TEXT_STROKE_TRANSPARENCY
- SlotNumber.TextStrokeColor3 = TEXT_STROKE_COLOR
- SlotNumber.TextSize = FONT_SIZE
- SlotNumber.Text = tostring(slotNum)
- SlotNumber.FontFace = Font.new(FONT_FAMILY.Family, Enum.FontWeight.Heavy, Enum.FontStyle.Normal)
- SlotNumber.Size = UDim2.fromScale(0.4, 0.4)
- SlotNumber.Visible = false
- SlotNumber.Parent = SlotFrame
- HotkeyFns[ZERO_KEY_VALUE + slotNum] = slot.Select
- end
- end
-
- do -- Dragging Logic
- local startPoint: UDim2 = SlotFrame.Position
- local lastUpTime: number = 0
- local startParent: any = nil
-
- SlotFrame.DragBegin:Connect(function(dragPoint: UDim2): ()
- Dragging[SlotFrame] = true
- startPoint = dragPoint
-
- SlotFrame.BorderSizePixel = 2
- inventoryIcon:lock()
-
- -- Raise above other slots
- SlotFrame.ZIndex = 2
- ToolIcon.ZIndex = 2
- ToolName.ZIndex = 2
- SlotFrame.Parent.ZIndex = 2
- if SlotNumber then
- SlotNumber.ZIndex = 2
- end
- -- if HighlightFrame then
- -- HighlightFrame.ZIndex = 2
- -- for _, child in pairs(HighlightFrame:GetChildren()) do
- -- child.ZIndex = 2
- -- end
- -- end
-
- -- Circumvent the ScrollingFrame's ClipsDescendants property
- startParent = SlotFrame.Parent
- if startParent == UIGridFrame then
- local newPosition: UDim2 = UDim2.new(
- 0,
- SlotFrame.AbsolutePosition.X - InventoryFrame.AbsolutePosition.X,
- 0,
- SlotFrame.AbsolutePosition.Y - InventoryFrame.AbsolutePosition.Y
- )
- SlotFrame.Parent = InventoryFrame
- SlotFrame.Position = newPosition
-
- FakeSlotFrame = Instance.new("Frame")
- FakeSlotFrame.Name = "FakeSlot"
- FakeSlotFrame.LayoutOrder = SlotFrame.LayoutOrder
- FakeSlotFrame.Size = SlotFrame.Size
- FakeSlotFrame.BackgroundTransparency = 1
- FakeSlotFrame.Parent = UIGridFrame
- end
- end)
-
- SlotFrame.DragStopped:Connect(function(x: number, y: number): ()
- if FakeSlotFrame then
- FakeSlotFrame:Destroy()
- end
-
- local now: number = os.clock()
-
- SlotFrame.Position = startPoint
- SlotFrame.Parent = startParent
-
- SlotFrame.BorderSizePixel = 0
- inventoryIcon:unlock()
-
- -- Restore height
- SlotFrame.ZIndex = 1
- ToolIcon.ZIndex = 1
- ToolName.ZIndex = 1
- startParent.ZIndex = 1
-
- if SlotNumber then
- SlotNumber.ZIndex = 1
- end
- -- if HighlightFrame then
- -- HighlightFrame.ZIndex = 1
- -- for _, child in pairs(HighlightFrame:GetChildren()) do
- -- child.ZIndex = 1
- -- end
- -- end
-
- Dragging[SlotFrame] = nil
-
- -- Make sure the tool wasn't dropped
- if not slot.Tool then
- return
- end
-
- -- Check where we were dropped
- if CheckBounds(InventoryFrame, x, y) then
- if slot.Index <= NumberOfHotbarSlots then
- slot:MoveToInventory()
- end
- -- Check for double clicking on an inventory slot, to move into empty hotbar slot
- if slot.Index > NumberOfHotbarSlots and now - lastUpTime < DOUBLE_CLICK_TIME then
- if LowestEmptySlot then
- local myTool: any = slot.Tool
- slot:Clear()
- LowestEmptySlot:Fill(myTool)
- slot:Delete()
- end
- now = 0 -- Resets the timer
- end
- elseif CheckBounds(HotbarFrame, x, y) then
- local closest: { number } = { math.huge, nil :: any }
- for i: number = 1, NumberOfHotbarSlots do
- local otherSlot: any = Slots[i]
- local offset: number = GetOffset(otherSlot.Frame, Vector2.new(x, y))
- if offset < closest[1] then
- closest = { offset, otherSlot }
- end
- end
- local closestSlot: any = closest[2]
- if closestSlot ~= slot then
- slot:Swap(closestSlot)
- if slot.Index > NumberOfHotbarSlots then
- local tool: Tool = slot.Tool
- if not tool then -- Clean up after ourselves if we're an inventory slot that's now empty
- slot:Delete()
- else -- Moved inventory slot to hotbar slot, and gained a tool that needs to be unequipped
- if IsEquipped(tool) then --NOTE: HopperBin
- UnequipAllTools()
- end
- -- Also hide the inventory slot if we're showing results right now
- if ViewingSearchResults then
- slot.Frame.Visible = false
- slot.Frame.Parent = InventoryFrame
- end
- end
- end
- end
- else
- -- local tool = slot.Tool
- -- if tool.CanBeDropped then --TODO: HopperBins
- -- tool.Parent = workspace
- -- --TODO: Move away from character
- -- end
- if slot.Index <= NumberOfHotbarSlots then
- slot:MoveToInventory() --NOTE: Temporary
- end
- end
-
- lastUpTime = now
- end)
- end
-
- -- All ready!
- SlotFrame.Parent = parent
- Slots[index] = slot
-
- if index > NumberOfHotbarSlots then
- UpdateScrollingFrameCanvasSize()
- -- Scroll to new inventory slot, if we're open and not viewing search results
- if InventoryFrame.Visible and not ViewingSearchResults then
- local offset: number = ScrollingFrame.CanvasSize.Y.Offset - ScrollingFrame.AbsoluteSize.Y
- ScrollingFrame.CanvasPosition = Vector2.new(0, math.max(0, offset))
- end
- end
-
- return slot
-end
-
-local function OnChildAdded(child: Instance): () -- To Character or Backpack
- if not child:IsA("Tool") and not child:IsA("HopperBin") then --NOTE: HopperBin
- if child:IsA("Humanoid") and child.Parent == Character then
- Humanoid = child
- end
- return
- end
- local tool: any = child
-
- if tool.Parent == Character then
- ShowVRBackpackPopup()
- end
-
- if ActiveHopper and tool.Parent == Character then --NOTE: HopperBin
- DisableActiveHopper()
- end
-
- --TODO: Optimize / refactor / do something else
- if not StarterToolFound and tool.Parent == Character and not SlotsByTool[tool] then
- local starterGear: Instance? = Player:FindFirstChild("StarterGear")
- if starterGear then
- if starterGear:FindFirstChild(tool.Name) then
- StarterToolFound = true
- local slot: any = LowestEmptySlot or MakeSlot(UIGridFrame)
- for i: number = slot.Index, 1, -1 do
- local curr = Slots[i] -- An empty slot, because above
- local pIndex: number = i - 1
- if pIndex > 0 then
- local prev = Slots[pIndex] -- Guaranteed to be full, because above
- prev:Swap(curr)
- else
- curr:Fill(tool)
- end
- end
- -- Have to manually unequip a possibly equipped tool
- for _, children: Instance in pairs(Character:GetChildren()) do
- if children:IsA("Tool") and children ~= tool then
- children.Parent = Backpack
- end
- end
- AdjustHotbarFrames()
- return -- We're done here
- end
- end
- end
-
- -- The tool is either moving or new
- local slot: any = SlotsByTool[tool]
- if slot then
- slot:UpdateEquipView()
- else -- New! Put into lowest hotbar slot or new inventory slot
- slot = LowestEmptySlot or MakeSlot(UIGridFrame)
- slot:Fill(tool)
- if slot.Index <= NumberOfHotbarSlots and not InventoryFrame.Visible then
- AdjustHotbarFrames()
- end
- if tool:IsA("HopperBin") then --NOTE: HopperBin
- if tool.Active then
- UnequipAllTools()
- ActiveHopper = tool
- end
- end
- end
-
- BackpackScript.BackpackItemAdded:Fire(slot)
-end
-
-local function OnChildRemoved(child: Instance): () -- From Character or Backpack
- if not child:IsA("Tool") and not child:IsA("HopperBin") then --NOTE: HopperBin
- return
- end
- local tool: Tool | any = child
-
- ShowVRBackpackPopup()
-
- -- Ignore this event if we're just moving between the two
- local newParent: any = tool.Parent
- if newParent == Character or newParent == Backpack then
- return
- end
-
- local slot: any = SlotsByTool[tool]
- if slot then
- slot:Clear()
- if slot.Index > NumberOfHotbarSlots then -- Inventory slot
- slot:Delete()
- elseif not InventoryFrame.Visible then
- AdjustHotbarFrames()
- end
- end
-
- if tool :: any == ActiveHopper then --NOTE: HopperBin
- ActiveHopper = nil :: any
- end
-
- if slot then
- BackpackScript.BackpackItemRemoved:Fire(slot)
- end
- if isInventoryEmpty() then
- BackpackScript.BackpackEmpty:Fire()
- end
-end
-
-local function OnCharacterAdded(character: Model): ()
- -- First, clean up any old slots
- for i: number = #Slots, 1, -1 do
- local slot = Slots[i]
- if slot.Tool then
- slot:Clear()
- end
- if i > NumberOfHotbarSlots then
- slot:Delete()
- end
- end
- ActiveHopper = nil :: any --NOTE: HopperBin
-
- -- And any old Connections
- for _, conn: RBXScriptConnection in pairs(CharConns) do
- conn:Disconnect()
- end
- CharConns = {}
-
- -- Hook up the new character
- Character = character
- table.insert(CharConns, character.ChildRemoved:Connect(OnChildRemoved))
- table.insert(CharConns, character.ChildAdded:Connect(OnChildAdded))
- for _, child: Instance in pairs(character:GetChildren()) do
- OnChildAdded(child)
- end
- --NOTE: Humanoid is set inside OnChildAdded
-
- -- And the new backpack, when it gets here
- Backpack = Player:WaitForChild("Backpack")
- table.insert(CharConns, Backpack.ChildRemoved:Connect(OnChildRemoved))
- table.insert(CharConns, Backpack.ChildAdded:Connect(OnChildAdded))
- for _, child: Instance in pairs(Backpack:GetChildren()) do
- OnChildAdded(child)
- end
-
- AdjustHotbarFrames()
-end
-
-local function OnInputBegan(input: InputObject, isProcessed: boolean): ()
- local ChatInputBarConfiguration =
- TextChatService:FindFirstChildOfClass("ChatInputBarConfiguration") :: ChatInputBarConfiguration
- -- Pass through keyboard hotkeys when not typing into a TextBox and not disabled (except for the Drop key)
- if
- input.UserInputType == Enum.UserInputType.Keyboard
- and not TextBoxFocused
- and not ChatInputBarConfiguration.IsFocused
- and (WholeThingEnabled or input.KeyCode.Value == DROP_HOTKEY_VALUE)
- then
- local hotkeyBehavior: any = HotkeyFns[input.KeyCode.Value]
- if hotkeyBehavior then
- hotkeyBehavior(isProcessed)
- end
- end
-
- local inputType: Enum.UserInputType = input.UserInputType
- if not isProcessed then
- if inputType == Enum.UserInputType.MouseButton1 or inputType == Enum.UserInputType.Touch then
- if InventoryFrame.Visible then
- inventoryIcon:deselect()
- end
- end
- end
-end
-
-local function OnUISChanged(): ()
- -- Detect if player is using Touch
- if UserInputService:GetLastInputType() == Enum.UserInputType.Touch then
- for i: number = 1, NumberOfHotbarSlots do
- Slots[i]:TurnNumber(false)
- end
- return
- end
-
- -- Detect if player is using Keyboard
- if UserInputService:GetLastInputType() == Enum.UserInputType.Keyboard then
- for i: number = 1, NumberOfHotbarSlots do
- Slots[i]:TurnNumber(true)
- end
- return
- end
-
- -- Detect if player is using Mouse
- for _, mouse: any in pairs(MOUSE_INPUT_TYPES) do
- if UserInputService:GetLastInputType() == mouse then
- for i: number = 1, NumberOfHotbarSlots do
- Slots[i]:TurnNumber(true)
- end
- return
- end
- end
-
- -- Detect if player is using Controller
- for _, gamepad: any in pairs(GAMEPAD_INPUT_TYPES) do
- if UserInputService:GetLastInputType() == gamepad then
- for i: number = 1, NumberOfHotbarSlots do
- Slots[i]:TurnNumber(false)
- end
- return
- end
- end
-end
-
-local lastChangeToolInputObject: InputObject = nil
-local lastChangeToolInputTime: number = nil
-local maxEquipDeltaTime: number = 0.06
-local noOpFunc = function() end
--- local selectDirection = Vector2.new(0, 0)
-
-function unbindAllGamepadEquipActions(): ()
- ContextActionService:UnbindAction("BackpackHasGamepadFocus")
- ContextActionService:UnbindAction("BackpackCloseInventory")
-end
-
--- local function setHotbarVisibility(visible: boolean, isInventoryScreen: boolean)
--- for i: number = 1, NumberOfHotbarSlots do
--- local hotbarSlot = Slots[i]
--- if hotbarSlot and hotbarSlot.Frame and (isInventoryScreen or hotbarSlot.Tool) then
--- hotbarSlot.Frame.Visible = visible
--- end
--- end
--- end
-
--- local function getInputDirection(inputObject: InputObject): Vector2
--- local buttonModifier = 1
--- if inputObject.UserInputState == Enum.UserInputState.End then
--- buttonModifier = -1
--- end
-
--- if inputObject.KeyCode == Enum.KeyCode.Thumbstick1 then
--- local Magnitude = inputObject.Position.Magnitude
-
--- if Magnitude > 0.98 then
--- local normalizedVector =
--- Vector2.new(inputObject.Position.X / Magnitude, -inputObject.Position.Y / Magnitude)
--- selectDirection = normalizedVector
--- else
--- selectDirection = Vector2.new(0, 0)
--- end
--- elseif inputObject.KeyCode == Enum.KeyCode.DPadLeft then
--- selectDirection = Vector2.new(selectDirection.X - 1 * buttonModifier, selectDirection.Y)
--- elseif inputObject.KeyCode == Enum.KeyCode.DPadRight then
--- selectDirection = Vector2.new(selectDirection.X + 1 * buttonModifier, selectDirection.Y)
--- elseif inputObject.KeyCode == Enum.KeyCode.DPadUp then
--- selectDirection = Vector2.new(selectDirection.X, selectDirection.Y - 1 * buttonModifier)
--- elseif inputObject.KeyCode == Enum.KeyCode.DPadDown then
--- selectDirection = Vector2.new(selectDirection.X, selectDirection.Y + 1 * buttonModifier)
--- else
--- selectDirection = Vector2.new(0, 0)
--- end
-
--- return selectDirection
--- end
-
--- local selectToolExperiment = function(actionName: string, inputState: Enum.UserInputState, inputObject: InputObject)
--- local inputDirection = getInputDirection(inputObject)
-
--- if inputDirection == Vector2.new(0, 0) then
--- return
--- end
-
--- local angle = math.atan2(inputDirection.Y, inputDirection.X) - math.atan2(-1, 0)
--- if angle < 0 then
--- angle = angle + (math.pi * 2)
--- end
-
--- local quarterPi = (math.pi * 0.25)
-
--- local index = (angle / quarterPi) + 1
--- index = math.floor(index + 0.5) -- round index to whole number
--- if index > NumberOfHotbarSlots then
--- index = 1
--- end
-
--- if index > 0 then
--- local selectedSlot = Slots[index]
--- if selectedSlot and selectedSlot.Tool and not selectedSlot:IsEquipped() then
--- selectedSlot:Select()
--- end
--- else
--- UnequipAllTools()
--- end
--- end
-
--- selene: allow(unused_variable)
-changeToolFunc = function(actionName: string, inputState: Enum.UserInputState, inputObject: InputObject): ()
- if inputState ~= Enum.UserInputState.Begin then
- return
- end
-
- if lastChangeToolInputObject then
- if
- (
- lastChangeToolInputObject.KeyCode == Enum.KeyCode.ButtonR1
- and inputObject.KeyCode == Enum.KeyCode.ButtonL1
- )
- or (
- lastChangeToolInputObject.KeyCode == Enum.KeyCode.ButtonL1
- and inputObject.KeyCode == Enum.KeyCode.ButtonR1
- )
- then
- if (os.clock() - lastChangeToolInputTime) <= maxEquipDeltaTime then
- UnequipAllTools()
- lastChangeToolInputObject = inputObject
- lastChangeToolInputTime = os.clock()
- return
- end
- end
- end
-
- lastChangeToolInputObject = inputObject
- lastChangeToolInputTime = os.clock()
-
- task.delay(maxEquipDeltaTime, function(): ()
- if lastChangeToolInputObject ~= inputObject then
- return
- end
-
- local moveDirection: number = 0
- if inputObject.KeyCode == Enum.KeyCode.ButtonL1 then
- moveDirection = -1
- else
- moveDirection = 1
- end
-
- for i: number = 1, NumberOfHotbarSlots do
- local hotbarSlot: any = Slots[i]
- if hotbarSlot:IsEquipped() then
- local newSlotPosition: number = moveDirection + i
- local hitEdge: boolean = false
- if newSlotPosition > NumberOfHotbarSlots then
- newSlotPosition = 1
- hitEdge = true
- elseif newSlotPosition < 1 then
- newSlotPosition = NumberOfHotbarSlots
- hitEdge = true
- end
-
- local origNewSlotPos: number = newSlotPosition
- while not Slots[newSlotPosition].Tool do
- newSlotPosition = newSlotPosition + moveDirection
- if newSlotPosition == origNewSlotPos then
- return
- end
-
- if newSlotPosition > NumberOfHotbarSlots then
- newSlotPosition = 1
- hitEdge = true
- elseif newSlotPosition < 1 then
- newSlotPosition = NumberOfHotbarSlots
- hitEdge = true
- end
- end
-
- if hitEdge then
- UnequipAllTools()
- lastEquippedSlot = nil
- else
- Slots[newSlotPosition]:Select()
- end
- return
- end
- end
-
- if lastEquippedSlot and lastEquippedSlot.Tool then
- lastEquippedSlot:Select()
- return
- end
-
- local startIndex: number = moveDirection == -1 and NumberOfHotbarSlots or 1
- local endIndex: number = moveDirection == -1 and 1 or NumberOfHotbarSlots
- for i: number = startIndex, endIndex, moveDirection do
- if Slots[i].Tool then
- Slots[i]:Select()
- return
- end
- end
- end)
-end
-
-function getGamepadSwapSlot(): any
- for i: number = 1, #Slots do
- if Slots[i].Frame.BorderSizePixel > 0 then
- return Slots[i]
- end
- end
- return
-end
-
--- selene: allow(unused_variable)
-function changeSlot(slot: any): ()
- local swapInVr: boolean = not VRService.VREnabled or InventoryFrame.Visible
-
- if slot.Frame == GuiService.SelectedObject and swapInVr then
- local currentlySelectedSlot: any = getGamepadSwapSlot()
-
- if currentlySelectedSlot then
- currentlySelectedSlot.Frame.BorderSizePixel = 0
- if currentlySelectedSlot ~= slot then
- slot:Swap(currentlySelectedSlot)
- VRInventorySelector.SelectionImageObject.Visible = false
-
- if slot.Index > NumberOfHotbarSlots and not slot.Tool then
- if GuiService.SelectedObject == slot.Frame then
- GuiService.SelectedObject = currentlySelectedSlot.Frame
- end
- slot:Delete()
- end
-
- if currentlySelectedSlot.Index > NumberOfHotbarSlots and not currentlySelectedSlot.Tool then
- if GuiService.SelectedObject == currentlySelectedSlot.Frame then
- GuiService.SelectedObject = slot.Frame
- end
- currentlySelectedSlot:Delete()
- end
- end
- else
- local startSize: UDim2 = slot.Frame.Size
- local startPosition: UDim2 = slot.Frame.Position
- slot.Frame:TweenSizeAndPosition(
- startSize + UDim2.fromOffset(10, 10),
- startPosition - UDim2.fromOffset(5, 5),
- Enum.EasingDirection.Out,
- Enum.EasingStyle.Quad,
- 0.1,
- true,
- function(): ()
- slot.Frame:TweenSizeAndPosition(
- startSize,
- startPosition,
- Enum.EasingDirection.In,
- Enum.EasingStyle.Quad,
- 0.1,
- true
- )
- end
- )
- slot.Frame.BorderSizePixel = 3
- VRInventorySelector.SelectionImageObject.Visible = true
- end
- else
- slot:Select()
- VRInventorySelector.SelectionImageObject.Visible = false
- end
-end
-
-function vrMoveSlotToInventory(): ()
- if not VRService.VREnabled then
- return
- end
-
- local currentlySelectedSlot: any = getGamepadSwapSlot()
- if currentlySelectedSlot and currentlySelectedSlot.Tool then
- currentlySelectedSlot.Frame.BorderSizePixel = 0
- currentlySelectedSlot:MoveToInventory()
- VRInventorySelector.SelectionImageObject.Visible = false
- end
-end
-
-function enableGamepadInventoryControl(): ()
- local goBackOneLevel = function(): ()
- -- if inputState ~= Enum.UserInputState.Begin then
- -- return
- -- end
-
- local selectedSlot: any = getGamepadSwapSlot()
- if selectedSlot then
- -- selene: allow(shadowing)
- local selectedSlot: any = getGamepadSwapSlot()
- if selectedSlot then
- selectedSlot.Frame.BorderSizePixel = 0
- return
- end
- elseif InventoryFrame.Visible then
- inventoryIcon:deselect()
- end
- end
-
- ContextActionService:BindAction("BackpackHasGamepadFocus", noOpFunc, false, Enum.UserInputType.Gamepad1)
- ContextActionService:BindAction(
- "BackpackCloseInventory",
- goBackOneLevel,
- false,
- Enum.KeyCode.ButtonB,
- Enum.KeyCode.ButtonStart
- )
-
- -- Gaze select will automatically select the object for us!
- if not UseGazeSelection() then
- GuiService.SelectedObject = HotbarFrame:FindFirstChild("1")
- end
-end
-
-function disableGamepadInventoryControl(): ()
- unbindAllGamepadEquipActions()
-
- for i: number = 1, NumberOfHotbarSlots do
- local hotbarSlot: any = Slots[i]
- if hotbarSlot and hotbarSlot.Frame then
- hotbarSlot.Frame.BorderSizePixel = 0
- end
- end
-
- if GuiService.SelectedObject and GuiService.SelectedObject:IsDescendantOf(MainFrame) then
- GuiService.SelectedObject = nil
- end
-end
-
-local function bindBackpackHotbarAction(): ()
- if WholeThingEnabled and not GamepadActionsBound then
- GamepadActionsBound = true
- ContextActionService:BindAction(
- "BackpackHotbarEquip",
- changeToolFunc,
- false,
- Enum.KeyCode.ButtonL1,
- Enum.KeyCode.ButtonR1
- )
- end
-end
-
-local function unbindBackpackHotbarAction(): ()
- disableGamepadInventoryControl()
- GamepadActionsBound = false
- ContextActionService:UnbindAction("BackpackHotbarEquip")
-end
-
-function gamepadDisconnected(): ()
- GamepadEnabled = false
- disableGamepadInventoryControl()
-end
-
-function gamepadConnected(): ()
- GamepadEnabled = true
- GuiService:AddSelectionParent("BackpackSelection", MainFrame)
-
- if FullHotbarSlots >= 1 then
- bindBackpackHotbarAction()
- end
-
- if InventoryFrame.Visible then
- enableGamepadInventoryControl()
- end
-end
-
-local function OnIconChanged(enabled: boolean): ()
- -- Check for enabling/disabling the whole thing
- local success, _topbarEnabled = pcall(function()
- return enabled and StarterGui:GetCore("TopbarEnabled")
- end)
-
- if not success then
- return
- end
-
- WholeThingEnabled = enabled
- MainFrame.Visible = enabled
-
- -- Eat/Release hotkeys (Doesn't affect UserInputService)
- -- for _, keyString in pairs(HotkeyStrings) do
- -- if enabled then
- -- GuiService:AddKey(keyString)
- -- else
- -- GuiService:RemoveKey(keyString)
- -- end
- -- end
-
- if enabled then
- if FullHotbarSlots >= 1 then
- bindBackpackHotbarAction()
- end
- else
- unbindBackpackHotbarAction()
- end
-end
-
-local function MakeVRRoundButton(name: string, image: string): (ImageButton, ImageLabel, ImageLabel)
- local newButton: ImageButton = Instance.new("ImageButton")
- newButton.BackgroundTransparency = 1
- newButton.Name = name
- newButton.Size = UDim2.fromOffset(40, 40)
- newButton.Image = "rbxasset://textures/ui/Keyboard/close_button_background.png"
-
- local buttonIcon: ImageLabel = Instance.new("ImageLabel")
- buttonIcon.Name = "Icon"
- buttonIcon.BackgroundTransparency = 1
- buttonIcon.Size = UDim2.fromScale(0.5, 0.5)
- buttonIcon.Position = UDim2.fromScale(0.25, 0.25)
- buttonIcon.Image = image
- buttonIcon.Parent = newButton
-
- local buttonSelectionObject: ImageLabel = Instance.new("ImageLabel")
- buttonSelectionObject.BackgroundTransparency = 1
- buttonSelectionObject.Name = "Selection"
- buttonSelectionObject.Size = UDim2.fromScale(0.9, 0.9)
- buttonSelectionObject.Position = UDim2.fromScale(0.05, 0.05)
- buttonSelectionObject.Image = "rbxasset://textures/ui/Keyboard/close_button_selection.png"
- newButton.SelectionImageObject = buttonSelectionObject
-
- return newButton, buttonIcon, buttonSelectionObject
-end
-
--- Make the main frame, which (mostly) covers the screen
-MainFrame = Instance.new("Frame")
-MainFrame.BackgroundTransparency = 1
-MainFrame.Name = "Backpack"
-MainFrame.Size = UDim2.fromScale(1, 1)
-MainFrame.Visible = false
-MainFrame.Parent = BackpackGui
-
--- Make the HotbarFrame, which holds only the Hotbar Slots
-HotbarFrame = Instance.new("Frame")
-HotbarFrame.BackgroundTransparency = 1
-HotbarFrame.Name = "Hotbar"
-HotbarFrame.Size = UDim2.fromScale(1, 1)
-HotbarFrame.Parent = MainFrame
-
--- Make all the Hotbar Slots
-for index: number = 1, NumberOfHotbarSlots do
- local slot: any = MakeSlot(HotbarFrame, index)
- slot.Frame.Visible = false
-
- if not LowestEmptySlot then
- LowestEmptySlot = slot
- end
-end
-
-local LeftBumperButton: ImageLabel = Instance.new("ImageLabel")
-LeftBumperButton.BackgroundTransparency = 1
-LeftBumperButton.Name = "LeftBumper"
-LeftBumperButton.Size = UDim2.fromOffset(40, 40)
-LeftBumperButton.Position = UDim2.new(0, -LeftBumperButton.Size.X.Offset, 0.5, -LeftBumperButton.Size.Y.Offset / 2)
-
-local RightBumperButton: ImageLabel = Instance.new("ImageLabel")
-RightBumperButton.BackgroundTransparency = 1
-RightBumperButton.Name = "RightBumper"
-RightBumperButton.Size = UDim2.fromOffset(40, 40)
-RightBumperButton.Position = UDim2.new(1, 0, 0.5, -RightBumperButton.Size.Y.Offset / 2)
-
--- Make the Inventory, which holds the ScrollingFrame, the header, and the search box
-InventoryFrame = Instance.new("Frame")
-InventoryFrame.Name = "Inventory"
-InventoryFrame.Size = UDim2.fromScale(1, 1)
-InventoryFrame.BackgroundTransparency = BACKGROUND_TRANSPARENCY
-InventoryFrame.BackgroundColor3 = BACKGROUND_COLOR
-InventoryFrame.Active = true
-InventoryFrame.Visible = false
-InventoryFrame.Parent = MainFrame
-
--- Add corners to the InventoryFrame
-local corner: UICorner = Instance.new("UICorner")
-corner.Name = "Corner"
-corner.CornerRadius = BACKGROUND_CORNER_RADIUS
-corner.Parent = InventoryFrame
-
-VRInventorySelector = Instance.new("TextButton")
-VRInventorySelector.Name = "VRInventorySelector"
-VRInventorySelector.Position = UDim2.new(0, 0, 0, 0)
-VRInventorySelector.Size = UDim2.fromScale(1, 1)
-VRInventorySelector.BackgroundTransparency = 1
-VRInventorySelector.Text = ""
-VRInventorySelector.Parent = InventoryFrame
-
-local selectorImage: ImageLabel = Instance.new("ImageLabel")
-selectorImage.BackgroundTransparency = 1
-selectorImage.Name = "Selector"
-selectorImage.Size = UDim2.fromScale(1, 1)
-selectorImage.Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png"
-selectorImage.ScaleType = Enum.ScaleType.Slice
-selectorImage.SliceCenter = Rect.new(12, 12, 52, 52)
-selectorImage.Visible = false
-VRInventorySelector.SelectionImageObject = selectorImage
-
-VRInventorySelector.MouseButton1Click:Connect(function(): ()
- vrMoveSlotToInventory()
-end)
-
--- Make the ScrollingFrame, which holds the rest of the Slots (however many)
-ScrollingFrame = Instance.new("ScrollingFrame")
-ScrollingFrame.BackgroundTransparency = 1
-ScrollingFrame.Name = "ScrollingFrame"
-ScrollingFrame.Size = UDim2.fromScale(1, 1)
-ScrollingFrame.Selectable = false
-ScrollingFrame.ScrollingDirection = Enum.ScrollingDirection.Y
-ScrollingFrame.BorderSizePixel = 0
-ScrollingFrame.ScrollBarThickness = 8
-ScrollingFrame.ScrollBarImageColor3 = Color3.new(1, 1, 1)
-ScrollingFrame.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar
-ScrollingFrame.CanvasSize = UDim2.new(0, 0, 0, 0)
-ScrollingFrame.Parent = InventoryFrame
-
-UIGridFrame = Instance.new("Frame")
-UIGridFrame.BackgroundTransparency = 1
-UIGridFrame.Name = "UIGridFrame"
-UIGridFrame.Selectable = false
-UIGridFrame.Size = UDim2.new(1, -(ICON_BUFFER_PIXELS * 2), 1, 0)
-UIGridFrame.Position = UDim2.fromOffset(ICON_BUFFER_PIXELS, 0)
-UIGridFrame.Parent = ScrollingFrame
-
-UIGridLayout = Instance.new("UIGridLayout")
-UIGridLayout.SortOrder = Enum.SortOrder.LayoutOrder
-UIGridLayout.CellSize = UDim2.fromOffset(ICON_SIZE_PIXELS, ICON_SIZE_PIXELS)
-UIGridLayout.CellPadding = UDim2.fromOffset(ICON_BUFFER_PIXELS, ICON_BUFFER_PIXELS)
-UIGridLayout.Parent = UIGridFrame
-
-ScrollUpInventoryButton = MakeVRRoundButton("ScrollUpButton", "rbxasset://textures/ui/Backpack/ScrollUpArrow.png")
-ScrollUpInventoryButton.Size = UDim2.fromOffset(34, 34)
-ScrollUpInventoryButton.Position =
- UDim2.new(0.5, -ScrollUpInventoryButton.Size.X.Offset / 2, 0, INVENTORY_HEADER_SIZE + 3)
-ScrollUpInventoryButton.Icon.Position = ScrollUpInventoryButton.Icon.Position - UDim2.fromOffset(0, 2)
-ScrollUpInventoryButton.MouseButton1Click:Connect(function(): ()
- ScrollingFrame.CanvasPosition = Vector2.new(
- ScrollingFrame.CanvasPosition.X,
- Clamp(
- 0,
- ScrollingFrame.CanvasSize.Y.Offset - ScrollingFrame.AbsoluteWindowSize.Y,
- ScrollingFrame.CanvasPosition.Y - (ICON_BUFFER_PIXELS + ICON_SIZE_PIXELS)
- )
- )
-end)
-
-ScrollDownInventoryButton = MakeVRRoundButton("ScrollDownButton", "rbxasset://textures/ui/Backpack/ScrollUpArrow.png")
-ScrollDownInventoryButton.Rotation = 180
-ScrollDownInventoryButton.Icon.Position = ScrollDownInventoryButton.Icon.Position - UDim2.fromOffset(0, 2)
-ScrollDownInventoryButton.Size = UDim2.fromOffset(34, 34)
-ScrollDownInventoryButton.Position =
- UDim2.new(0.5, -ScrollDownInventoryButton.Size.X.Offset / 2, 1, -ScrollDownInventoryButton.Size.Y.Offset - 3)
-ScrollDownInventoryButton.MouseButton1Click:Connect(function(): ()
- ScrollingFrame.CanvasPosition = Vector2.new(
- ScrollingFrame.CanvasPosition.X,
- Clamp(
- 0,
- ScrollingFrame.CanvasSize.Y.Offset - ScrollingFrame.AbsoluteWindowSize.Y,
- ScrollingFrame.CanvasPosition.Y + (ICON_BUFFER_PIXELS + ICON_SIZE_PIXELS)
- )
- )
-end)
-
-ScrollingFrame.Changed:Connect(function(prop: string): ()
- if prop == "AbsoluteWindowSize" or prop == "CanvasPosition" or prop == "CanvasSize" then
- local canScrollUp: boolean = ScrollingFrame.CanvasPosition.Y ~= 0
- local canScrollDown: boolean = ScrollingFrame.CanvasPosition.Y
- < ScrollingFrame.CanvasSize.Y.Offset - ScrollingFrame.AbsoluteWindowSize.Y
-
- ScrollUpInventoryButton.Visible = canScrollUp
- ScrollDownInventoryButton.Visible = canScrollDown
- end
-end)
-
--- Position the frames and sizes for the Backpack GUI elements
-UpdateBackpackLayout()
-
---Make the gamepad hint frame
-local gamepadHintsFrame: Frame = Instance.new("Frame")
-gamepadHintsFrame.Name = "GamepadHintsFrame"
-gamepadHintsFrame.Size = UDim2.fromOffset(HotbarFrame.Size.X.Offset, (IsTenFootInterface and 95 or 60))
-gamepadHintsFrame.BackgroundTransparency = BACKGROUND_TRANSPARENCY
-gamepadHintsFrame.BackgroundColor3 = BACKGROUND_COLOR
-gamepadHintsFrame.Visible = false
-gamepadHintsFrame.Parent = MainFrame
-
-local gamepadHintsFrameLayout: UIListLayout = Instance.new("UIListLayout")
-gamepadHintsFrameLayout.Name = "Layout"
-gamepadHintsFrameLayout.Padding = UDim.new(0, 25)
-gamepadHintsFrameLayout.FillDirection = Enum.FillDirection.Horizontal
-gamepadHintsFrameLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
-gamepadHintsFrameLayout.SortOrder = Enum.SortOrder.LayoutOrder
-gamepadHintsFrameLayout.Parent = gamepadHintsFrame
-
-local gamepadHintsFrameCorner: UICorner = Instance.new("UICorner")
-gamepadHintsFrameCorner.Name = "Corner"
-gamepadHintsFrameCorner.CornerRadius = BACKGROUND_CORNER_RADIUS
-gamepadHintsFrameCorner.Parent = gamepadHintsFrame
-
-local function addGamepadHint(hintImageString: string, hintTextString: string): ()
- local hintFrame: Frame = Instance.new("Frame")
- hintFrame.Name = "HintFrame"
- hintFrame.AutomaticSize = Enum.AutomaticSize.XY
- hintFrame.BackgroundTransparency = 1
- hintFrame.Parent = gamepadHintsFrame
-
- local hintLayout: UIListLayout = Instance.new("UIListLayout")
- hintLayout.Name = "Layout"
- hintLayout.Padding = (IsTenFootInterface and UDim.new(0, 20) or UDim.new(0, 12))
- hintLayout.FillDirection = Enum.FillDirection.Horizontal
- hintLayout.SortOrder = Enum.SortOrder.LayoutOrder
- hintLayout.VerticalAlignment = Enum.VerticalAlignment.Center
- hintLayout.Parent = hintFrame
-
- local hintImage: ImageLabel = Instance.new("ImageLabel")
- hintImage.Name = "HintImage"
- hintImage.Size = (IsTenFootInterface and UDim2.fromOffset(60, 60) or UDim2.fromOffset(30, 30))
- hintImage.BackgroundTransparency = 1
- hintImage.Image = hintImageString
- hintImage.Parent = hintFrame
-
- local hintText: TextLabel = Instance.new("TextLabel")
- hintText.Name = "HintText"
- hintText.AutomaticSize = Enum.AutomaticSize.XY
- hintText.FontFace = Font.new(FONT_FAMILY.Family, Enum.FontWeight.Medium, Enum.FontStyle.Normal)
- hintText.TextSize = (IsTenFootInterface and 32 or 19)
- hintText.BackgroundTransparency = 1
- hintText.Text = hintTextString
- hintText.TextColor3 = Color3.new(1, 1, 1)
- hintText.TextXAlignment = Enum.TextXAlignment.Left
- hintText.TextYAlignment = Enum.TextYAlignment.Center
- hintText.TextWrapped = true
- hintText.Parent = hintFrame
-
- local textSizeConstraint: UITextSizeConstraint = Instance.new("UITextSizeConstraint")
- textSizeConstraint.MaxTextSize = hintText.TextSize
- textSizeConstraint.Parent = hintText
-end
-
-addGamepadHint(UserInputService:GetImageForKeyCode(Enum.KeyCode.ButtonX), "Remove From Hotbar")
-addGamepadHint(UserInputService:GetImageForKeyCode(Enum.KeyCode.ButtonA), "Select/Swap")
-addGamepadHint(UserInputService:GetImageForKeyCode(Enum.KeyCode.ButtonB), "Close Backpack")
-
-local function resizeGamepadHintsFrame(): ()
- gamepadHintsFrame.Size =
- UDim2.new(HotbarFrame.Size.X.Scale, HotbarFrame.Size.X.Offset, 0, (IsTenFootInterface and 95 or 60))
- gamepadHintsFrame.Position = UDim2.new(
- HotbarFrame.Position.X.Scale,
- HotbarFrame.Position.X.Offset,
- InventoryFrame.Position.Y.Scale,
- InventoryFrame.Position.Y.Offset - gamepadHintsFrame.Size.Y.Offset - ICON_BUFFER_PIXELS
- )
-
- local spaceTaken: number = 0
-
- local gamepadHints: { Instance } = gamepadHintsFrame:GetChildren()
- local filteredGamepadHints: any = {}
-
- for _, child: Instance in pairs(gamepadHints) do
- if child:IsA("GuiObject") then
- table.insert(filteredGamepadHints, child)
- end
- end
-
- --First get the total space taken by all the hints
- for guiObjects = 1, #filteredGamepadHints do
- if filteredGamepadHints[guiObjects]:IsA("GuiObject") then
- filteredGamepadHints[guiObjects].Size = UDim2.new(1, 0, 1, -5)
- filteredGamepadHints[guiObjects].Position = UDim2.new(0, 0, 0, 0)
- spaceTaken = spaceTaken
- + (
- filteredGamepadHints[guiObjects].HintText.Position.X.Offset
- + filteredGamepadHints[guiObjects].HintText.TextBounds.X
- )
- end
- end
-
- --The space between all the frames should be equal
- local spaceBetweenElements: number = (gamepadHintsFrame.AbsoluteSize.X - spaceTaken) / (#filteredGamepadHints - 1)
- for i: number = 1, #filteredGamepadHints do
- filteredGamepadHints[i].Position = (
- i == 1 and UDim2.new(0, 0, 0, 0)
- or UDim2.new(
- 0,
- filteredGamepadHints[i - 1].Position.X.Offset
- + filteredGamepadHints[i - 1].Size.X.Offset
- + spaceBetweenElements,
- 0,
- 0
- )
- )
- filteredGamepadHints[i].Size = UDim2.new(
- 0,
- (filteredGamepadHints[i].HintText.Position.X.Offset + filteredGamepadHints[i].HintText.TextBounds.X),
- 1,
- -5
- )
- end
-end
-
-local searchFrame: Frame = Instance.new("Frame")
-do -- Search stuff
- searchFrame.Name = "Search"
- searchFrame.BackgroundColor3 = SEARCH_BACKGROUND_COLOR
- searchFrame.BackgroundTransparency = SEARCH_BACKGROUND_TRANSPARENCY
- searchFrame.Size = UDim2.new(
- 0,
- SEARCH_WIDTH_PIXELS - (SEARCH_BUFFER_PIXELS * 2),
- 0,
- INVENTORY_HEADER_SIZE - (SEARCH_BUFFER_PIXELS * 2)
- )
- searchFrame.Position = UDim2.new(1, -searchFrame.Size.X.Offset - SEARCH_BUFFER_PIXELS, 0, SEARCH_BUFFER_PIXELS)
- searchFrame.Parent = InventoryFrame
-
- local searchFrameCorner: UICorner = Instance.new("UICorner")
- searchFrameCorner.Name = "Corner"
- searchFrameCorner.CornerRadius = SEARCH_CORNER_RADIUS
- searchFrameCorner.Parent = searchFrame
-
- local searchFrameBorder: UIStroke = Instance.new("UIStroke")
- searchFrameBorder.Name = "Border"
- searchFrameBorder.Color = SEARCH_BORDER_COLOR
- searchFrameBorder.Thickness = SEARCH_BORDER_THICKNESS
- searchFrameBorder.Transparency = SEARCH_BORDER_TRANSPARENCY
- searchFrameBorder.Parent = searchFrame
-
- local searchBox: TextBox = Instance.new("TextBox")
- searchBox.BackgroundTransparency = 1
- searchBox.Name = "TextBox"
- searchBox.Text = ""
- searchBox.TextColor3 = TEXT_COLOR
- searchBox.TextStrokeTransparency = TEXT_STROKE_TRANSPARENCY
- searchBox.TextStrokeColor3 = TEXT_STROKE_COLOR
- searchBox.FontFace = Font.new(FONT_FAMILY.Family, Enum.FontWeight.Medium, Enum.FontStyle.Normal)
- searchBox.PlaceholderText = SEARCH_TEXT_PLACEHOLDER
- searchBox.TextColor3 = TEXT_COLOR
- searchBox.TextTransparency = TEXT_STROKE_TRANSPARENCY
- searchBox.TextStrokeColor3 = TEXT_STROKE_COLOR
- searchBox.ClearTextOnFocus = false
- searchBox.TextTruncate = Enum.TextTruncate.AtEnd
- searchBox.TextSize = FONT_SIZE
- searchBox.TextXAlignment = Enum.TextXAlignment.Left
- searchBox.TextYAlignment = Enum.TextYAlignment.Center
- searchBox.Size = UDim2.new(
- 0,
- (SEARCH_WIDTH_PIXELS - (SEARCH_BUFFER_PIXELS * 2)) - (SEARCH_TEXT_OFFSET * 2) - 20,
- 0,
- INVENTORY_HEADER_SIZE - (SEARCH_BUFFER_PIXELS * 2) - (SEARCH_TEXT_OFFSET * 2)
- )
- searchBox.AnchorPoint = Vector2.new(0, 0.5)
- searchBox.Position = UDim2.new(0, SEARCH_TEXT_OFFSET, 0.5, 0)
- searchBox.ZIndex = 2
- searchBox.Parent = searchFrame
-
- local xButton: TextButton = Instance.new("TextButton")
- xButton.Name = "X"
- xButton.Text = ""
- xButton.Size = UDim2.fromOffset(30, 30)
- xButton.Position = UDim2.new(1, -xButton.Size.X.Offset, 0.5, -xButton.Size.Y.Offset / 2)
- xButton.ZIndex = 4
- xButton.Visible = false
- xButton.BackgroundTransparency = 1
- xButton.Parent = searchFrame
-
- local xImage: ImageButton = Instance.new("ImageButton")
- xImage.Name = "X"
- xImage.Image = SEARCH_IMAGE_X
- xImage.BackgroundTransparency = 1
- xImage.Size = UDim2.new(
- 0,
- searchFrame.Size.Y.Offset - (SEARCH_BUFFER_PIXELS * 4),
- 0,
- searchFrame.Size.Y.Offset - (SEARCH_BUFFER_PIXELS * 4)
- )
- xImage.AnchorPoint = Vector2.new(0.5, 0.5)
- xImage.Position = UDim2.fromScale(0.5, 0.5)
- xImage.ZIndex = 1
- xImage.BorderSizePixel = 0
- xImage.Parent = xButton
-
- local function search(): ()
- local terms: { [string]: boolean } = {}
- for word: string in searchBox.Text:gmatch("%S+") do
- terms[word:lower()] = true
- end
-
- local hitTable = {}
- for i: number = NumberOfHotbarSlots + 1, #Slots do -- Only search inventory slots
- local slot = Slots[i]
- local hits: any = slot:CheckTerms(terms)
- table.insert(hitTable, { slot, hits })
- slot.Frame.Visible = false
- slot.Frame.Parent = InventoryFrame
- end
-
- table.sort(hitTable, function(left: any, right: any): boolean
- return left[2] > right[2]
- end)
- ViewingSearchResults = true
-
- local hitCount: number = 0
- for _, data in ipairs(hitTable) do
- local slot, hits: any = data[1], data[2]
- if hits > 0 then
- slot.Frame.Visible = true
- slot.Frame.Parent = UIGridFrame
- slot.Frame.LayoutOrder = NumberOfHotbarSlots + hitCount
- hitCount = hitCount + 1
- end
- end
-
- ScrollingFrame.CanvasPosition = Vector2.new(0, 0)
- UpdateScrollingFrameCanvasSize()
-
- xButton.ZIndex = 3
- end
-
- local function clearResults(): ()
- if xButton.ZIndex > 0 then
- ViewingSearchResults = false
- for i: number = NumberOfHotbarSlots + 1, #Slots do
- local slot = Slots[i]
- slot.Frame.LayoutOrder = slot.Index
- slot.Frame.Parent = UIGridFrame
- slot.Frame.Visible = true
- end
- xButton.ZIndex = 0
- end
- UpdateScrollingFrameCanvasSize()
- end
-
- local function reset(): ()
- clearResults()
- searchBox.Text = ""
- end
-
- local function onChanged(property: string): ()
- if property == "Text" then
- local text: string = searchBox.Text
- if text == "" then
- searchBox.TextTransparency = TEXT_STROKE_TRANSPARENCY
- clearResults()
- elseif text ~= SEARCH_TEXT then
- searchBox.TextTransparency = 0
- search()
- end
- xButton.Visible = text ~= "" and text ~= SEARCH_TEXT
- end
- end
-
- local function focusLost(enterPressed: boolean): ()
- if enterPressed then
- --TODO: Could optimize
- search()
- end
- end
-
- xButton.MouseButton1Click:Connect(reset)
- searchBox.Changed:Connect(onChanged)
- searchBox.FocusLost:Connect(focusLost)
-
- BackpackScript.StateChanged.Event:Connect(function(isNowOpen: boolean): ()
- -- InventoryIcon:getInstance("iconButton").Modal = isNowOpen -- Allows free mouse movement even in first person
-
- if not isNowOpen then
- reset()
- end
- end)
-
- HotkeyFns[Enum.KeyCode.Escape.Value] = function(isProcessed: any): ()
- if isProcessed then -- Pressed from within a TextBox
- reset()
- end
- end
- local function detectGamepad(lastInputType: Enum.UserInputType): ()
- if lastInputType == Enum.UserInputType.Gamepad1 and not UserInputService.VREnabled then
- searchFrame.Visible = false
- else
- searchFrame.Visible = true
- end
- end
- UserInputService.LastInputTypeChanged:Connect(detectGamepad)
-end
-
--- When menu is opend, disable backpack
-GuiService.MenuOpened:Connect(function(): ()
- BackpackGui.Enabled = false
- inventoryIcon:setEnabled(false)
-end)
-
--- When menu is closed, enable backpack
-GuiService.MenuClosed:Connect(function(): ()
- BackpackGui.Enabled = true
- inventoryIcon:setEnabled(true)
-end)
-
-do -- Make the Inventory expand/collapse arrow (unless TopBar)
- -- selene: allow(unused_variable)
- local removeHotBarSlot = function(name: string, state: Enum.UserInputState, input: InputObject): ()
- if state ~= Enum.UserInputState.Begin then
- return
- end
- if not GuiService.SelectedObject then
- return
- end
-
- for i: number = 1, NumberOfHotbarSlots do
- if Slots[i].Frame == GuiService.SelectedObject and Slots[i].Tool then
- Slots[i]:MoveToInventory()
- return
- end
- end
- end
-
- local function openClose(): ()
- if not next(Dragging) then -- Only continue if nothing is being dragged
- InventoryFrame.Visible = not InventoryFrame.Visible
- local nowOpen: boolean = InventoryFrame.Visible
- AdjustHotbarFrames()
- HotbarFrame.Active = not HotbarFrame.Active
- for i: number = 1, NumberOfHotbarSlots do
- Slots[i]:SetClickability(not nowOpen)
- end
- end
-
- if InventoryFrame.Visible then
- if GamepadEnabled then
- if GAMEPAD_INPUT_TYPES[UserInputService:GetLastInputType()] then
- resizeGamepadHintsFrame()
- gamepadHintsFrame.Visible = not UserInputService.VREnabled
- end
- enableGamepadInventoryControl()
- end
- if BackpackPanel and VRService.VREnabled then
- BackpackPanel:SetVisible(true)
- BackpackPanel:RequestPositionUpdate()
- end
- else
- if GamepadEnabled then
- gamepadHintsFrame.Visible = false
- end
- disableGamepadInventoryControl()
- end
-
- if InventoryFrame.Visible then
- ContextActionService:BindAction("BackpackRemoveSlot", removeHotBarSlot, false, Enum.KeyCode.ButtonX)
- else
- ContextActionService:UnbindAction("BackpackRemoveSlot")
- end
-
- BackpackScript.IsOpen = InventoryFrame.Visible
- BackpackScript.StateChanged:Fire(InventoryFrame.Visible)
- end
-
- StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, false)
- BackpackScript.OpenClose = openClose -- Exposed
-end
-
--- Now that we're done building the GUI, we Connect to all the major events
-
--- Wait for the player if LocalPlayer wasn't ready earlier
-while not Player do
- task.wait()
- Player = Players.LocalPlayer
-end
-
--- Listen to current and all future characters of our player
-Player.CharacterAdded:Connect(OnCharacterAdded)
-if Player.Character then
- OnCharacterAdded(Player.Character)
-end
-
-do -- Hotkey stuff
- -- Listen to key down
- UserInputService.InputBegan:Connect(OnInputBegan)
-
- -- Listen to ANY TextBox gaining or losing focus, for disabling all hotkeys
- UserInputService.TextBoxFocused:Connect(function(): ()
- TextBoxFocused = true
- end)
- UserInputService.TextBoxFocusReleased:Connect(function(): ()
- TextBoxFocused = false
- end)
-
- -- Manual unequip for HopperBins on drop button pressed
- HotkeyFns[DROP_HOTKEY_VALUE] = function(): () --NOTE: HopperBin
- if ActiveHopper then
- UnequipAllTools()
- end
- end
-
- -- Listen to keyboard status, for showing/hiding hotkey labels
- UserInputService.LastInputTypeChanged:Connect(OnUISChanged)
- OnUISChanged()
-
- -- Listen to gamepad status, for allowing gamepad style selection/equip
- if UserInputService:GetGamepadConnected(Enum.UserInputType.Gamepad1) then
- gamepadConnected()
- end
- UserInputService.GamepadConnected:Connect(function(gamepadEnum: Enum.UserInputType): ()
- if gamepadEnum == Enum.UserInputType.Gamepad1 then
- gamepadConnected()
- end
- end)
- UserInputService.GamepadDisconnected:Connect(function(gamepadEnum: Enum.UserInputType): ()
- if gamepadEnum == Enum.UserInputType.Gamepad1 then
- gamepadDisconnected()
- end
- end)
-end
-
--- Sets whether the backpack is enabled or not
-function BackpackScript:SetBackpackEnabled(Enabled: boolean): ()
- BackpackEnabled = Enabled
-end
-
--- Returns if the backpack's inventory is open
-function BackpackScript:IsOpened(): boolean
- return BackpackScript.IsOpen
-end
-
--- Returns on if the backpack is enabled or not
-function BackpackScript:GetBackpackEnabled(): boolean
- return BackpackEnabled
-end
-
--- Returns the BindableEvent for when the backpack state changes
-function BackpackScript:GetStateChangedEvent(): BindableEvent
- return BackpackScript.StateChanged
-end
-
--- Update every heartbeat the icon state
-RunService.Heartbeat:Connect(function(): ()
- OnIconChanged(BackpackEnabled)
-end)
-
--- Update the transparency of the backpack based on GuiService.PreferredTransparency
-local function OnPreferredTransparencyChanged(): ()
- local preferredTransparency: number = GuiService.PreferredTransparency
-
- BACKGROUND_TRANSPARENCY = BACKGROUND_TRANSPARENCY_DEFAULT * preferredTransparency
- InventoryFrame.BackgroundTransparency = BACKGROUND_TRANSPARENCY
-
- SLOT_LOCKED_TRANSPARENCY = SLOT_LOCKED_TRANSPARENCY_DEFAULT * preferredTransparency
- for _, slot in ipairs(Slots) do
- slot.Frame.BackgroundTransparency = SLOT_LOCKED_TRANSPARENCY
- end
-
- SEARCH_BACKGROUND_TRANSPARENCY = SEARCH_BACKGROUND_TRANSPARENCY_DEFAULT * preferredTransparency
- searchFrame.BackgroundTransparency = SEARCH_BACKGROUND_TRANSPARENCY
-end
-GuiService:GetPropertyChangedSignal("PreferredTransparency"):Connect(OnPreferredTransparencyChanged)
-
-return BackpackScript
+--!strict
+
+const api = script.Api
+const bindableEvents = script.BindableEvents
+
+return {
+ -- Functions
+ getEnabled = require(api.getEnabled),
+ setEnabled = require(api.setEnabled),
+ getTheme = require(api.getTheme),
+ setTheme = require(api.setTheme),
+ getTopbarIcon = require(api.getTopbarIcon),
+ openInventory = require(api.openInventory),
+ closeInventory = require(api.closeInventory),
+ isInventoryOpen = require(api.isInventoryOpen),
+
+ -- Events
+ backpackItemAdded = bindableEvents.BackpackItemAdded.Event,
+ backpackItemRemoved = bindableEvents.BackpackItemRemoved.Event,
+ backpackItemEquipped = bindableEvents.BackpackItemEquipped.Event,
+ backpackItemUnequipped = bindableEvents.BackpackItemUnequipped.Event,
+ inventoryOpened = bindableEvents.InventoryOpened.Event,
+ inventoryClosed = bindableEvents.InventoryClosed.Event,
+ themeChanged = bindableEvents.ThemeChanged.Event,
+}
diff --git a/src/init.meta.json b/src/init.meta.json
deleted file mode 100644
index 6efa65d8..00000000
--- a/src/init.meta.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "properties": {
- "Attributes": {
- "BackgroundColor3": {
- "Color3": [0.0980392157, 0.105882353, 0.11372549]
- },
- "BackgroundTransparency": {
- "Float32": 0.3
- },
- "CornerRadius": {
- "UDim": [0, 8]
- },
- "EquipBorderColor3": {
- "Color3": [1, 1, 1]
- },
- "EquipBorderSizePixel": {
- "Float32": 5
- },
- "InsetIconPadding": {
- "Bool": true
- },
- "OutlineEquipBorder": {
- "Bool": true
- },
- "TextColor3": {
- "Color3": [1, 1, 1]
- },
- "TextSize": {
- "Float32": 16
- },
- "TextStrokeColor3": {
- "Color3": [0, 0, 0]
- },
- "TextStrokeTransparency": {
- "Float32": 0.5
- }
- }
- }
-}
diff --git a/develop.project.json b/test.project.json
similarity index 62%
rename from develop.project.json
rename to test.project.json
index c8168d13..bb738310 100644
--- a/develop.project.json
+++ b/test.project.json
@@ -1,12 +1,13 @@
{
- "name": "Develop Satchel",
+ "name": "Satchel",
"emitLegacyScripts": false,
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
- "SatchelLoader": {
- "$path": "models/SatchelLoader"
+ "$path": "tests",
+ "Satchel": {
+ "$path": "models/Satchel"
}
}
}
diff --git a/tests/PrintBackpack.client.luau b/tests/PrintBackpack.client.luau
new file mode 100644
index 00000000..5939f2f9
--- /dev/null
+++ b/tests/PrintBackpack.client.luau
@@ -0,0 +1,67 @@
+--!strict
+
+local Players = game:GetService("Players")
+
+local player = Players.LocalPlayer
+local backpack = player:WaitForChild("Backpack")
+local character = player.Character or player.CharacterAdded:Wait()
+
+-- Check if instance is an item
+local function isItem(instance: Tool | HopperBin): boolean
+ return instance:IsA("Tool") or instance:IsA("HopperBin")
+end
+
+-- Connect to item events
+local function onItem(item: Tool | HopperBin)
+ if not isItem(item) then
+ return
+ end
+
+ print("[Added]", item.Name)
+
+ item.AncestryChanged:Connect(function(_, newParent)
+ if newParent == character then
+ print("[Equipped]", item.Name)
+ elseif newParent == backpack then
+ print("[Unequipped]", item.Name)
+ elseif newParent == nil then
+ print("[Removed]", item.Name)
+ end
+ end)
+end
+
+local function onChildAdded(child)
+ if isItem(child) then
+ onItem(child)
+ end
+end
+
+local function onChildRemoved(child)
+ if isItem(child) and child.Parent ~= character and child.Parent ~= backpack then
+ print("[Removed]", child.Name)
+ end
+end
+
+backpack.ChildAdded:Connect(onChildAdded)
+backpack.ChildRemoved:Connect(onChildRemoved)
+character.ChildAdded:Connect(onChildAdded)
+character.ChildRemoved:Connect(onChildRemoved)
+
+-- Handle character respawn
+player.CharacterAdded:Connect(function(newCharacter)
+ character = newCharacter
+ character.ChildAdded:Connect(onChildAdded)
+ character.ChildRemoved:Connect(onChildRemoved)
+
+ for _, child in character:GetChildren() do
+ onChildAdded(child)
+ end
+end)
+
+-- Add existing items in backpack and character
+for _, child in backpack:GetChildren() do
+ onChildAdded(child)
+end
+for _, child in character:GetChildren() do
+ onChildAdded(child)
+end
diff --git a/tests/SatchelEnabled.client.luau b/tests/SatchelEnabled.client.luau
new file mode 100644
index 00000000..d6dd5df6
--- /dev/null
+++ b/tests/SatchelEnabled.client.luau
@@ -0,0 +1,36 @@
+--!strict
+
+const StarterGui = game:GetService("StarterGui")
+
+const Icon = require("./Satchel/Packages/topbarplus")
+const Satchel = require("./Satchel")
+
+const icon = Icon.new()
+icon:modifyTheme({
+ { "IconLabelContainer", "TargetWidth", 0 }, -- Force minimum width
+ { "IconLabel", "AutoLocalize", false }, -- Don't translate font icon
+})
+icon:select()
+icon:setLabel("two-switches-horizontal")
+icon:align("Right")
+icon:setTextSize(24)
+icon:setTextFont(
+ "rbxasset://LuaPackages/Packages/_Index/BuilderIcons/BuilderIcons/BuilderIcons.json",
+ Enum.FontWeight.Bold,
+ Enum.FontStyle.Normal,
+ "Selected"
+)
+icon:setTextFont(
+ "rbxasset://LuaPackages/Packages/_Index/BuilderIcons/BuilderIcons/BuilderIcons.json",
+ Enum.FontWeight.Regular,
+ Enum.FontStyle.Normal,
+ "Deselected"
+)
+icon:autoDeselect(false)
+icon:setCaption("Toggle Satchel")
+icon.toggled:Connect(function(isSelected, fromSource)
+ if fromSource == "User" then
+ Satchel.setEnabled(isSelected)
+ StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, not isSelected)
+ end
+end)
diff --git a/tests/SwitchTheme.client.luau b/tests/SwitchTheme.client.luau
new file mode 100644
index 00000000..d45b8bc1
--- /dev/null
+++ b/tests/SwitchTheme.client.luau
@@ -0,0 +1,65 @@
+--!strict
+
+const Icon = require("./Satchel/Packages/topbarplus")
+const Satchel = require("./Satchel")
+
+const icon = Icon.new()
+icon:modifyTheme({
+ { "IconLabelContainer", "TargetWidth", 0 }, -- Force minimum width
+ { "IconLabel", "AutoLocalize", false }, -- Don't translate font icon
+})
+icon:setLabel("two-arrows-left-right")
+icon:align("Right")
+icon:setTextSize(24)
+icon:setTextFont(
+ "rbxasset://LuaPackages/Packages/_Index/BuilderIcons/BuilderIcons/BuilderIcons.json",
+ Enum.FontWeight.Bold,
+ Enum.FontStyle.Normal,
+ "Selected"
+)
+icon:setTextFont(
+ "rbxasset://LuaPackages/Packages/_Index/BuilderIcons/BuilderIcons/BuilderIcons.json",
+ Enum.FontWeight.Regular,
+ Enum.FontStyle.Normal,
+ "Deselected"
+)
+icon:autoDeselect(false)
+icon:setCaption("Switch Theme")
+
+local defaultTheme: Icon.Icon
+local legacyTheme: Icon.Icon
+
+defaultTheme = Icon.new()
+defaultTheme:select()
+defaultTheme:setLabel("Default")
+defaultTheme:bindEvent("selected", function()
+ Satchel.setTheme("DefaultTheme")
+end)
+defaultTheme:bindEvent("deselected", function()
+ if not legacyTheme.isSelected then
+ defaultTheme:select()
+ Satchel.setTheme("DefaultTheme")
+ end
+end)
+
+legacyTheme = Icon.new()
+legacyTheme:setLabel("Legacy")
+legacyTheme:bindEvent("selected", function()
+ Satchel.setTheme("LegacyTheme")
+end)
+legacyTheme:bindEvent("deselected", function()
+ if not defaultTheme.isSelected then
+ defaultTheme:select()
+ Satchel.setTheme("DefaultTheme")
+ end
+end)
+
+icon:setDropdown({ defaultTheme, legacyTheme })
+
+Satchel.themeChanged:Connect(function(theme)
+ if theme.Name == "DefaultTheme" then
+ defaultTheme:select()
+ elseif theme.Name == "LegacyTheme" then
+ legacyTheme:select()
+ end
+end)
diff --git a/wally.lock b/wally.lock
index fbe98925..1beb3f25 100644
--- a/wally.lock
+++ b/wally.lock
@@ -3,11 +3,136 @@
registry = "test"
[[package]]
-name = "1foreverhd/topbarplus"
-version = "3.4.0"
+name = "evaera/promise"
+version = "4.0.0"
dependencies = []
+[[package]]
+name = "haedrix/react"
+version = "17.3.8"
+dependencies = [["LuauPolyfill", "jsdotlua/luau-polyfill@1.2.7"], ["ReactGlobals", "haedrix/react-globals@17.3.8"], ["Shared", "haedrix/shared@17.3.8"]]
+
+[[package]]
+name = "haedrix/react-debug-tools"
+version = "17.3.8"
+dependencies = [["LuauPolyfill", "jsdotlua/luau-polyfill@1.2.7"], ["ReactGlobals", "haedrix/react-globals@17.3.8"], ["ReactReconciler", "haedrix/react-reconciler@17.3.8"], ["Shared", "haedrix/shared@17.3.8"]]
+
+[[package]]
+name = "haedrix/react-devtools"
+version = "17.3.8"
+dependencies = [["ReactDevtoolsCore", "haedrix/react-devtools-core@17.3.8"]]
+
+[[package]]
+name = "haedrix/react-devtools-core"
+version = "17.3.8"
+dependencies = [["LuauPolyfill", "jsdotlua/luau-polyfill@1.2.7"], ["ReactDevtoolsShared", "haedrix/react-devtools-shared@17.3.8"], ["ReactGlobals", "haedrix/react-globals@17.3.8"], ["ReactTelemetry", "haedrix/react-telemetry@17.3.8"], ["SafeFlags", "haedrix/safe-flags@0.1.1"]]
+
+[[package]]
+name = "haedrix/react-devtools-shared"
+version = "17.3.8"
+dependencies = [["LuauPolyfill", "jsdotlua/luau-polyfill@1.2.7"], ["React", "haedrix/react@17.3.8"], ["ReactDebugTools", "haedrix/react-debug-tools@17.3.8"], ["ReactGlobals", "haedrix/react-globals@17.3.8"], ["ReactIs", "haedrix/react-is@17.3.8"], ["ReactReconciler", "haedrix/react-reconciler@17.3.8"], ["ReactRoblox", "haedrix/react-roblox@17.3.8"], ["Shared", "haedrix/shared@17.3.8"]]
+
+[[package]]
+name = "haedrix/react-globals"
+version = "17.3.8"
+dependencies = [["SafeFlags", "haedrix/safe-flags@0.1.1"]]
+
+[[package]]
+name = "haedrix/react-is"
+version = "17.3.8"
+dependencies = [["ReactGlobals", "haedrix/react-globals@17.3.8"], ["SafeFlags", "haedrix/safe-flags@0.1.1"], ["Shared", "haedrix/shared@17.3.8"]]
+
+[[package]]
+name = "haedrix/react-reconciler"
+version = "17.3.8"
+dependencies = [["LuauPolyfill", "jsdotlua/luau-polyfill@1.2.7"], ["Promise", "evaera/promise@4.0.0"], ["React", "haedrix/react@17.3.8"], ["ReactGlobals", "haedrix/react-globals@17.3.8"], ["SafeFlags", "haedrix/safe-flags@0.1.1"], ["Scheduler", "haedrix/scheduler@17.3.8"], ["Shared", "haedrix/shared@17.3.8"]]
+
+[[package]]
+name = "haedrix/react-roblox"
+version = "17.3.8"
+dependencies = [["LuauPolyfill", "jsdotlua/luau-polyfill@1.2.7"], ["React", "haedrix/react@17.3.8"], ["ReactGlobals", "haedrix/react-globals@17.3.8"], ["ReactReconciler", "haedrix/react-reconciler@17.3.8"], ["Scheduler", "haedrix/scheduler@17.3.8"], ["Shared", "haedrix/shared@17.3.8"]]
+
+[[package]]
+name = "haedrix/react-telemetry"
+version = "17.3.8"
+dependencies = [["LuauPolyfill", "jsdotlua/luau-polyfill@1.2.7"], ["ReactGlobals", "haedrix/react-globals@17.3.8"], ["SafeFlags", "haedrix/safe-flags@0.1.1"]]
+
+[[package]]
+name = "haedrix/safe-flags"
+version = "0.1.1"
+dependencies = []
+
+[[package]]
+name = "haedrix/scheduler"
+version = "17.3.8"
+dependencies = [["LuauPolyfill", "jsdotlua/luau-polyfill@1.2.7"], ["ReactGlobals", "haedrix/react-globals@17.3.8"], ["SafeFlags", "haedrix/safe-flags@0.1.1"], ["Shared", "haedrix/shared@17.3.8"]]
+
+[[package]]
+name = "haedrix/shared"
+version = "17.3.8"
+dependencies = [["LuauPolyfill", "jsdotlua/luau-polyfill@1.2.7"], ["ReactGlobals", "haedrix/react-globals@17.3.8"], ["SafeFlags", "haedrix/safe-flags@0.1.1"]]
+
+[[package]]
+name = "jsdotlua/boolean"
+version = "1.2.7"
+dependencies = [["number", "jsdotlua/number@1.2.7"]]
+
+[[package]]
+name = "jsdotlua/collections"
+version = "1.2.7"
+dependencies = [["es7-types", "jsdotlua/es7-types@1.2.7"], ["instance-of", "jsdotlua/instance-of@1.2.7"]]
+
+[[package]]
+name = "jsdotlua/console"
+version = "1.2.7"
+dependencies = [["collections", "jsdotlua/collections@1.2.7"]]
+
+[[package]]
+name = "jsdotlua/es7-types"
+version = "1.2.7"
+dependencies = []
+
+[[package]]
+name = "jsdotlua/instance-of"
+version = "1.2.7"
+dependencies = []
+
+[[package]]
+name = "jsdotlua/luau-polyfill"
+version = "1.2.7"
+dependencies = [["boolean", "jsdotlua/boolean@1.2.7"], ["collections", "jsdotlua/collections@1.2.7"], ["console", "jsdotlua/console@1.2.7"], ["es7-types", "jsdotlua/es7-types@1.2.7"], ["instance-of", "jsdotlua/instance-of@1.2.7"], ["math", "jsdotlua/math@1.2.7"], ["number", "jsdotlua/number@1.2.7"], ["string", "jsdotlua/string@1.2.7"], ["symbol-luau", "jsdotlua/symbol-luau@1.0.1"], ["timers", "jsdotlua/timers@1.2.7"]]
+
+[[package]]
+name = "jsdotlua/math"
+version = "1.2.7"
+dependencies = []
+
+[[package]]
+name = "jsdotlua/number"
+version = "1.2.7"
+dependencies = []
+
+[[package]]
+name = "jsdotlua/string"
+version = "1.2.7"
+dependencies = [["es7-types", "jsdotlua/es7-types@1.2.7"], ["number", "jsdotlua/number@1.2.7"]]
+
+[[package]]
+name = "jsdotlua/symbol-luau"
+version = "1.0.1"
+dependencies = []
+
+[[package]]
+name = "jsdotlua/timers"
+version = "1.2.7"
+dependencies = [["collections", "jsdotlua/collections@1.2.7"]]
+
[[package]]
name = "ryanlua/satchel"
-version = "1.4.1"
-dependencies = [["topbarplus", "1foreverhd/topbarplus@3.4.0"]]
+version = "2.0.0"
+dependencies = [["react", "haedrix/react@17.3.8"], ["react-devtools", "haedrix/react-devtools@17.3.8"], ["react-globals", "haedrix/react-globals@17.3.8"], ["react-roblox", "haedrix/react-roblox@17.3.8"], ["topbarplus", "ryanlua/topbarplus@1.0.1"]]
+
+[[package]]
+name = "ryanlua/topbarplus"
+version = "1.0.1"
+dependencies = []
diff --git a/wally.toml b/wally.toml
index e028f28c..dd6b2cca 100644
--- a/wally.toml
+++ b/wally.toml
@@ -1,7 +1,7 @@
[package]
name = "ryanlua/satchel"
description = "A modern open-source alternative to Roblox's default backpack."
-version = "1.4.1"
+version = "2.0.0"
license = "MPL-2.0"
authors = ["Ryan Luu "]
realm = "shared"
@@ -12,4 +12,8 @@ exclude = ["**"]
include = ["src", "src/**", "wally.toml", "wally.lock", "default.project.json", "LICENSE", "README.md"]
[dependencies]
-topbarplus = "1foreverhd/topbarplus@3.4.0"
+react = "haedrix/react@17.3.8"
+react-devtools = "haedrix/react-devtools@17.3.8"
+react-globals = "haedrix/react-globals@17.3.8"
+react-roblox = "haedrix/react-roblox@17.3.8"
+topbarplus = "ryanlua/topbarplus@1.0.1"