Badge-Gated Doors: Conditional Buttons and Unlock Logic
In this lesson you'll learn to
- βStudent can gate interactions with conditional_button_device and a Verse gatekeeper module that checks the persistence profile.
Student can gate interactions with conditional_button_device and a Verse gatekeeper module that checks the persistence profile.
π Reference & full walkthrough: Badge-Gated Doors: Conditional Buttons and Unlock Logic
π Builds on: north-jungle: verse-modules / shared-helper-module (import the gatekeeper as a module)
π§© Your capstone piece: portal_gatekeeper module: free booths always open, paid-zone booths require the previous badge
Professor AweShucks has a rule about the five portal booths ringing the plaza: anyone may sail back to the free shores, but the paid-zone portals only open for creators who finished the zone before it. Two very different checks make that rule work β and knowing which check lives where is the whole lesson. The conditional_button_device natively verifies items (a booth fee of town coins). Your Verse gatekeeper module verifies badges β persistent, account-level progress the device panel can't see. Put each check where it belongs and the plaza runs itself.
What you will build
The portal_gatekeeper module β the capstone piece AweShucks Town's hub imports at every booth:
- A
PortalGatekeepermodule exposing badge ids,HasBadge,GrantBadge, and the one gate that matters:CanEnter[Player, RequiredBadge]. - The rule from the capstone spec: free booths always open (
RequiredBadge = 0), paid-zone booths require the previous zone's badge. - A
portal_booth_deviceyou stamp five times around the plaza β one@editable RequiredBadgeper booth is the only per-booth configuration. - The badge profile rides the persistence pattern from the Cross-Zone Progress lesson (
class<final><persistable>+weak_map(player, ...)+ copy-constructor), and the module technique is the same one you learned in the North Jungle when you built and imported CodewoodKit.
Walkthrough
Step 1 β Decide which check goes where
| Check | Owner | Why |
|---|---|---|
| Booth fee (town coins in inventory) | conditional_button_device |
The device counts key items natively and fires NotEnoughItemsEvent for free β no Verse needed. |
| Zone badge (finished the previous capstone) | Verse PortalGatekeeper module |
Badges live in a persistable profile that survives sessions. Devices can't read a weak_map β only Verse can. |
The device is the bouncer checking what's in your pockets right now; the module is the town registry checking what you've earned, ever.
Step 2 β Declare the module and its persistent profile
PortalGatekeeper<public> := module: opens the named box. Inside it: four public badge-id constants, the island_progress persistable class (the badge profile the Cross-Zone Progress lesson established), its copy-constructor, and the module-scope weak_map that the engine persists per player.
Step 3 β Write the three public verbs
HasBadge[Player, Badge]β failable lookup: read the profile,Findthe badge id in the earned array.GrantBadge[Player, Badge]β the copy-constructor idiom: build a freshisland_progressoverriding onlyBadges, copying everything else, so an award never wipes another field. It's idempotent β granting twice stores once.CanEnter[Player, RequiredBadge]β the gate. WhenRequiredBadgeis0theifbody never runs, so the check always succeeds: that's the free booth. Otherwise it delegates toHasBadge.
Step 4 β Wire the booth device
portal_booth_device subscribes to both button events. ActivatedEvent fires only after the device's own item check passed β that's the moment Verse layers the badge check on top. NotEnoughItemsEvent fires when the item check failed, so you get the "can't afford the fee" case for free.
Here is the complete script β module and booth device in one file:
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# Localized helper so we can pass dynamic text as a `message`.
BoothPrompt<localizes>(S : string) : message = "{S}"
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# PortalGatekeeper β the module every booth in AweShucks Town imports.
# Same technique as CodewoodKit from the North Jungle: one named box,
# public surface on top, private plumbing underneath.
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
PortalGatekeeper<public> := module:
# Badge ids β one per zone capstone, in curriculum order.
BadgeShellHunt<public> : int = 1
BadgeModRally<public> : int = 2
BadgeVolcano<public> : int = 3
BadgeCoastalRace<public> : int = 4
# The persistent badge profile from the Cross-Zone Progress lesson.
island_progress<public> := class<final><persistable>:
Version : int = 0
Badges : []int = array{}
# Copy-constructor: rebuild a fresh record around one change.
MakeIslandProgress<constructor>(Src : island_progress)<transacts> := island_progress:
Version := Src.Version
Badges := Src.Badges
# One profile per player, persisted across sessions by the engine.
var ProgressMap : weak_map(player, island_progress) = map{}
# Make sure the player has a profile row before any read/write.
EnsureProfile<public>(Player : player)<transacts><decides> : void =
Player.IsActive[]
if (not ProgressMap[Player]):
set ProgressMap[Player] = island_progress{}
# Succeeds only when Player has earned Badge.
HasBadge<public>(Player : player, Badge : int)<transacts><decides> : void =
Progress := ProgressMap[Player]
Progress.Badges.Find[Badge]
# Award a badge (idempotent β granting twice stores it once).
GrantBadge<public>(Player : player, Badge : int)<transacts><decides> : void =
EnsureProfile[Player]
Src := ProgressMap[Player]
if (not Src.Badges.Find[Badge]):
set ProgressMap[Player] = island_progress:
Badges := Src.Badges + array{Badge}
MakeIslandProgress<constructor>(Src)
# THE gate. Free booths pass RequiredBadge = 0 β always open.
# Paid booths pass the PREVIOUS zone's badge id.
CanEnter<public>(Player : player, RequiredBadge : int)<transacts><decides> : void =
EnsureProfile[Player]
if (RequiredBadge > 0):
HasBadge[Player, RequiredBadge]
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# One portal booth = one of these devices, wired in the editor.
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
portal_booth_device := class(creative_device):
# The conditional button players interact with at the booth.
@editable
BoothButton : conditional_button_device = conditional_button_device{}
# Fires the booth's open-portal wiring (lights, matchmaking portal enable).
@editable
OpenPortalTrigger : trigger_device = trigger_device{}
# 0 = free booth (South Shores). Paid booths use PortalGatekeeper badge ids.
@editable
RequiredBadge : int = 0
# Booth name shown in the interaction prompt.
@editable
BoothName : string = "South Shores"
OnBegin<override>()<suspends> : void =
BoothButton.SetInteractionText(BoothPrompt("Travel to {BoothName}"))
BoothButton.ActivatedEvent.Subscribe(OnBoothActivated)
BoothButton.NotEnoughItemsEvent.Subscribe(OnMissingItems)
BoothButton.Enable()
# The DEVICE's item check already passed β now run the VERSE badge check.
OnBoothActivated(Agent : agent) : void =
if (Player := player[Agent], PortalGatekeeper.CanEnter[Player, RequiredBadge]):
OpenPortalTrigger.Trigger(Agent)
Print("Portal open β safe travels to {BoothName}!")
else:
BoothButton.SetInteractionText(BoothPrompt("Locked β earn the previous zone's badge first"))
# The DEVICE said no: the player lacks the key items (the booth fee).
OnMissingItems(Agent : agent) : void =
Print("Booth fee not met β the button's own item check failed.")
Step 5 β Split it into files (the module import)
In the real hub project the module gets its own file and each booth script imports it, exactly like importing CodewoodKit in the North Jungle:
# --- file: Hub/booths.verse ---
# The gatekeeper lives in its own file/module; every booth imports it β
# exactly how you imported CodewoodKit back in the North Jungle.
using { PortalGatekeeper }
Step 6 β Stamp out the five booths in the editor
- Place a
conditional_button_device+ atrigger_deviceat each booth; set the button's key item (Town Coin) and count in its panel if the booth charges a fee. - Place one
portal_booth_deviceper booth and wireBoothButton/OpenPortalTrigger. - Set
RequiredBadge:0for South Shores (free),1(BadgeShellHunt) on the North Jungle booth,2on the Volcano booth, and so on down the chain. - The
OpenPortalTriggerfeeds whatever opens the portal β in the finished hub, the matchmaking portal's enable wiring.
Common patterns
Awarding the badge when a player returns from a zone
The other half of the gate: the return pad at the hub grants the badge the next booth requires.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# Minimal gatekeeper (same shape as the main lesson) so this file is
# self-contained. In your project it lives once, in its own file.
BadgeVault<public> := module:
badge_profile<public> := class<final><persistable>:
Version : int = 0
Badges : []int = array{}
MakeBadgeProfile<constructor>(Src : badge_profile)<transacts> := badge_profile:
Version := Src.Version
Badges := Src.Badges
var Profiles : weak_map(player, badge_profile) = map{}
GrantBadge<public>(Player : player, Badge : int)<transacts><decides> : void =
Player.IsActive[]
if (not Profiles[Player]):
set Profiles[Player] = badge_profile{}
Src := Profiles[Player]
if (not Src.Badges.Find[Badge]):
set Profiles[Player] = badge_profile:
Badges := Src.Badges + array{Badge}
MakeBadgeProfile<constructor>(Src)
# Placed at the hub's return pad: the zone capstone teleports finishers
# here, the trigger fires, and the badge lands in the profile.
badge_award_device := class(creative_device):
@editable
ReturnTrigger : trigger_device = trigger_device{}
# Which badge this pad awards (1 = Shell Hunt, 2 = Mod Rally, ...).
@editable
BadgeToGrant : int = 1
OnBegin<override>()<suspends> : void =
ReturnTrigger.TriggeredEvent.Subscribe(OnReturned)
OnReturned(MaybeAgent : ?agent) : void =
if (Agent := MaybeAgent?, Player := player[Agent], BadgeVault.GrantBadge[Player, BadgeToGrant]):
Print("Badge banked β the next booth on the plaza just unlocked.")
Letting the device do all the money work
When a booth just charges coins with no badge rule, don't write a Verse check at all β configure the device and listen:
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
FeePrompt<localizes>(S : string) : message = "{S}"
# A booth that ALSO charges town coins β the device handles the item
# cost natively; Verse only tunes it and reads the result.
fee_booth_device := class(creative_device):
@editable
BoothButton : conditional_button_device = conditional_button_device{}
# Key item index 0 = Town Coin (configured in the device panel).
CoinIndex : int = 0
@editable
CoinCost : int = 5
OnBegin<override>()<suspends> : void =
# Enforce the fee in code so it's tuned from one place.
BoothButton.SetItemCountRequired(CoinIndex, CoinCost)
BoothButton.SetInteractionText(FeePrompt("Pay {CoinCost} coins to travel"))
BoothButton.Enable()
BoothButton.ActivatedEvent.Subscribe(OnPaid)
OnPaid(Agent : agent) : void =
# HasAllItems is <decides> β usable as a readiness probe elsewhere too.
if (BoothButton.HasAllItems[Agent]):
Print("Player could pay again β coins to spare.")
else:
Print("Fee paid β that was their last coinful.")
Where this goes next
This module is the beating heart of the AweShucks Town hub capstone. The very next lessons consume it: the portal-gate scene-graph prefab reuses one gatekeeper call per booth, and the matchmaking-portal-device lesson enables the five live zone portals only when PortalGatekeeper.CanEnter says so. By the final integration lesson, every paid door in town swings on the hinge you just built β and the Halo of Badges above the plaza lights straight from island_progress.Badges.
Keep going β free
You've read the intro. The rest of this lesson is free for members. Sign in to continue and track your progress.
Sign in free to continue this lessonSources
/guides/portal-gating-conditional-buttonBiloxi Studios original lesson
Add this lesson to your free study plan.
π§ The Keeperβs log
Quest complete? Chart your next heading from the π§ Center Village (Free) expedition.
β΅ Take me there