Overview
The stat_creator_device generates a single named scoreboard statistic that can be scoped to an individual Player, a Team, or the whole Match. That stat can drive Game End / Round End conditions, be shown on the HUD, or power an RPG-style progression: when its Value reaches Max Value, the device can bump a Level up by 1 and reset Value to 0.
Reach for this device when you want a number you control — coins collected, kills toward a custom objective, crafting XP, faction reputation — instead of one of Fortnite's built-in stats. From Verse you can:
- Read the current value/level with
GetValue,GetLevel,GetValueForMatch,GetLevelForMatch. - Write it with
SetValue,SetLevel,SetValueForMatch,SetLevelForMatch. - React to changes by subscribing to
ValueChangedEvent,LevelChangedEvent, andMaximumReachedEvent.
A critical detail: the Scope you choose in the device's Details panel decides which overload succeeds. The GetValue(Agent)/SetValue(Agent, ...) calls only succeed when Scope is Player; the (Team) overloads only succeed when Scope is Team; the ...ForMatch calls only succeed when Scope is Match. All of these are <decides>, so you call them inside if (...).
API Reference
stat_creator_device
A device that generates a scoreboard stat that can be used by the game to determine Game End and Round End conditions. The stat can apply to individual players, teams, or everyone in the match. It can also generate a Level that will increment by 1 each time Value reaches Max Value, resetting Value to 0 when Level increments.
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse).
stat_creator_device<public> := class<concrete><final>(creative_device_base):
Events (subscribe a handler to react):
| Event | Signature | Description |
|---|---|---|
ValueChangedEvent |
ValueChangedEvent<public>:listenable(tuple(?agent, int)) |
Signaled when the stat changes Value. Sends a tuple of the instigating agent (or false if none) and the new Value. Value increases to a maximum of Max Value. If the stat has not yet reached Max Level then Value will reset to 0 and L |
LevelChangedEvent |
LevelChangedEvent<public>:listenable(tuple(?agent, int)) |
Signaled when the stat changes Level. Sends a tuple of the instigating agent (or false if none) and the new Level. |
MaximumReachedEvent |
MaximumReachedEvent<public>:listenable(tuple(?agent, int)) |
Signaled when an agent reaches the maximum stat value. This occurs when the stat reaches Max Value if the device has no levels, or Max Level if that option is set. Sends the agent that caused the stat event that changed the level an |
Methods (call these to make the device act):
| Method | Signature | Description |
|---|---|---|
GetValue |
GetValue<public>(Agent:agent)<transacts><decides>:int |
Returns stat Value for Agent. Succeeds if Scope is set to Player and Agent passes the requirement checks on the device. |
GetValue |
GetValue<public>(Team:team)<transacts><decides>:int |
Returns stat Value for Team. Succeeds if Scope is set to Team and Team passes the requirement checks on the device. |
GetValueForMatch |
GetValueForMatch<public>()<transacts><decides>:int |
Returns stat Value for the match. Succeeds if Scope is set to Match. |
SetValue |
SetValue<public>(Agent:agent, Value:int)<transacts><decides>:void |
Updates stat Value for Agent. Succeeds if Scope is set to Agent and Agent passes the requirement checks on the device. If the stat does not have levels, Value is clamped between 0 and Max Value. If the stat does have levels, the |
SetValue |
SetValue<public>(Team:team, Value:int)<transacts><decides>:void |
Updates stat Value for Team. Succeeds if Scope is set to Team and Team passes the requirement checks on the device. If the stat does not have levels, Value is clamped between 0 and Max Value. If the stat does have levels, the Va |
SetValueForMatch |
SetValueForMatch<public>(Value:int)<transacts><decides>:void |
Updates stat Value for the match. Succeeds if Scope is set to Match. If the stat does not have levels, Value is clamped between 0 and Max Value. If the stat does have levels, the Value is not clamped and setting a value below 0 or a |
GetLevel |
GetLevel<public>(Agent:agent)<transacts><decides>:int |
Returns stat Level for Agent. Succeeds if Number of Levels is set to greater than 0, Scope is set to Player and Agent passes the requirement checks on the device. |
GetLevel |
GetLevel<public>(Team:team)<transacts><decides>:int |
Returns stat Level for Team. Succeeds if Number of Levels is set to greater than 0, Scope is set to Team and the Team passes the requirement checks on the device. |
GetLevelForMatch |
GetLevelForMatch<public>()<transacts><decides>:int |
Returns stat Level for the match. Succeeds if Number of Levels is set to greater than 0 and Scope is set to Match. |
SetLevel |
SetLevel<public>(Agent:agent, Level:int)<transacts><decides>:void |
Updates stat Level for Agent. Succeeds if Number of Levels is set to greater than 0 and Scope is set to Agent. Altering stat Level will set Agent's stat Value to 0. |
SetLevel |
SetLevel<public>(Team:team, Level:int)<transacts><decides>:void |
Updates stat Level for Team. Succeeds if Number of Levels is set to greater than 0 and Scope is set to Team. Altering stat Level will set Team's stat Value to 0. |
SetLevelForMatch |
SetLevelForMatch<public>(Level:int)<transacts><decides>:void |
Updates stat Level for the match. Succeeds if Number of Levels is set to greater than 0 and Scope is set to Match . Altering stat Level will set the Match's stat Value to 0. |
GetName |
GetName<public>():string |
Returns the name for the stat that is generated by this Stat Creator. |
Walkthrough
Let's build a coin XP system. The scope of our stat is Player with levels enabled (Number of Levels > 0, Max Value = 100). When a player steps on a coin pad (a button_device), we add 25 to their custom "Coins" stat. Each time the value reaches 100, the device automatically increments their Level and resets Value to 0 — and we light up a billboard with their current level.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Colors }
coin_xp_game := class(creative_device):
# The stat_creator_device — Scope = Player, Levels enabled, Max Value = 100
@editable
CoinStat : stat_creator_device = stat_creator_device{}
# Button the player interacts with to earn coins
@editable
CoinButton : button_device = button_device{}
# Billboard to show feedback
@editable
StatusBillboard : billboard_device = billboard_device{}
# Localized message helper — message params need localized text, not raw strings
StatusText<localizes>(S : string) : message = "{S}"
OnBegin<override>()<suspends>:void =
# React when a player earns/loses coins
CoinStat.ValueChangedEvent.Subscribe(OnValueChanged)
# React when a player levels up
CoinStat.LevelChangedEvent.Subscribe(OnLevelChanged)
# React when a player maxes out the stat
CoinStat.MaximumReachedEvent.Subscribe(OnMaxReached)
# Stepping the button awards coins
CoinButton.InteractedWithEvent.Subscribe(OnButtonPressed)
# Award 25 coins to whoever pressed the button
OnButtonPressed(Agent : agent) : void =
if:
# Read the player's current value (Scope must be Player)
Current := CoinStat.GetValue[Agent]
# Write the new value; clamped 0..MaxValue, overflow rolls the level
CoinStat.SetValue[Agent, Current + 25]
then:
# success
# ValueChangedEvent hands us (?agent, int) — the instigator and the new Value
OnValueChanged(Payload : tuple(?agent, int)) : void =
NewValue := Payload(1)
if (A := Payload(0)?):
StatusBillboard.SetText(StatusText("{CoinStat.GetName()}: {NewValue}"))
# LevelChangedEvent fires when Value rolled over Max Value and Level went up
OnLevelChanged(Payload : tuple(?agent, int)) : void =
NewLevel := Payload(1)
if (A := Payload(0)?):
StatusBillboard.SetText(StatusText("Level Up! Now level {NewLevel}"))
# MaximumReachedEvent fires when the player hits the maximum level/value
OnMaxReached(Payload : tuple(?agent, int)) : void =
if (A := Payload(0)?):
StatusBillboard.SetText(StatusText("MAX REACHED!"))
Line by line:
CoinStat,CoinButton,StatusBillboardare@editablefields, so you bind the real placed devices in the Details panel. You can only call a placed device through such a field.- In
OnBeginwe subscribe all three events plus the button. Handlers are ordinary methods at class scope. - The event payloads are
tuple(?agent, int).Payload(0)is the optional instigating agent — unwrap it withif (A := Payload(0)?):.Payload(1)is the new Value (or Level). GetValue[Agent]andSetValue[Agent, ...]are<decides>, so they live insideif:/then:. We read the current value, add 25, and write it back. Because levels are enabled, when the value crosses 100 the device rolls Value to 0 and raises Level — which firesLevelChangedEventfor free.GetName()returns the configured stat name as astring, handy for labels.
Common patterns
Team objective: set and read a Team-scoped value
When Scope is Team, use the (team) overloads. Here a capture trigger adds 10 to the capturing player's team score.
using { /Fortnite.com/Devices }
using { /Fortnite.com/Teams }
using { /Verse.org/Simulation }
team_capture_game := class(creative_device):
@editable
TeamScoreStat : stat_creator_device = stat_creator_device{}
@editable
CaptureTrigger : trigger_device = trigger_device{}
OnBegin<override>()<suspends>:void =
CaptureTrigger.TriggeredEvent.Subscribe(OnCaptured)
OnCaptured(MaybeAgent : ?agent) : void =
if (A := MaybeAgent?):
Teams := GetPlayspace().GetTeamCollection()
if:
Team := Teams.GetTeam[A]
Current := TeamScoreStat.GetValue[Team]
TeamScoreStat.SetValue[Team, Current + 10]
then:
# team score updated
Match-wide countdown using ...ForMatch
When Scope is Match, use GetValueForMatch/SetValueForMatch. This snippet seeds a shared match resource pool at start.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
match_resource_game := class(creative_device):
@editable
ResourceStat : stat_creator_device = stat_creator_device{}
OnBegin<override>()<suspends>:void =
ResourceStat.MaximumReachedEvent.Subscribe(OnPoolFull)
if:
ResourceStat.SetValueForMatch[50]
Seeded := ResourceStat.GetValueForMatch[]
then:
# match pool seeded to 50
OnPoolFull(Payload : tuple(?agent, int)) : void =
# the shared match pool hit its maximum
RPG level control with SetLevel / GetLevel
With levels enabled and Scope = Player, you can jump a player directly to a level. Setting Level resets that player's Value to 0.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
level_grant_game := class(creative_device):
@editable
XPStat : stat_creator_device = stat_creator_device{}
@editable
PromoteButton : button_device = button_device{}
OnBegin<override>()<suspends>:void =
XPStat.LevelChangedEvent.Subscribe(OnLevelChanged)
PromoteButton.InteractedWithEvent.Subscribe(OnPromote)
OnPromote(Agent : agent) : void =
if:
CurrentLevel := XPStat.GetLevel[Agent]
# promote one level (this resets their Value to 0)
XPStat.SetLevel[Agent, CurrentLevel + 1]
then:
# promoted
OnLevelChanged(Payload : tuple(?agent, int)) : void =
NewLevel := Payload(1)
if (A := Payload(0)?):
# player A is now NewLevel
Gotchas
- Scope decides which overload succeeds.
GetValue[Agent]fails if Scope is Team or Match;GetValueForMatch[]fails unless Scope is Match. These are<decides>calls — wrap them inifand pick the overload that matches your device's configured Scope. Mixing them silently fails theif. - All Get/Set methods are
<decides>. You must call them inside anif/then(or another failure context), not as bare statements. They also require the agent/team to pass the device's requirement checks. - Event payloads are tuples with an optional agent.
ValueChangedEvent,LevelChangedEvent, andMaximumReachedEventall hand youtuple(?agent, int). UsePayload(0)for the optional instigator (unwrap withif (X := Payload(0)?):) andPayload(1)for the new value/level. The agent isfalsewhen no instigator caused the change. - Setting Level zeroes Value.
SetLevel/SetLevelForMatchreset the corresponding Value to 0 — expected for RPG progression but surprising if you wanted to keep accumulated value. - Value clamping depends on levels. Without levels,
SetValueclamps between 0 and Max Value. With levels enabled, the value is not clamped the same way — overflow rolls into the next Level instead, which is what firesLevelChangedEvent. messageparams need localized text. Anything taking amessage(like a billboard'sSetText) needs a<localizes>helper —StatusText<localizes>(S:string):message = "{S}"— not a raw string. There is noStringToMessage.GetName()returns astring, not amessage. Use it inside string interpolation, then wrap the whole thing in your<localizes>helper if you need amessage.