update rules

This commit is contained in:
Joe 2026-02-02 14:05:11 -06:00
parent 4c390fdd37
commit d5522b2fef
8549 changed files with 11783 additions and 1888 deletions

View file

@ -0,0 +1,459 @@
---
title: Bundles package
description: Learn about the bundles feature package.
---
The **Bundles** feature package offers out-of-the-box functionality to sell collections of items to players at a discount. You can choose whether to allow players to purchase bundles using a custom in-experience currency or Robux, which bundle type you want to use, what set of items you want to sell, and how you want to prompt players during their gameplay.
Using the package's customization options, you can tailor your bundles to meet the design and monetization goals of your experiences, such as:
- Targetting a low [conversion rate](../../production/game-design/analytics-essentials.md#monetization-metrics) metric by offering discounted [starter packs](../../production/game-design/starter-pack-design.md) that provide value to new players and encourage early spend.
- Increasing [spend depth](../../production/game-design/analytics-essentials.md#monetization-metrics) by bundling items at various price points to appeal to a range of players.
- Monetizing live operations (LiveOps) [events](../../production/game-design/liveops-essentials.md#content-cadence) by offering limited-time bundles of exclusive items.
<img src="../../assets/resources/feature-packages/Bundles/Bundles-Intro.png" width="100%" />
<Alert severity="info">
For information on how to strategically provide purchase opportunities to meet player needs at key stages of your experience, see [Contextual Purchases](../../production/game-design/contextual-purchases.md). For industry best practices on how implement starter pack bundles, see [Starter Pack Design](../../production/game-design/starter-pack-design.md).
</Alert>
## Get package
The **Creator Store** is a tab of the **Toolbox** that you can use to find all assets that are made by Roblox and the Roblox community for use within your projects, including model, image, mesh, audio, plugin, video, and font assets. You can use the Creator Store to add one or more assets directly into an open experience, including feature packages!
Every feature package requires the **Core** feature package to function properly. Once the **Core** and **Bundles** feature package assets are within your inventory, you can reuse them in any project on the platform.
To get the packages from your inventory into your experience:
1. Add the **Core** and **Bundles** feature package to your inventory within Studio by clicking the **Add to Inventory** link in the following set of components.
<Grid container spacing={2} style={{marginBottom: 48}}>
<Grid item container xs={12} sm={6} direction='row'>
<Grid item container direction='column'>
<BrowseSampleCard href='https://create.roblox.com/store/asset/94918533221001' description='The Core feature package offers shared data store logic for all feature packages.' title='Core Feature Package' assetId={94918533221001} />
</Grid>
</Grid>
<Grid item container xs={12} sm={6} direction='row'>
<Grid item container direction='column'>
<BrowseSampleCard href='https://create.roblox.com/store/asset/72975253516300' description='The Bundles Feature Package offers functionality to sell collections of items to players at a discount.' title='Bundles Feature Package' assetId={72975253516300} />
</Grid>
</Grid>
</Grid>
2. From Studio's **Window** menu or **Home** tab toolbar, open the [Toolbox](../../projects/assets/toolbox.md).
3. In the **Toolbox** window, click the **Inventory** tab. The **My Models** sort displays.
<img src="../../assets/studio/toolbox/Inventory-Tab.png" alt="Studio's Toolbox window with the Inventory tab highlighted." width="360" />
4. Click the **Feature Package Core** tile, then the **Bundle Feature Package** tile. Both package folders display in the **Explorer** window.
5. Drag the package folders into `Class.ReplicatedStorage`.
6. Allow data store calls to track player purchases with the packages.
1. Open Studio's **File** ⟩ **Experience Settings** window.
1. Navigate to the **Security** tab, then enable **Enable Studio Access to API Services**.
## Define currencies
<Alert severity="info">
If you do not plan on charging in-experience currencies for any bundles, you can skip this section and configure bundles through `devProductId`.
</Alert>
If your experience has its own currency system, you can register those with the **Core** feature package by defining them in `ReplicatedStorage.FeaturePackagesCore.Configs.Currencies`. There is a commented out example of a Gems currency already in this file; replace it with your own.
``` lua title="Currencies"
Gems = {
displayName = "Gems",
symbol = "💎",
icon = nil,
},
```
The `Currencies` script tells the **Core** feature package some metadata about your currency:
- (required) `displayName` - The name of your currency. If you do not specify a symbol or icon, this name is used in purchase buttons (i.e. "100 Gems").
- (optional) `symbol` - If you have a text character to use as the icon for your currency, this is used instead of the `displayName` in purchase buttons (i.e. "💎100").
- (optional) `icon` - If you have an `AssetId` image icon for your currency, this is used instead of the `displayName` in purchase buttons (i.e. image will be placed to the left of the price "🖼️100")
<Alert severity="info">
If you specify both a symbol and an icon for your currency, the package uses the icon instead of the symbol.
</Alert>
Once your currency is set up, you need to manually specify the bundle's price, currency, and icon for the heads up display instead of that information being fetched from the bundle's associated developer product.
``` lua title="Bundles"
-- If you want to use a dev product, you must provide a unique devProductId, only used by one bundle.
-- We will fetch bundle price and icon from the developer product
pricing = {
priceType = CurrencyTypes.PriceType.Marketplace,
devProductId = 1795621566,
},
-- Otherwise, if you want to use in-experience currency instead of a dev product, you can use the following instead:
-- Price here is in the in-experience currency, not Robux
pricing = {
priceType = CurrencyTypes.PriceType.InExperience,
price = 79,
currencyId = "Gems",
icon = 18712203759,
},
```
You also need to reference the `BundlesExample` script to call `setInExperiencePurchaseHandler`.
``` lua title="BundlesExample"
local function awardInExperiencePurchase(
_player: Player,
_bundleId: Types.BundleId,
_currencyId: CurrencyTypes.CurrencyId,
_price: number
)
-- Check if the player has enough currency to purchase the bundle
-- Update player data, give items, etc.
-- Deduct the currency from the player
task.wait(2)
return true
end
local function initializePurchaseHandlers()
local bundles = Bundles.getBundles()
for bundleId, bundle in bundles do
-- Bundle is not associated with a developer product if it does not have marketplace price type
if not bundle or bundle.pricing.priceType ~= "Marketplace" then
continue
end
Bundles.setPurchaseHandler(bundleId, awardMarketplacePurchase)
receiptHandlers[bundle.pricing.devProductId] = receiptHandler
end
-- If you have any in-experience currencies that you are using for bundles, set the handler here
for currencyId, _ in Currencies do
Bundles.setInExperiencePurchaseHandler(currencyId, awardInExperiencePurchase)
end
end
```
Specifically, you need to fill out `awardInExperiencePurchase`, which is called by a loop through `Currencies` inside of the example `initializePurchaseHandlers` (i.e. each currencyId is connected to the handler through `Bundles.setInExperiencePurchaseHandler(currencyId, awardInExperiencePurchase)`).
## Define bundles
All bundles offerable in your experience can be defined within `ReplicatedStorage.Bundles.Configs.Bundles`, with types exported from the `Types` script in the same folder.
If you're using a `devProductId`, you need to update the bundle's main `devProductId` to match the one in your experience. This is what will be prompted through `Class.MarketplaceService` to purchase the bundle itself. **It's strongly recommended to use a new developer product for the bundle to make it easier to track separate sales.**
If you want a bundle with multiple items, and if these are already represented by developer products in your experience, you don't need to explicitly set item price/assetId/name, which will be fetched via product info:
``` lua title="README"
{
itemType = ItemTypes.ItemType.DevProduct,
devProductId = <DEV_PRODUCT_ID>,
metadata = {
caption = {
text = "x1",
color = Color3.fromRGB(236, 201, 74),
} -- Caption is optional! You can also omit this field
}
},
```
Otherwise, you can manually configure those item details:
``` lua title="README"
{
itemType = ItemTypes.ItemType.Robux,
priceInRobux = 49,
icon = <IMAGE_ASSET_ID>,
metadata = {
caption = {
text = "x1",
color = Color3.fromRGB(236, 201, 74),
} -- Caption is optional! You can also leave omit this field
}
},
```
For example, your entire bundle will likely look like this:
``` lua title="README"
local starterBundle: Types.RelativeTimeBundle = {
bundleType = Types.BundleType.RelativeTime,
-- If you want to use a dev product, you must provide a unique devProductId, only used by one bundle.
-- We will fetch bundle price and icon from the developer product
pricing = {
priceType = CurrencyTypes.PriceType.Marketplace,
devProductId = <DEV_PRODUCT_ID>,
},
-- Otherwise, if you want to use in-experience currency instead of a dev product, you can use the following instead:
-- Price here is in the in-experience currency, not Robux
-- pricing = {
-- priceType = CurrencyTypes.PriceType.InExperience,
-- price = 79,
-- currencyId = <CURRENCY_ID>,
-- icon = <IMAGE_ASSET_ID>,
-- },
includedItems = {
[1] = {
-- The item itself is not sold via a developer product, so indicate how much it is worth in Robux and give an icon
-- The priceInRobux helps Bundles show relative value of the bundle price vs. the sum of its contents
itemType = ItemTypes.ItemType.Robux,
priceInRobux = 49,
icon = <IMAGE_ASSET_ID>,
-- Alternatively, if this has a dev product leave off price and icon above and just set the devProductId
-- The price and icon will be fetched from the developer product
-- devProductId = <ITEM_DEV_PRODUCT_ID>
-- There are more optional metadata fields that are UI-specific if needed
metadata = {
caption = {
text = "x1",
color = Color3.fromRGB(236, 201, 74),
},
},
},
[2] = {
itemType = ItemTypes.ItemType.Robux,
priceInRobux = 99,
icon = <IMAGE_ASSET_ID>,
metadata = {
caption = {
text = "x1",
color = Color3.fromRGB(236, 201, 74),
},
},
},
[3] = {
itemType = ItemTypes.ItemType.Robux,
priceInRobux = 149,
icon = <IMAGE_ASSET_ID>,
metadata = {
caption = {
text = "x1",
color = Color3.fromRGB(236, 201, 74),
},
},
},
},
singleUse = true, -- Once purchased or expired, no longer valid even if your experience tries to prompt (onPlayerAdded). You can make this false while testing in studio.
durationInSeconds = 900, -- 15 minutes
includesOfflineTime = false, -- Only count time elapsed in the experience
metadata = {
displayName = "STARTER BUNDLE",
description = "Save 75% and get a head start!",
},
}
```
## Integrate server logic
Take a look at `ReplicatedStorage.Bundles.Server.Examples.BundlesExample`, which shows how your server will interact with the **Bundles** feature package and the above methods on the `Class.ModuleScript`. The snippets below are from that script.
You mainly need to hook up four things once dragging the **Bundles** feature package into your experience:
1. Connect purchase handlers through `Bundles.setPurchaseHandler` to specify the functions to call to award items when a purchase is being processed.
``` lua title="BundlesExample"
local function awardMarketplacePurchase(_player: Player, _bundleId: Types.BundleId, _receiptInfo: { [string]: any })
-- Update player data, give items, etc.
-- ... AND record receiptInfo.PurchaseId so we can check if user already has this bundle
task.wait(2)
return Enum.ProductPurchaseDecision.PurchaseGranted
end
local function awardInExperiencePurchase(
_player: Player,
_bundleId: Types.BundleId,
_currencyId: CurrencyTypes.CurrencyId,
_price: number
)
-- Check if the player has enough currency to purchase the bundle
-- Update player data, give items, etc.
-- Deduct the currency from the player
task.wait(2)
return true
end
local function initializePurchaseHandlers()
local bundles = Bundles.getBundles()
for bundleId, bundle in bundles do
-- Bundle is not associated with a developer product if it does not have marketplace price type
if not bundle or bundle.pricing.priceType ~= "Marketplace" then
continue
end
Bundles.setPurchaseHandler(bundleId, awardMarketplacePurchase)
receiptHandlers[bundle.pricing.devProductId] = receiptHandler
end
-- If you have any in-experience currencies that you are using for bundles, set the handler here
for currencyId, _ in Currencies do
Bundles.setInExperiencePurchaseHandler(currencyId, awardInExperiencePurchase)
end
end
```
1. Connect your logic for `Class.MarketplaceService.ProcessReceipt`, but this might be done elsewhere if your experience already has developer products for sale. Essentially, when a developer product receipt is being processed, they will now call `Bundles.getBundleByDevProduct` to check if the product belongs to a bundle. If it does, the script then calls `Bundles.processReceipt`.
``` lua title="BundlesExample"
-- Process receipt from marketplace to determine if player needs to be charged or not
local function processReceipt(receiptInfo): Enum.ProductPurchaseDecision
local userId, productId = receiptInfo.PlayerId, receiptInfo.ProductId
local player = Players:GetPlayerByUserId(userId)
if not player then
return Enum.ProductPurchaseDecision.NotProcessedYet
end
local handler = receiptHandlers[productId] -- Get the handler for the product
local success, result = pcall(handler, receiptInfo, player) -- Call the handler to check if purchase logic is successful
if not success or not result then
warn("Failed to process receipt:", receiptInfo, result)
return Enum.ProductPurchaseDecision.NotProcessedYet
end
return Enum.ProductPurchaseDecision.PurchaseGranted
end
local function receiptHandler(receiptInfo: { [string]: any }, player: Player)
local bundleId, _bundle = Bundles.getBundleByProductId(receiptInfo.ProductId)
if bundleId then
-- This purchase belongs to a bundle, let Bundles handle it
local purchaseDecision = Bundles.processReceiptAsync(player, bundleId, receiptInfo)
return purchaseDecision == Enum.ProductPurchaseDecision.PurchaseGranted
end
-- This purchase does not belong to a bundle,
-- ... Handle all your existing logic here if you have any
return false
end
```
1. Connect `Players.PlayerAdded:Connect(Bundles.OnPlayerAdded)` so that the **Bundles** feature package re-prompts any active bundles that have not yet expired for a player.
``` lua title="README"
local function onPlayerAdded(player: Player)
-- Tell Bundles when player joins so it can reload their data
Bundles.onPlayerAdded(player)
-- If you had some starter bundle that you wanted to offer to all new users, you could prompt that here
-- ... Bundles will handle if player already has purchased it or if it's expired since it's not repeatable
-- Bundles.promptIfValidAsync(player, "StarterBundle")
-- Calling this here just for example, you can call this whenever or wherever you want
onPromptBundleXYZEvent(player)
end
```
1. Prompt bundles. While this depends on gameplay, the example prompt players with a StarterBundle `onPlayerAdded`.
- The **Bundles** feature package logic ensures each player doesn't get a repeat offer if they have already purchased the bundle, or if they let the offer already expire (based on bundle config).
- Whenever you want to prompt a bundle to a player, call `Bundles.promptIfValidAsync(player, bundleId)`.
``` lua title="README"
local function onPromptBundleXYZEvent(player: Player)
-- Connect whatever experience event you want to use to determine when a player gets prompted the bundle
-- ... This will be whenever you've met your elligibility criteria to prompt a player the bundle
-- ... For example, if you want to prompt a bundle when a player joins, or when a player levels up
task.spawn(Bundles.promptIfValidAsync, player, <Some_Bundle_Id>)
-- ... If creating multiple bundles, using task.spawn() to wrap the above function call will minimize discrepancies between countdowns
end
```
<Alert severity="warning">
When awarding purchases, ensure that you also check and record `receiptInfo.PurchaseId` in your data stores.
</Alert>
Consider the following best practice guidance on redundant recordings of ReceiptIds:
- While the **Bundles** feature package does record ReceiptIds to avoid processing the same receipt twice, you should also be recording ReceiptIds inside of your tables so that if the purchase flow fails after their purchase handler has already finished, you know on subsequent retry not to award items again.
- The **Bundles** feature package will not record the ReceiptId if the purchase fails at any step, so you should ensure that you are recording the ReceiptId in your tables before processing the receipt as part of your purchaseHandler.
- This redundancy helps ensure that all purchase logic has been appropriately handled and that your data store and the **Bundles** feature package's data store reach eventual consistency, with your data store being the source of truth.
## Configure constants
Constants for the **Core** feature package live in two spots:
- Shared constants live in `ReplicatedStorage.FeaturePackagesCore.Configs.SharedConstants`.
- Package-specific constants, in this case the **Bundles** feature package, live in `ReplicatedStorage.Bundles.Configs.Constants`.
The main things you might want to adjust to meet the design requirements of your experience:
- Sound asset IDs
- Purchase effect duration and particle colors
- Heads up display collapsibility
Additionally, you can find strings for translation broken out into one location: `ReplicatedStorage.FeaturePackagesCore.Configs.TranslationStrings`.
## Customize UI components
By modifying the package objects, such as colors, font, and transparency, you can adjust the visual presentation of your bundle prompts. However, keep in mind that if you move any of the objects around hierarchically, the code will not be able to find them, and you'll need to make adjustments to your code.
A prompt is made up of two high-level components:
- `PromptItem` – The individual component repeated out for each item within a bundle (item image, caption, name, price).
- `Prompt` – The prompt window itself.
The heads up display is also made up of two components:
- `HudItem` – An individual component that represents each menu option in the heads up display.
- `Hud` – To be filled with programmatically with `HudItems`.
If you want to have greater control over the heads up display, instead of just using the existing HUD UI within `ReplicatedStorage.Bundles.Objects.BundlesGui`, you can move things around to meet your own design requirements. Just be sure to update client script behavior in the `ReplicatedStorage.Bundles.Client.UIController` script.
## API reference
### Types
#### RelativeTime
Once the `RelativeTime` bundle is offered to a player, it remains available until the time duration runs out. This type displays on the player's heads up display, and automatically prompts on future sessions until the bundle expires or the player purchases it.
A common example of this bundle type is a single-use starter pack offer that displays to all new players for 24 hours. For industry best practices on how implement starter pack bundles, see [Starter Pack Design](../../production/game-design/starter-pack-design.md).
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>`includeOfflineTime`</td>
<td>`bool`</td>
<td>**(Optional)** If not set, only time spent in experience will count towards the remaining offer duration.</td>
</tr>
<tr>
<td>`singleUse`</td>
<td>`bool`</td>
<td>**(Optional)** If not set, the purchase can be reactivated after it's purchased or expired.<br></br><br></br>If set, once purchased or expired the first time, it will not be promptable ever again, even if you call `Bundles.promptIfValidAsync` with the bundleId.</td>
</tr>
</tbody>
</table>
#### FixedTime
Once the `FixedTime` bundle is offered to a player, it remains available until the end of the set coordinated universal time (UTC). This type displays on the player's heads up display, and automatically prompts on future sessions until the bundle expires or the player purchases it.
A common example of this bundle type is a holiday offer that's only available for a given month.
#### OneTime
A `OneTime` bundle is only available in the moment that it's offered to a player. It does not display on the player's heads up display, and once a player closes the prompt, it cannot be reopened until it's prompted by the server again.
A common example of this bundle type is an offer to purchase more in-experience currency the moment a player runs out.

View file

@ -0,0 +1,83 @@
---
title: Changelog
description: Learn about updates to all feature packages.
---
## Core
<h3 style={{marginBottom: 24, marginTop: 48}}>Version 4 -> Version 5</h3>
- **ModalManager**
- **[BREAKING CHANGE]** HUD buttons for all packages are now stored in a centralized location, in a `ScreenGui` under `FeaturePackagesCore`. This `ScreenGui` can be fetched with `ModalManager.getHudGui()`. It contains two frames, which are used to store HUD buttons in different locations on the screen. All packages need to be updated to use the new centralized HUD button locations.
- Added `ModalManager.toggleOpen` method to toggle the visibility of a given modal.
- **UITimer, SharedConstants**
- **[BREAKING CHANGE]** Renamed `CircularIndicator` to `RoundProgressBar` to be more consistent with the name of the linear `ProgressBar`. This includes the CollectionServiceTag and instances of the same name.
- **playPurchaseEffect**
- Fixed animated ImageLabels not getting cleaned up after the collection animation completes
- Fixed animation loop exiting early after the first item when overrideTransparency is true
- Animated ImageLabels now always have `ImageTransparency` of `0` to allow for better visibility when animating icons that are not opaque
- Reduced `Hover` animation duration from `0.5` to `0.3` seconds for a slightly snappier feel
- Adjusted `UIHover` animations to clarify logic and avoid resizing TextButtons and TextLabels, opting to adjust the `TextTransparency` and `BackgroundTransparency` instead. Other instance types still get resized.
- **UITween**
- Made `playTween` function public as `UITween.play` to allow for more custom tweening of multiple properties
- **UITimer, Attributes**
- Time is now based on `Workspace:GetServerTimeNow()` instead of `DateTime.now()` to allow for better synchronization between the client and server timers
- Fixed `FeaturePackagesTimerExpired` attribute not getting set when the timer expires
- **TranslationStrings**
- Added a couple strings used in the new Engagement Rewards package
## Bundles
<h3 style={{marginBottom: 24, marginTop: 48}}>Version 8 -> Version 9</h3>
- **UIController**
- **[BREAKING CHANGE]** Refactored HUD button creation to go through the new centralized `FeaturePackagesCore.ModalManager` location
- **[BREAKING CHANGE]** Updated the styling of the HUD buttons to achieve a more consistent look across all packages
- **[BREAKING CHANGE]** Major refactor of logic controlling the HUD button collapsible behavior to be more intuitive with smoother animations
## Missions
<h3 style={{marginBottom: 24, marginTop: 48}}>Version 9 -> Version 10</h3>
- **UIController**
- **[BREAKING CHANGE]** Refactored HUD button creation to go through the new centralized `FeaturePackagesCore.ModalManager` location
- **[BREAKING CHANGE]** Updated the styling of the HUD buttons to achieve a more consistent look across all packages
- **MissionsUI**
- Added a HUD button getter `getMissionsHudButton` to decouple the HUD button hierarchy from the Season Passes package
- **Server.Missions**
- Fixed a potential infinite loop when initializing data for a new player
- **MissionsExample**
- Fixed walking detection not working very well when the player walks at a consistent speed
- Removed jump detection to simplify the example, since listening for jumps on the server is inconsistent
- **Configs.Missions**
- Fixed a typo `startImmeadiately` -> `startImmediately`. The package still checks for the typo'd value, but new work should use the corrected name. Also fixed various typos in internal code across various scripts.
- Renamed example missions requirement from `Walks` to `Steps` for better grammar
- Updated example missions to use `Walking` instead of `Jumping` for all metrics
- Updated example numeric values to allow for slightly more time to test in studio
- Updated example mission rewards to add XP instead of coins to better integrate the Season Passes example
- UI button instances:
- Added padding, corner radius, UIHover tag, and adjusted text positioning to slightly improve styling
## Season Passes
<h3 style={{marginBottom: 24, marginTop: 48}}>Version 5 -> Version 6</h3>
- **UIController**
- **[BREAKING CHANGE]** Updated the reference to Missions HUD button to use the new `MissionsUI.getMissionsHudButton` method
- **Configs.Season**
- Updated the example season `startUtc` and `endUtc` to be relative to the current time in order to allow testing in studio to work regardless of when the package is being tested. For most purposes, you would still want to use an absolute time in your own season configuration.
- **UI button instances**
- Added padding, corner radius, and UIHover tag to match the slightly improved styling of the Missions panel

View file

@ -0,0 +1,168 @@
---
title: Engagement rewards
description: Learn about the engagement rewards feature package.
---
The **Engagement Rewards** feature package is a customizable framework for offering in-game rewards for certain player activities, including daily login streaks and play session time. In addition to client and server logic, the package includes a default UI for viewing reward progress, reward status, and claiming rewards.
<img src="../../assets/resources/feature-packages/engagement/rewards.jpg" width="730" />
## Get the package
1. Add the **Core** and **Engagement Rewards** packages to your inventory within Studio by clicking the **Add to Inventory** links here:
<Grid container spacing={2} style={{marginBottom: 48}}>
<Grid item container xs={12} sm={6} direction='row'>
<Grid item container direction='column'>
<BrowseSampleCard href='https://create.roblox.com/store/asset/94918533221001' description='The Core feature package offers shared data store logic for all feature packages.' title='Core' assetId={94918533221001} />
</Grid>
</Grid>
<Grid item container xs={12} sm={6} direction='row'>
<Grid item container direction='column'>
<BrowseSampleCard href='https://create.roblox.com/store/asset/80053655757903' description='The Engagement Rewards feature package helps you offer rewards for daily logins, login streaks, and time played.' title='Engagement Rewards' assetId={80053655757903} />
</Grid>
</Grid>
</Grid>
2. From Studio's **Window** menu or **Home** tab toolbar, open the [Toolbox](../../projects/assets/toolbox.md).
3. In the **Toolbox** window, click the **Inventory** tab. The **My Models** sort displays.
<img src="../../assets/studio/toolbox/Inventory-Tab.png" alt="Studio's Toolbox window with the Inventory tab highlighted." width="360" />
4. Click the **Feature Package Core** tile, then the **Engagement Rewards Feature Package** tile. Both package folders display in the **Explorer** window.
5. Drag the folders into `Class.ReplicatedStorage`.
## Initialize the package
Moving the packages to `Class.ReplicatedStorage` and testing your experience runs the `EngagementRewardsExample` script in `ReplicatedStorage.EngagementRewards.Server.Examples`.
This script shows how to initialize the package for use in your experience, which involves requiring several module scripts and defining a `rewardClaimedHandlerFunction()` function that ultimately gives the reward(s) to the player.
This function must return a boolean. In the example script, you can see that it doesn't give any rewards to the player; it just prints the player, reward, and quantity being claimed.
```lua title="EngagementRewardsExample"
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local EngagementRewardsConfig = require(ReplicatedStorage.EngagementRewards.Configs.EngagementRewardsConfig)
local EngagementRewards = require(ReplicatedStorage.EngagementRewards.Server.EngagementRewards)
local EngagementRewardsUtils = require(ReplicatedStorage.EngagementRewards.Utils.EngagementRewardsUtils)
local Types = require(ReplicatedStorage.EngagementRewards.Configs.Types)
-- Replace this handler function with your own reward claimed handler function
-- This function should handle the reward claimed event for the rewardId passed in
local function rewardClaimedHandlerFunction(player: Player, rewardId: RewardId, quantity: number): (boolean, string?)
print(`Reward {rewardId} claimed by {player} with quantity {quantity}`)
return true
end
-- more
```
You can either modify this script directly or move it to `Class.ServerScriptService` if that's your preferred location for server code. As-is, the script is only useful for testing purposes.
Giving the reward to the player varies by experience. In some experiences, you might confer an experience boost or just increment the player's gold count. In other experiences, you might have a custom inventory system, whereas others might place an item into player backpacks. In all cases, however, you must replace `rewardClaimedHandlerFunction()` with your own function.
## Add rewards and criteria
Most rewards customization occurs within `ReplicatedStorage.EngagementRewards.Configs.EngagementRewardsConfig`. This module script defines the reward and the requirements for unlocking it.
You can see the full type declarations (or add new ones) in `ReplicatedStorage.EngagementRewards.Configs.Types`, but you probably only need to work with `EngagementRewardsConfig`. The feature package includes two reward types: `Time` and `Daily`.
```lua
local engagementRewardsConfig: Types.EngagementRewardsConfig = {
[Types.RewardType.Time] = {
tabDisplayName = "Time",
tabOrder = 1,
description = "Keep playing to unlock rewards!",
rewards = {
MinutesPlayed1 = {
icon = 116913478160966,
displayName = "Ice Lance",
requiredSecondsInGame = 1 * 5,
},
-- more
[Types.RewardType.Daily] = {
tabDisplayName = "Daily",
tabOrder = 2,
description = "Play daily to unlock rewards!",
rewards = {
DailyStreak1 = {
icon = 116913478160966,
displayName = "Ice Lance",
requiredDaysVisitedStreak = 1,
effect = Types.RewardEffect.Valuable,
},
-- more
```
Both reward types require an icon, which is a Roblox asset ID for an image (**not** a decal). You likely also want a display name for use in the UI. Specify `quantity` for values other than 1. You can also optionally designate a reward as valuable (`effect = Types.RewardEffect.Valuable`) to give it a different background frame in the UI.
Variable | Type | Description | Default | Required
:--- | :--- | :--- | :--- | :---
icon | Number | A Roblox asset ID for the UI icon. | N/A | Yes
displayName | String | A name for the rewards for use in the UI. | The `RewardId` | No
quantity | Number | The number of items to reward. | 1 | No
effect | RewardEffect | The visual effect to use in the UI. See [Customize the UI](#customize-the-ui). | `Types.RewardEffect.Default` | No
### Time rewards
`Time` rewards unlock after a certain number of seconds in-game. Customize these numbers to meet your needs. You might use small numbers for easy testing, and in the published experience, confer a reward after 10 minutes, another after 30 minutes, another after an hour, and so on.
<Alert severity="info">
Time tracking starts at 0 for each session, so players lose all progress towards `Time` rewards when they leave the experience.
</Alert>
Multiplication makes numbers in seconds easier for humans to work with, so for two hours, you might prefer to specify `2 * 60 * 60` rather than `7200`.
Variable | Type | Description | Default | Required
:--- | :--- | :--- | :--- | :---
requiredSecondsInGame | Number | The number of seconds (a positive integer) the player must spend in-game to earn the reward. | N/A | Yes
### Daily rewards
`Daily` rewards unlock after a certain number of consecutive daily logins. For example, the first day that a player logs in, you might give them three potions or a cinnamon roll. Subsequent days, you might reward more valuable consumables, followed by a durable item, like a new fishing rod, after seven straight days.
Variable | Type | Description | Default | Required
:--- | :--- | :--- | :--- | :---
requiredDaysVisitedStreak | Number | The number of consecutive days the player must connect to the experience to earn the reward. | N/A | Yes
Two additional daily reward configuration options are available in the `DailyRewardTabConfig`.
```lua
[Types.RewardType.Daily] = {
tabDisplayName = "Daily",
tabOrder = 2,
description = "Play daily to unlock rewards!",
isHiddenOnJoin = true,
isAlignedToStreakResetTime = true,
rewards = {
DailyStreak1 = {
icon = 116913478160966,
displayName = "Ice Lance",
requiredDaysVisitedStreak = 1,
},
-- more
```
Variable | Type | Description | Default | Required
:--- | :--- | :--- | :--- | :---
isHiddenOnJoin | Boolean | When true, rewards don't pop up automatically when joining the experience. When false, rewards automatically pop up when joining the game if the player has a new daily reward to claim. | False | No
isAlignedToStreakResetTime | Boolean | When true, rewards for day 2 can be claimed 24 hours after the initial day 1 claimable time. When false, day 2 rewards can be claimed at the first midnight after the day 1 claimable time. For example, if a player logs in at 11:00 PM (23:00) and claims their day 1 reward, true means they can't claim the day 2 reward until 11:00 PM the next day. False means they can claim the day 2 reward at 12:00 AM (00:00), one hour later. Rewards for day 3 and beyond are always available 24 hours after the previous day's claimable time. | False | No
## Customize the UI
In the previous section, you might have noticed the `tabDisplayName`, `tabOrder`, and `description` fields, which allow for basic customization of the user interface. You can also find some UI strings in `ReplicatedStorage.FeaturePackagesCore.Configs.TranslationStrings`.
<img src="../../assets/resources/feature-packages/engagement/rewards-annotated.png" width="730" />
- To specify a new reward background using the `effect` variable, add a frame to `ReplicatedStorage.EngagementRewards.Objects.RewardItemFrames`. Then add the name of the frame to the `Types.RewardEffect` table in `ReplicatedStorage.EngagementRewards.Configs.Types`.
- For more comprehensive customization, modify the objects in `ReplicatedStorage.EngagementRewards.Objects`. For example, you might modify the `Class.Frame.BackgroundColor3|BackgroundColor3` of the `RewardsHudButton.Background` frame or the `Class.UIGradient.Color|Color` of `FooterContentFrame.ClaimableUIGradient`.
<Alert severity="info">
Be careful if you modify `ReplicatedStorage.FeaturePackagesCore.Objects`; changes to **Core** affect all feature packages.
</Alert>
- The entry point for the user interface is the `ReplicatedStorage.EngagementRewards.Client.UIController` script, which gets the necessary objects and initializes the UI. If you add or rename top-level objects (as opposed to only adding children or modifying properties), you likely need to update the code in this folder to handle them.

View file

@ -0,0 +1,297 @@
---
title: Feature packages
description: Feature Packages let you drag and drop fully functional, customizable features directly into your experiences.
---
Feature packages are fully functional, customizable features that you can drag and drop directly into your experience. Each feature package comes with:
- Functional back-end code for minimal engineering work
- Default UI components that you can customize to fit your unique aesthetics
- Built-in game design and UI/UX best practices
- Funnel analytics and insights
You can use feature packages to add new functionality and gameplay features to your experiences, improve your experience's key metrics, and contribute to sustainable [live operations](../../production/game-design/liveops-essentials.md) (LiveOps).
<br /> <br />
<Card>
<CardContent>
<h2 style={{marginBottom: 12}}>Bundles</h2>
<Typography variant="body2" color="textSecondary" component="p">
The **Bundles** feature package offers out-of-the-box functionality to sell collections of items to players at a discount.
</Typography>
<>
<Grid
alignItems="stretch"
container
direction="row">
<Grid item Large={6} XSmall={12} direction="column">
<div class="container"
style={{position: "relative", paddingBottom: "56.25%", height: 0}}>
<img src="../../assets/resources/feature-packages/Bundles/Bundles-Intro.png" width="100%" allowfullscreen style={{position: "absolute", top: 0, left: 0, width: "90%" }} />
</div>
</Grid>
<Grid item Large={6} XSmall={12} direction="column">
<>
<Stepper activeStep={3} orientation="vertical">
<Step style={{marginTop: -36}}>
<StepLabel optional="Register your own in-experience currency for players to purchase items.">
<h5 style={{marginTop: 36}}>Define currencies</h5>
</StepLabel>
</Step>
<Step style={{marginTop: -36}}>
<StepLabel
optional="Select the items you want to bundle at a discount.">
<h5 style={{marginTop: 36}}>Choose items</h5>
</StepLabel>
</Step>
<Step style={{marginTop: -36}}>
<StepLabel optional="Create bundle prompts that anticipate player needs at key points of your experience." >
<h5 style={{marginTop: 36}}>Customize prompts</h5>
</StepLabel>
</Step>
</Stepper>
</>
</Grid>
</Grid>
</>
<a href="../feature-packages/bundles.md">
<Button
variant="contained"
size="large">
Start creating
</Button>
</a>
</CardContent>
</Card>
<br /> <br />
<Card>
<CardContent>
<h2 style={{marginBottom: 12}}>Missions</h2>
<Typography variant="body2" color="textSecondary" component="p">
The **Missions** feature package offers out-of-the-box functionality to create missions that players can complete to achieve rewards and progress in your experience.
</Typography>
<>
<Grid
alignItems="stretch"
container
direction="row">
<Grid item Large={6} XSmall={12} direction="column">
<div class="container"
style={{position: "relative", paddingBottom: "56.25%", height: 0}}>
<img src="../../assets/resources/feature-packages/Missions/Missions-Intro.png" allowfullscreen style={{position: "absolute", top: 0, left: 0, width: "90%"}} />
</div>
</Grid>
<Grid item Large={6} XSmall={12} direction="column">
<>
<Stepper activeStep={3} orientation="vertical">
<Step style={{marginTop: -36}}>
<StepLabel optional="Determine what players need to do to complete a mission.">
<h5 style={{marginTop: 36}}>Define tasks</h5>
</StepLabel>
</Step>
<Step style={{marginTop: -36}}>
<StepLabel
optional="Add logic that updates task progress as players work on set objectives.">
<h5 style={{marginTop: 36}}>Track progress</h5>
</StepLabel>
</Step>
<Step style={{marginTop: -36}}>
<StepLabel optional="Create unique UI that matches the aesthetics of your experiences." >
<h5 style={{marginTop: 36}}>Customize menus</h5>
</StepLabel>
</Step>
</Stepper>
</>
</Grid>
</Grid>
</>
<a href="../feature-packages/missions.md">
<Button
variant="contained"
size="large">
Start creating
</Button>
</a>
</CardContent>
</Card>
<br /> <br />
<Card>
<CardContent>
<h2 style={{marginBottom: 12}}>Season passes</h2>
<Typography variant="body2" color="textSecondary" component="p">
The **Season Passes** feature package offers out-of-the-box functionality to create quests that players can complete to earn rewards during a pre-defined period of time.
</Typography>
<>
<Grid
alignItems="stretch"
container
direction="row">
<Grid item Large={6} XSmall={12} direction="column">
<div class="container"
style={{position: "relative", paddingBottom: "56.25%", height: 0}}>
<img src="../../assets/resources/feature-packages/Season-Passes/Season-Passes-Intro.png" allowfullscreen style={{position: "absolute", top: 0, left: 0, width: "90%"}} />
</div>
</Grid>
<Grid item Large={6} XSmall={12} direction="column">
<>
<Stepper activeStep={3} orientation="vertical">
<Step style={{marginTop: -36}}>
<StepLabel optional="Determine what players need to do to complete a quest.">
<h5 style={{marginTop: 36}}>Define quests</h5>
</StepLabel>
</Step>
<Step style={{marginTop: -36}}>
<StepLabel
optional="Add logic that updates quest progress as players work on set objectives.">
<h5 style={{marginTop: 36}}>Track progress</h5>
</StepLabel>
</Step>
<Step style={{marginTop: -36}}>
<StepLabel optional="Create experience-specific rewards to encourage players to progress through a season." >
<h5 style={{marginTop: 36}}>Customize rewards</h5>
</StepLabel>
</Step>
</Stepper>
</>
</Grid>
</Grid>
</>
<a href="../feature-packages/season-passes.md">
<Button
variant="contained"
size="large">
Start creating
</Button>
</a>
</CardContent>
</Card>
<br /> <br />
<Card>
<CardContent>
<h2 style={{marginBottom: 12}}>Engagement rewards</h2>
<Typography variant="body2" color="textSecondary" component="p">
The **Engagement Rewards** feature package is a customizable framework for offering in-game rewards for certain player activities, such as daily logins, login streaks, and time played.
</Typography>
<>
<Grid
alignItems="stretch"
container
direction="row">
<Grid item Large={6} XSmall={12} direction="column">
<div class="container"
style={{position: "relative", paddingBottom: "56.25%", height: 0}}>
<img src="../../assets/resources/feature-packages/engagement/rewards.jpg" allowfullscreen style={{position: "absolute", top: 0, left: 0, width: "90%"}} />
</div>
</Grid>
<Grid item Large={6} XSmall={12} direction="column">
<>
<Stepper activeStep={3} orientation="vertical">
<Step style={{marginTop: -36}}>
<StepLabel optional="Set up the core function to give players rewards.">
<h5 style={{marginTop: 36}}>Initialize the package</h5>
</StepLabel>
</Step>
<Step style={{marginTop: -36}}>
<StepLabel
optional="Create rewards, provide thumbnails, and set reward criteria.">
<h5 style={{marginTop: 36}}>Add rewards</h5>
</StepLabel>
</Step>
<Step style={{marginTop: -36}}>
<StepLabel optional="Match the user interface to your experience." >
<h5 style={{marginTop: 36}}>Customize the UI</h5>
</StepLabel>
</Step>
</Stepper>
</>
</Grid>
</Grid>
</>
<a href="../feature-packages/engagement-rewards.md">
<Button
variant="contained"
size="large">
Start creating
</Button>
</a>
</CardContent>
</Card>

View file

@ -0,0 +1,387 @@
---
title: Missions package
description: Learn about the missions feature package.
---
<Alert severity="warning">
This feature package is in beta.
</Alert>
The **Missions** feature package offers out-of-the-box functionality to create missions that players can complete to achieve rewards and progress in your experience. All missions must have an ID, category, and a list of tasks that players must complete to finish the mission. However, the tasks list can be empty, allowing the mission's rewards to be claimed immediately.
Using the package's customization options, you can personalize all missions to meet your unique gameplay requirements, such as:
- Creating missions that [onboard](../../production/game-design/onboarding.md) new players into your experience and improve [D1 retention](../../production/game-design/analytics-essentials.md#retention-metrics).
- Targetting [D7 and D30 retention](../../production/game-design/analytics-essentials.md#retention-metrics) by adding mid- to long-term goals and a sense of progression.
- Increasing [engagement](../../production/game-design/analytics-essentials.md#engagement-metrics) by surfacing the various systems in your game and rewarding players for interacting with them.
<img src="../../assets/resources/feature-packages/Missions/Missions-Intro.png" width="100%" />
<Alert severity="info">
For information on how to provide players with fresh goals and incentives so that they're motivated to play longer and increase their engagement, see [Introduction to Quest Design](../../production/game-design/introduction-to-quest-design.md).
</Alert>
## Get package
The **Creator Store** is a tab of the **Toolbox** that you can use to find all assets that are made by Roblox and the Roblox community for use within your projects, including model, image, mesh, audio, plugin, video, and font assets. You can use the Creator Store to add one or more assets directly into an open experience, including feature packages!
Every feature package requires the **Core** feature package to function properly. Once the **Core** and **Missions** feature package assets are within your inventory, you can reuse them in any project on the platform.
To get the packages from your inventory into your experience:
1. Add the **Core** and **Missions** to your inventory within Studio by clicking the **Add to Inventory** link in the following set of components.
<Grid container spacing={2} style={{marginBottom: 48}}>
<Grid item container xs={12} sm={6} direction='row'>
<Grid item container direction='column'>
<BrowseSampleCard href='https://create.roblox.com/store/asset/94918533221001' description='The Core feature package offers shared data store logic for all feature packages.' title='Core Feature Package' assetId={94918533221001} />
</Grid>
</Grid>
<Grid item container xs={12} sm={6} direction='row'>
<Grid item container direction='column'>
<BrowseSampleCard href='https://create.roblox.com/store/asset/89760436366160' description='The Missions Feature Package offers functionality to create missions that players can complete to achieve rewards and progress in your experience.' title='Missions Feature Package' assetId={89760436366160} />
</Grid>
</Grid>
</Grid>
2. From Studio's **Window** menu or **Home** tab toolbar, open the [Toolbox](../../projects/assets/toolbox.md).
3. In the **Toolbox** window, click the **Inventory** tab. The **My Models** sort displays.
<img src="../../assets/studio/toolbox/Inventory-Tab.png" alt="Studio's Toolbox window with the Inventory tab highlighted." width="360" />
4. Click the **Feature Package Core** tile, then the **Missions Feature Package** tile. Both package folders display in the **Explorer** window.
5. Drag the package folders into `Class.ReplicatedStorage`.
## Define missions
Each completeable mission includes a set of tasks that must be finished to complete the mission, configuration options, and optional display metadata, all of which can be defined within `ReplicatedStorage.Missions.Configs.Missions`, with types exported from the `Types` script in the same folder.
### Required fields
The following fields are required for each mission.
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>`missionId`</td>
<td>`string`</td>
<td>The main Missions table key. All missions are identified by their own unique string.</td>
</tr>
<tr>
<td>`categoryId`</td>
<td>`string`</td>
<td>Missions must belong to a category, and are grouped by category in the UI.</td>
</tr>
<tr>
<td>`tasks`</td>
<td>`table`</td>
<td>A list of tasks that the player must finish to complete the mission.</td>
</tr>
</tbody>
</table>
### Unlock conditions
By default, missions are unlocked for players automatically and can be completed exactly once. However, you can use the following **optional** configuration options to alter this behavior.
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>`prerequisites`</td>
<td>`table`</td>
<td>A list of other missionIds that must be completed before the mission can be unlocked.</td>
</tr>
<tr>
<td>`manualOnly`</td>
<td>`bool`</td>
<td>Disables the automatic unlocking of the mission. Instead, a function must be called to unlock the mission. Any other unlock conditions must still be met as well.</td>
</tr>
<tr>
<td>`availableAfterUtc`</td>
<td>`bool`</td>
<td>The mission cannot be unlocked before the specified time in UTC.</td>
</tr>
<tr>
<td>`availableBeforeUtc`</td>
<td>`bool`</td>
<td>The mission cannot be unlocked after the specified time in UTC. If it is unlocked but not completed before this time, the mission will be failed.</td>
</tr>
<tr>
<td>`repeatable`</td>
<td>`bool`</td>
<td>The mission becomes unlocked again after it is completed.</td>
</tr>
<tr>
<td>`repeatLimit`</td>
<td>`number`</td>
<td>If the mission is repeatable, it cannot be repeated more than this many times.</td>
</tr>
<tr>
<td>`repeatCooldownSeconds`</td>
<td>`number`</td>
<td>If the mission is repeatable, there is a delay before it becomes unlocked.</td>
</tr>
<tr>
<td>`expireSeconds`</td>
<td>`number`</td>
<td>If the mission is unlocked and not completed within the specified number of seconds, then it will fail instead.</td>
</tr>
<tr>
<td>`expireCountOffline`</td>
<td>`bool`</td>
<td>If the mission has `expireSeconds` and `expireCountOffline` is set to true, time while the player is not actually in the experience will count toward mission expiration.</td>
</tr>
</tbody>
</table>
### Metadata
Missions have metadata that specifies how they will be displayed in the Missions user interface. You can use the following **optional** fields to customize its metadata.
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>`displayName`</td>
<td>`string`</td>
<td>A name to display for the mission in the user interface instead of the `missionId`.</td>
</tr>
<tr>
<td>`description`</td>
<td>`string`</td>
<td>A longer block of text that provides additional information or context about the mission.</td>
</tr>
<tr>
<td>`visibleAfterComplete`</td>
<td>`bool`</td>
<td>If set to true, the mission will show up in the list of missions even after it is completed but marked as completed.</td>
</tr>
<tr>
<td>`visibleAfterFailed`</td>
<td>`bool`</td>
<td>If set to true, the mission will show up in the list of missions even after it is failed but marked as failed.</td>
</tr>
<tr>
<td>`visibleBeforeUnlocked`</td>
<td>`bool`</td>
<td>If set to true, the mission will be visible but locked in the list of missions before being available for completion.</td>
</tr>
<tr>
<td>`invisibleWhileActive`</td>
<td>`bool`</td>
<td>If set to true, the mission will be invisible even while actively in progress.</td>
</tr>
<tr>
<td>`rewards`</td>
<td>`table`</td>
<td>A list of reward display information.<br></br><ul><li>`assetID` (`number`) - The image ID that displays in the missions UI under the rewards for a quest. This is required if a reward is present.</li><li>`displayName` (`string`) - The name that displays in the missions UI under the icon.</li></ul></td>
</tr>
</tbody>
</table>
## Define tasks
Each mission can have zero or more tasks. If a mission has zero tasks, it is claimable immediately after it's unlocked; if a mission has one or more tasks, once the tasks are complete, the player can collect any rewards associated with the mission. Each task has a `taskId`, which is the key associated with the task for a given mission.
Tasks come in two types:
- **Timed Tasks** - Lets you start and stop the task at different points in time. A certain amount of time must pass while the task's timer is running, then the task is complete.
- **Count Tasks** - Lets you add to or set the task's progress. When the progress reaches a set value, the task is complete.
Both task types share the following fields:
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>`taskType`</td>
<td>`string`</td>
<td>Specifies if the task type is count or timed.</td>
</tr>
<tr>
<td>`counter`</td>
<td>`object`</td>
<td>(Optional) The counter this task tracks. Counters are player-specific persistent storage for a number or a timer. Multiple tasks can track a single counter; for example, if more than one task tracks how many coins a player has collected, then they all can share the same “coins” counter. These tasks can independently track new coins collected, starting from zero, or continue counting from the counter value (all coins already collected).<br></br><ul><li>`counterId` (`string`) - The ID of the counter to be tracked. Use this ID to get or set the value of the counter.</li><li>`continueFromCounter` (Optional `bool`) - If set to true, the task progress will directly match the value of the counter rather than the amount it has increased.</li></ul></td>
</tr>
<tr>
<td>`metadata`</td>
<td>`object`</td>
<td>(Optional) Information about how the task displays in the UI.<br></br><ul><li>`displayName` (`string`) - Name of the task, used when shown in the UI.</li><li>`numericType` (Optional `string`) - how the progress number in the task should be displayed.<ul><li>Fraction, ex. 5/10 or 5k/50k</li><li>Percentage, ex. 50% or 10%</li><li>LongFraction, ex. 5/10 or 5000/50000</li><li>Boolean, ex. Incomplete</li></ul></li></ul></td>
</tr>
<tr>
<td>`callToAction`</td>
<td>`object`</td>
<td>(Optional) A button that triggers a callback function.<br></br><ul><li>`callback` (`function`) - Function triggered by the `callToAction` button.</li><li>`buttonText` (`string`) - The text that is displayed in the button UI.</li></ul></td>
</tr>
</tbody>
</table>
### Count tasks fields
Count tasks have a required value. When the task progress meets this amount, the task is complete.
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>`goalCount`</td>
<td>`number`</td>
<td>(Optional) The progress required to complete the task.</td>
</tr>
</tbody>
</table>
### Time tasks fields
Time tasks have a target amount of time spent, and are started and stopped. When the target amount of time is met, the task is complete.
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>`goalSeconds`</td>
<td>`number`</td>
<td>The number of seconds that must pass to complete the task.</td>
</tr>
<tr>
<td>`startImmediately`</td>
<td>`bool`</td>
<td>If the task should start counting time as soon as it is unlocked, rather than only after it is started.</td>
</tr>
<tr>
<td>`includesOfflineTime`</td>
<td>`bool`</td>
<td>If the task should include time spent while the player is not actively in the experience.</td>
</tr>
</tbody>
</table>
## Configure categories
Categories do not have to be explicitly defined to be used, as the category for a mission has default values that will be used. However, you can configure these values in `ReplicatedStorage.Missions.Configs.Categories` to add additional effects to the category. Categories are identified by unique `CategoryIds`, the same ones referenced in the missions config.
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>`repeatDelaySeconds`</td>
<td>`number`</td>
<td>(Optional) If set, all missions in the category are reset every time the specified amount of time passes, and may be unlocked, completed, and their rewards claimed again. This is separate from repeatable missions, which are repeatable within a given category repeat.</td>
</tr>
</tbody>
</table>
## Integrate server logic
Take a look at `ReplicatedStorage.Missions.Server.Examples.MissionsExample`, which shows how your server will interact with the **Missions** feature package.
You mainly need to hook up four things once dragging the **Missions** feature package into your experience:
1. Define missions in your missions config.
2. Add logic to your experience to update task progress or the counters the progress is tied to.
``` lua title="README"
-- Increases progress on a mission Jumping with a Jumps task
Missions.addProgressToTask(player, "Jumping", "Jumps", 1)
-- Starts the timer on a mission BattlingTime with a TimeInBattle task
Missions.startTimedTask(player, "BattlingTime", "TimeInBattle")
-- Stops the timer on a mission BattlingTime with a TimeInBattle task
Missions.stopTimedTask(player, "BattlingTime", "TimeInBattle")
-- Increases the progress on all tasks tied to the Jumps counter
CounterSystem.addCounter(player, "Jumps", 1)
-- Starts the timer on all timed tasks tied to the TimeInBattle timer counter
CounterSystem.startTimer(player, "TimeInBattle")
-- Stops the timer on all timed tasks tied to the TimeInBattle timer counter
CounterSystem.stopTimer(player, "TimeInBattle")
```
3. Set mission completion handlers, and optionally unlock or fail handlers. Use the completion handler to award the rewards from the mission in your experience.
``` lua title="README"
local function completeHandler(player: Player, missionId: Types.MissionId)
print(`{player} completed mission {missionId}`)
-- Award player their rewards.
end
Missions.setCompletionHandler(missionId, completeHandler)
```
4. Unlock missions that are not unlocked automatically. **Missions** feature package logic ensures that all mission requirements are met before the mission is complete, and its rewards are collectible.
``` lua title="README"
Missions.unlockMission(player, "Manual")
```
## Configure constants
Constants for the **Core** feature package live in two spots:
- Shared constants live in `ReplicatedStorage.FeaturePackagesCore.Configs.SharedConstants`.
- Package-specific constants, in this case the **Missions** feature package, live in `ReplicatedStorage.Missions.Configs.Constants`.
Additionally, you can find strings for translation broken out into one location: `ReplicatedStorage.FeaturePackagesCore.Configs.TranslationStrings`.
## Customize UI components
By modifying the package objects, such as colors, font, and transparency, you can adjust the visual presentation of your missions UI. For example, in `ReplicatedStorage.Missions.Configs.Constants`, you can enable `SingleTaskMode` to display a progress bar for a task directly on the mission itself for missions that only have a single task.
<Alert severity="warning">
If you move any of the objects around hierarchically, the code will not be able to find them, and you'll need to make adjustments in **MissionsUI**.
</Alert>
In addition, if your experience already has an existing UI that you would like to integrate with the **Missions** feature package, the client `Class.ModuleScript` `MissionsClient` contains all the functions necessary to get the information about a player's missions sent from the server.

View file

@ -0,0 +1,244 @@
---
title: Season passes package
description: Learn about the season passes feature package.
---
<Alert severity="warning">
This feature package is in beta and requires custom configuration for the unique needs of your experiences. Because of this, make sure to test the balance and functionality of your season pass before publishing to your audience.
</Alert>
The **Season Passes** feature package offers out-of-the-box functionality to create limited-time, quest-based progression systems in which players can complete quest objectives to earn rewards that are only available during a pre-defined period of time. Using the package's customization options, you can personalize all season passes to deliver new content to your audience, promote player retention, and generate revenue.
<img src="../../assets/resources/feature-packages/Season-Passes/Season-Passes-Intro.png" width="100%" />
<Alert severity="info">
For information on how to encourage players to complete tasks, gain pass experience points (XP), and climb through reward tiers, see [Season pass design](../../production/game-design/season-pass-design.md).
</Alert>
## Get package
The **Creator Store** is a tab of the **Toolbox** that you can use to find all assets that are made by Roblox and the Roblox community for use within your projects, including model, image, mesh, audio, plugin, video, and font assets. You can use the Creator Store to add one or more assets directly into an open experience, including feature packages!
Every feature package requires the **Core** feature package to function properly, and the **Season Passes** package requires the **Missions** feature package to define a season's missions. Once all three package assets are within your inventory, you can reuse them in any project on the platform.
To get the packages from your inventory into your experience:
1. Add the **Core**, **Missions**, and **Season Passes** packages to your inventory within Studio by clicking the **Add to Inventory** link in the following set of components.
<Grid container spacing={3} style={{marginBottom: 48}}>
<Grid item container xs={12} sm={4} direction='row'>
<Grid item container direction='column'>
<BrowseSampleCard href='https://create.roblox.com/store/asset/94918533221001' description='The Core Feature Package offers shared data store table logic for all feature packages.' title='Core Feature Package' assetId={94918533221001} />
</Grid>
</Grid>
<Grid item container xs={12} sm={4} direction='row'>
<Grid item container direction='column'>
<BrowseSampleCard href='https://create.roblox.com/store/asset/89760436366160' description='The Missions Feature Package offers functionality to create missions that players can complete to achieve rewards and progress in your experience.' title='Missions Feature Package' assetId={89760436366160} />
</Grid>
</Grid>
<Grid item container xs={12} sm={4} direction='row'>
<Grid item container direction='column'>
<BrowseSampleCard href='https://create.roblox.com/store/asset/89781974928804' description='The Season Passes Feature Package offers functionality to create a limited-time, quest-based progression system that players can complete to earn rewards.' title='Season Passes Feature Package' assetId={89781974928804} />
</Grid>
</Grid>
</Grid>
2. From Studio's **Window** menu or **Home** tab toolbar, open the [Toolbox](../../projects/assets/toolbox.md).
3. In the **Toolbox** window, click the **Inventory** tab. The **My Models** sort displays.
<img src="../../assets/studio/toolbox/Inventory-Tab.png" alt="Studio's Toolbox window with the Inventory tab highlighted." width="360" />
4. Click the **Feature Package Core** tile, **Missions Feature Package** tile, then the **Season Passes Feature Package** tile. All three package folders display in the **Explorer** window.
5. Drag the package folders into `Class.ReplicatedStorage`.
6. Allow data store calls to track player purchases with the packages.
1. Open Studio's **File** ⟩ **Experience Settings** window.
1. Navigate to the **Security** tab, then enable **Enable Studio Access to API Services**.
## Configure mission dependencies
For seasons to function properly, you must add logic so that the **Season Passes** feature package can track player progress and update its UI components. For simplicity, the example below uses the **Missions** feature package and calls `SeasonPasses.addProgressToSeason()` after players complete missions, but you can track player progress however you'd like. Whether you define missions or not, though, you must still add the **Missions** feature package to your experience for the **Season Passes** code to function.
Missions each have a set of tasks that must be finished to complete the mission, configuration options, and optional display metadata. For an in-depth guide on how to configure missions and their tasks, see [Define missions](missions.md#define-missions).
### Add XP from missions to seasons
This example uses the **Missions** feature package to define missions that players can complete to gain experience (XP) and progress through the season. To award players XP when they complete a defined mission, you must add logic to the `ReplicatedStorage.Missions.Server.Examples.MissionsExample` server script so that it can import season pass data and reward players according to their progress.
``` lua
local SeasonPasses = require(ReplicatedStorage.SeasonPasses.Server.SeasonPasses)
local function completeHandler(player: Player, missionId: Types.MissionId)
print(`{player} completed mission {missionId}`)
if missionId == "Walking" then
SeasonPasses.addProgressToSeason(player, 100)
end
if missionId == "Walking2" then
SeasonPasses.addProgressToSeason(player, 100)
end
if missionId == "Walking3" then
SeasonPasses.addProgressToSeason(player, 100)
end
if missionId == "Availability" then
SeasonPasses.addProgressToSeason(player, 100)
end
if missionId == "Manual" then
SeasonPasses.addProgressToSeason(player, 100)
end
if missionId == "Minutely" then
SeasonPasses.addProgressToSeason(player, 100)
end
end
```
### Add season tier reward icons
In addition to granting players XP when they complete missions, the **Season Passes** feature package uses the **Missions** feature package to define unique reward tiers within a season. If you want to add an on-screen icon for a tier reward that players can earn if they enough XP, add an asset ID in the `Missions` config for each season tier reward.
``` lua
Tier3Mission = {
categoryId = "General",
metadata = {
displayName = "Tier 3 Mission",
description = "Jump 10 times",
visibleAfterComplete = true,
visibleBeforeUnlocked = true,
rewards = {
{
icon = 16826775737,
displayName = "Item",
},
{
icon = 106912146245070,
displayName = "Season XP",
},
},
},
}
```
## Configure seasons
After you have set up every mission you want to include in a season, it's time to configure the season pass itself. In `ReplicatedStorage.SeasonPasses.Configs.Season`, there is an example of a season configuration for your reference, and the following code samples are from that file.
### Define passes
[Passes](../../production/monetization/passes.md) allow you to charge players a one-time Robux fee in order to access a season pass's missions and potential rewards. After you create a pass with a unique asset ID, you can set the details of the pass in the `premiumTrack` field of your season.
For example, the following code sample defines a pass with an example asset ID of `928192647` to represent a season pass, then it gives the season pass a track name of "Premium Track".
``` lua
premiumTrack = {
pricing = {
gamePassId = 928192647,
},
trackMetadata = {
trackName = "Premium Track",
},
},
```
<Alert severity="info">
To test the purchasing of the premium pass, as well as how the purchasing effects the season, you need to publish the experience to Roblox and test in the client. Studio does not mock pass purchasing.
</Alert>
### Configure tiers and rewards
Season tiers represent the milestones that players must meet in order to earn rewards throughout a season. To climb the tiers, players complete missions in order to earn XP, and when they earn enough XP, they graduate to the next tier and receive rewards for the tier they just completed.
Tiers have an `upperBoundXP` property that sets the threshold of XP necessary to claim the rewards for a tier. Each tier can offer two types of rewards:
- **Free** - A reward that players can earn without purchasing the season pass.
- **Premium** - A reward that players can only earn by purchasing the season pass.
To demonstrate, review the following code sample for a season with one tier that requires 100 XP to earn a free ice lance reward and a premium blast shield reward. The `icon` is the asset ID of the UI image to display for a reward, and the `displayName` displays a name for the reward. Note that the `displayName` displays in the UI, but the `rewardId` is hidden from the player and is mainly used for [data store](../../cloud-services/data-stores/index.md) purposes.
``` lua
tiers = {
[1] = {
upperBoundXP = 100,
freeReward = {
icon = 128181721602852,
displayName = "Ice Lance",
rewardId = "iceLance",
},
premiumReward = {
icon = 95882371958115,
displayName = "Blast Shield",
rewardId = "blastShield",
},
},
}
```
While this script is for illustration purposes on how to configure one tier at a time, most seasons include more than one tier. As you define multiple tiers and rewards for a season, make sure each `rewardId` is numeric and in increasing order so that your tiers display in the order that you define them in the tiers list.
### Define season details
Now that you have a season pass with tiers and rewards, it's time to define your season's details:
- `title` - The name of your season.
- `description` - The description of your season.
- `startUtc` - The beginning of the season in coordinated universal time.
- `endUtc` - The end of the season in coordinated universal time.
- `seasonId` - Lets you track the XP a player gains throughout the given season.
The package uses `startUtc` and `endUtc` to display the countdown of the season.
``` lua
startUtc = DateTime.fromUniversalTime(2024, 10, 01, 17, 0, 0),
endUtc = DateTime.fromUniversalTime(2025, 2, 28, 17, 0, 0),
title = "Season 1",
description = "Into Space City",
seasonId = "season1",
```
## Integrate server logic
Take a look at `ReplicatedStorage.SeasonPasses.Server.Examples.SeasonPassesExample`, which shows how your server will interact with the **Season Passes Feature Package**. The snippet below is from that script.
You mainly need to create a handler function for awarding claimed rewards to your players. For example, the following code sample creates a `rewardClaimedHandlerFunction()` function that tracks when players progress to the next tier, then rewards them either just the free reward, or both the free and premium rewards if they purchased the season pass.
For your purposes, replace the `Library.task.wait()` call with logic that allows players to get the rewards that they have claimed in the season. This function **must** return a boolean: true if the server granted the reward, false if it did not.
``` lua
local function rewardClaimedHandlerFunction(_player: Player, _rewardId: Types.RewardId): boolean
task.wait(2)
return true
end
local function initializeRewardClaimedHandlers()
for tierId, tier in pairs(Season.tiers) do
if tier then
if tier.freeReward then
SeasonPasses.setRewardClaimedHandler(tier.freeReward.rewardId, rewardClaimedHandlerFunction)
end
if tier.premiumReward then
SeasonPasses.setRewardClaimedHandler(tier.premiumReward.rewardId, rewardClaimedHandlerFunction)
end
end
end
end
```
## Configure constants
Constants for the **Core** feature package live in two spots:
- Shared constants live in `ReplicatedStorage.FeaturePackagesCore.Configs.SharedConstants`.
- Package-specific constants, in this case the **Season Passes** feature package, live in `ReplicatedStorage.SeasonPasses.Configs.Constants`.
The main things you might want to adjust to meet the design requirements of your experience:
- Sound asset IDs
- Purchase effect duration and particle colors
Additionally, you can find strings for translation broken out into one location: `ReplicatedStorage.FeaturePackagesCore.Configs.TranslationStrings`.
## Customize UI components
By modifying the package objects, such as colors, font, and transparency, you can adjust the visual presentation of your season pass prompts. However, keep in mind that if you move any of the objects around hierarchically, the code will not be able to find them, and you'll need to make adjustments to your code.
A season is made up of the following high-level components that are programatically added to the **Missions Feature Package** UI components:
- `SeasonBottomFrame` – The frame that includes information about each track, a scrolling frame of the rewards for tiers, and tier data.
- `SeasonDescriptionFrame` - The frame in the header that displays the season title, description and countdown.
- `TierRewardFrame` - The frame that displays a specific reward for a tier.
- `TierProgressFrame` - The frame that displays the progress the player has made toward the current tier they are working on.
- `PurchaseFrame` - The frame that prompts the player to purchase a premium pass.
- `SeasonProgressFrame` - The frame that displays the player's progress bar throughout the entire season.
- `RewardsButtonFrame` - The frame that adds a button that allows the season's information to be shown through the `SeasonBottomFrame`.