Overview
Most of Verse's type system runs at compile time — the compiler proves your types line up and then throws that information away. But sometimes you don't know a value's exact type until the game is running. Picture a pirate cove where three kinds of chests wash ashore: a GoldChest, a CursedChest, and a plain DecoyChest. They all share a common Chest base, and your loot script receives them as Chest — but the reward depends on which subtype actually showed up.
That is exactly the problem casting-subtype solves. By marking a class <castable>, you attach runtime type information so your code can perform a fallible cast — SubType[Value] — which succeeds (and hands you the narrowed value) only if the object really is that subtype. It is the Verse equivalent of asking "is this chest actually cursed?" and only running the curse logic when the answer is yes.
Reach for casting-subtype whenever you have a heterogeneous collection of related objects (enemies, pickups, interactable props) handed to you through a shared base type, and you need to branch on the real type at runtime. It pairs naturally with a repeating tick loop (via Sleep) and device events, so your island can keep re-checking as new objects arrive on the shore.
API Reference
(API surface could not be resolved for this device.)
Walkthrough
Our sunny scenario: a lagoon dock where chests drift in. A timer_device counts down each wave, and when it starts we sort the chests we know about. Each chest is a <castable> subtype of a common base, and we branch on the real subtype to hand out rewards, print a curse warning, or ignore a decoy. Notice the code calls the device's real Start method and uses a Sleep-driven loop — not just Print for its own sake.
using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/Patchwork }
using { /Verse.org/Simulation }
# A localized-message helper (message params need this, not raw strings)
CoveText<localizes>(S:string):message = "{S}"
# Common base — marked <castable> so subtypes can be tested at runtime.
chest := class<castable>:
Label:string = "chest"
# Three subtypes that all wash ashore as `chest`.
gold_chest := class(chest):
Coins:int = 100
cursed_chest := class(chest):
Damage:int = 25
decoy_chest := class(chest):
cove_dock := class(creative_device):
# The wave timer placed in the level.
@editable WaveTimer:timer_device = timer_device{}
# The chests that drifted into the cove this session.
var Chests:[]chest = array{}
OnBegin<override>()<suspends>:void =
# Seed the cove with a mixed pile of chests.
set Chests = array{ gold_chest{}, decoy_chest{}, cursed_chest{}, gold_chest{} }
# Kick off the wave timer for every player, then sort chests each wave.
for (Player : GetPlayspace().GetPlayers()):
WaveTimer.Start(Player)
loop:
SortChests()
Sleep(5.0) # re-sort every 5 seconds as new chests arrive
# Walk the pile and branch on each chest's REAL subtype.
SortChests():void =
for (C : Chests):
HandleChest(C)
HandleChest(C:chest):void =
# Fallible cast: gold_chest[C] succeeds only if C really is a gold_chest.
if (Gold := gold_chest[C]):
Print(CoveText("Gold chest! Coins: {Gold.Coins}"))
else if (Cursed := cursed_chest[C]):
Print(CoveText("Cursed chest! Damage: {Cursed.Damage}"))
else:
Print(CoveText("Just a decoy — leave it on the sand."))
Line by line:
CoveText<localizes>(...)— thePrint/message system wants a localizedmessage, so we make a tiny helper. There is noStringToMessage.chest := class<castable>:— the<castable>specifier is the key. Without it,gold_chest[C]would not compile because Verse would have no runtime type info to test against.gold_chest,cursed_chest,decoy_chestall extendchest, so they can be stored in one[]chestarray.@editable WaveTimer:timer_device— a device field you drop a real Timer device into. BareWaveTimer.Start(...)only works because it is an@editablefield on acreative_device.WaveTimer.Start(Player)— calls the device's REALStart(Agent:agent):void, one call per player.gold_chest[C]is the fallible cast: it returns an optional, soif (Gold := gold_chest[C]):only runs whenCtruly is a gold chest, and inside the blockGoldis typed asgold_chest(soGold.Coinsis legal).Sleep(5.0)yields for 5 seconds each loop so we re-sort as the cove changes without hogging the frame.
Common patterns
Pattern 1 — Dynamic type-based casting with a stored type
Types are first-class values in Verse, so you can pass the type you want to test for into a helper and reuse it. Here we count how many chests match a given subtype.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
chest := class<castable>{}
gold_chest := class(chest){}
cursed_chest := class(chest){}
treasure_counter := class(creative_device):
var Chests:[]chest = array{}
OnBegin<override>()<suspends>:void =
set Chests = array{ gold_chest{}, cursed_chest{}, gold_chest{} }
GoldCount := CountGold()
Print(TC("Gold chests on the dock: {GoldCount}"))
CountGold()<transacts>:int =
var Total:int = 0
for (C : Chests, gold_chest[C]):
set Total += 1
Total
TC<localizes>(S:string):message = "{S}"
The for (C : Chests, gold_chest[C]) uses the fallible cast as a filter — the loop body only runs for chests that pass the cast.
Pattern 2 — Casting inside a tick loop with Sleep
Every frame we scan the pile and react only to cursed chests. Sleep(0.0) yields to the next update so the coroutine cooperates.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
chest := class<castable>{}
cursed_chest := class(chest):
Damage:int = 25
gold_chest := class(chest){}
curse_watcher := class(creative_device):
var Chests:[]chest = array{ cursed_chest{}, gold_chest{}, cursed_chest{} }
OnBegin<override>()<suspends>:void =
loop:
for (C : Chests):
if (Cursed := cursed_chest[C]):
Print(CW("A cursed chest deals {Cursed.Damage}!"))
Sleep(0.0) # yield until next tick
CW<localizes>(S:string):message = "{S}"
Pattern 3 — Starting a device per matched subtype
Combine the cast with the timer device: only start the wave timer when at least one gold chest is present, and start it for the interacting player.
using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/Patchwork }
using { /Verse.org/Simulation }
chest := class<castable>{}
gold_chest := class(chest){}
reward_gate := class(creative_device):
@editable RewardTimer:timer_device = timer_device{}
var Chests:[]chest = array{ gold_chest{} }
OnBegin<override>()<suspends>:void =
if (HasGold()):
for (Player : GetPlayspace().GetPlayers()):
RewardTimer.Start(Player)
HasGold()<decides><transacts>:void =
var Found:logic = false
for (C : Chests):
if (gold_chest[C]):
set Found = true
Found?
Gotchas
- Forgetting
<castable>. If the base class is not marked<castable>,SubType[Value]fails to compile — Verse has no runtime type data to check against. The specifier is mandatory for fallible subtype casts. - Fallible cast is fallible.
gold_chest[C]returns an optional-style result and must be used in a failure context (if (X := gold_chest[C]):, aforfilter, or a<decides>function). Using it in a plain assignment will not compile. - Base type in your collection. Store the pile as
[]chest(the base), not[]gold_chest, otherwise you have already narrowed the type and there is nothing left to cast. - Localized messages, not strings.
Print(and anymessageparam) needs a localizedmessage. Use a<localizes>helper likeCoveText("..."); there is noStringToMessage. - Devices must be
@editablefields. CallingWaveTimer.Start(Player)only works becauseWaveTimeris an@editable timer_deviceon thecreative_device. A baretimer_device{}.Start(...)on an unplaced device does nothing useful. Starttakes an agent. The real signature isStart(Agent:agent):void— pass a specific player/agent (e.g. fromGetPlayspace().GetPlayers()), unwrapping any?agentfrom an event first withif (A := Agent?):.Sleepcooperation. In aloop, alwaysSleep(evenSleep(0.0)) so the coroutine yields; an un-yieldingloopwill hang the frame.