Reference Devices compiles

PROJECT: Sunken Dive Round (integrated mode)

Seven lessons of parts become one playable machine: a two-team dive round where divers drop through a trigger gate into first-person, grab relic plates on the seafloor, and race a 90-second air supply to bank their haul on the surface barge. This is CAPSTONE PHASE 1 of the Athenaeum Depths — and a deliberate reunion tour, reusing the west-coves race{} timeout, the south-shores enum phase machine and SpawnProp, the center-village round-logic spine, and the east-volcano persistable ledger.

Updated Examples verified on the live UEFN compiler

You have spent seven lessons collecting parts on the seafloor: trigger plates, team loadouts, a first-person dive cam, relic arrays, leave-cleanup, and an air-supply timer. Today you bolt them together into one machine. This is CAPSTONE PHASE 1 of the Athenaeum Depths — the entire playable, device-driven dive round — and it is also a tour of the whole archipelago, because almost every pattern in it was forged in another zone first.

What you will build

A complete 2-team dive round:

  • Divers drop through a moon-pool trigger gate, snap into first-person camera, and hunt relic plates scattered across the seafloor.
  • Each relic plate grabbed adds to a per-player carried tally. Carried relics are worth nothing until they are banked on the surface barge plate.
  • The whole dive runs inside an air-supply race{}: bank RelicsToWin relics before AirSeconds expires, or the sea keeps whatever you still carry.
  • A four-state phase enum (Lobby → Diving → Surfacing → Debrief) is the single source of truth; every handler checks it before acting.
  • Banked totals are folded into a persistent ledger (weak_map(player, int)) that survives across sessions.
  • Tenders (the second team) hold the barge; if every diver runs out of respawns, the tenders win outright.

This is the playable core that Phase 2 (athenaeum-depths) will fuse with the scene-graph relic economy and the sequencer reveal.

Architecture at a glance

One device, sunken_dive_round, owns the round. Everything else is wiring:

DiveGate (trigger) ──► first-person cam AddTo
RelicPlates[] (triggers) ──► RelicsCarried[player] += 1
BankPlate (trigger) ──► RelicsBanked += carried, Signal RelicBankedEvent
                                   │
RunRound(): Lobby ► Diving ► race{ air timer | banked-out } ► Surfacing ► Debrief
                                   │
RelicLedger (module weak_map) ◄── banked totals persist across sessions

The round spine is the same start/play/win/reset orchestration you built in Professor AweShucks' custom-round-logic-using-verse — only the phases have grown teeth.

Walkthrough

Here is the full device. Read the section banners: each one names the zone that taught the pattern it uses.

using { /Fortnite.com/Devices }
using { /Fortnite.com/Game }
using { /Fortnite.com/Playspaces }
using { /Verse.org/Simulation }
using { /Verse.org/Random }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /UnrealEngine.com/Temporary/SpatialMath }

# The ship's ledger (east-volcano persistable pattern): total relics each
# player has EVER banked. A module-scoped weak_map keyed on player
# persists across sessions automatically — no save device required.
var RelicLedger : weak_map(player, int) = map{}

# The four phases of a dive round, in play order (the south-shores
# enum phase machine, back for its deepest job yet).
dive_phase := enum:
    Lobby
    Diving
    Surfacing
    Debrief

# The whole playable dive round, orchestrated by one device.
sunken_dive_round := class(creative_device):

    # --- Trigger plates (deeps lesson: dive-trigger-volumes) ---

    # Plate at the moon pool. Crossing it drops you into first person.
    @editable
    DiveGate : trigger_device = trigger_device{}

    # One plate per sunken relic, placed on the seafloor.
    @editable
    RelicPlates : []trigger_device = array{}

    # Plate on the surface barge where carried relics become banked.
    @editable
    BankPlate : trigger_device = trigger_device{}

    # --- Team loadouts (deeps lesson: team-settings-and-inventory-device) ---

    @editable
    DiverLoadout : team_settings_and_inventory_device = team_settings_and_inventory_device{}

    @editable
    TenderLoadout : team_settings_and_inventory_device = team_settings_and_inventory_device{}

    # --- First-person dive cam (deeps lesson: gameplay-camera-first-person-device) ---

    @editable
    DiveCam : gameplay_camera_first_person_device = gameplay_camera_first_person_device{}

    # --- Presentation + round end ---

    @editable
    Announcer : hud_message_device = hud_message_device{}

    @editable
    RoundEnd : end_game_device = end_game_device{}

    # --- Relic props (south-shores SpawnProp) ---

    @editable
    RelicAsset : creative_prop_asset = DefaultCreativePropAsset

    @editable
    RelicMarkers : []creative_prop = array{}

    # --- Tuning ---

    @editable
    LobbySeconds : float = 15.0

    # The air supply: the entire dive must fit inside this window.
    @editable
    AirSeconds : float = 90.0

    @editable
    RelicsToWin : int = 3

    # --- Round state ---

    # The single source of truth for the whole round.
    var CurrentPhase : dive_phase = dive_phase.Lobby

    # Every relic prop spawned this round, so we can Dispose them later.
    var SpawnedRelics : []creative_prop = array{}

    # Relics grabbed below but not yet banked. Lost if the air runs out.
    var RelicsCarried : [player]int = map{}

    # Relics safely banked on the barge this round.
    var RelicsBanked : [player]int = map{}

    # Fires every time someone banks (west-coves events-over-polling).
    RelicBankedEvent : event() = event(){}

    # Wraps a plain string so message-typed parameters accept it.
    Msg<localizes>(S : string) : message = "{S}"

    OnBegin<override>()<suspends> : void =
        DiveCam.Enable()
        DiveGate.TriggeredEvent.Subscribe(OnDiveGate)
        BankPlate.TriggeredEvent.Subscribe(OnBankPlate)

        # If every diver runs out of respawns, the tenders hold the barge.
        DiverLoadout.TeamOutOfRespawnsEvent.Subscribe(OnDiversWiped)

        # Leave-cleanup (deeps lesson 5): never count a ghost diver.
        GetPlayspace().PlayerRemovedEvent().Subscribe(OnPlayerLeft)

        RunRound()

    # ---------- The round spine (center-village round-logic shape) ----------

    RunRound()<suspends> : void =
        EnterPhase(dive_phase.Lobby)
        Sleep(LobbySeconds)

        SeedTallies()
        SpawnRelics()
        WatchRelicPlates()

        EnterPhase(dive_phase.Diving)

        # west-coves race{}: the dive ends when EITHER the air runs out
        # OR someone banks enough relics. First branch to finish wins;
        # the loser is cancelled on the spot.
        race:
            block:
                Sleep(AirSeconds)
                Announcer.Show(Msg("Air spent! The sea keeps whatever you still carry."), ?DisplayTime := 5.0)
            AwaitBankedOut()

        EnterPhase(dive_phase.Surfacing)
        DiveCam.RemoveFromAll()
        BankRoundIntoLedger()
        CleanupRelics()
        Sleep(3.0)

        EnterPhase(dive_phase.Debrief)
        CrownChampion()

    # ---------- Phase machine (south-shores enum-with-data pattern) ----------

    # ONE doorway for every phase change: reassign, then react.
    EnterPhase(NewPhase : dive_phase) : void =
        set CurrentPhase = NewPhase
        Announcer.Show(Msg(PhaseLabel(NewPhase)), ?DisplayTime := 4.0)
        Print("Phase is now: {PhaseLabel(NewPhase)}")

    # Each phase's on-screen banner text — the data riding on the enum.
    PhaseLabel(Phase : dive_phase) : string =
        case (Phase):
            dive_phase.Lobby => "Suit up - the dive begins soon"
            dive_phase.Diving => "DIVE! Bank {RelicsToWin} relics before your air runs out"
            dive_phase.Surfacing => "Surfacing - tallying the haul"
            dive_phase.Debrief => "Debrief - crowning the deep champion"
            _ => "Unknown phase"

    # ---------- Setup ----------

    SeedTallies() : void =
        for (P : GetPlayspace().GetPlayers()):
            if (set RelicsCarried[P] = 0) {}
            if (set RelicsBanked[P] = 0) {}

    SpawnRelics() : void =
        for (Marker : Shuffle(RelicMarkers)):
            # SpawnProp returns tuple(?creative_prop, spawn_prop_result).
            Result := SpawnProp(RelicAsset, Marker.GetTransform())
            if (Relic := Result(0)?):
                set SpawnedRelics += array{Relic}
        Print("{SpawnedRelics.Length} relics rest in the silt.")

    WatchRelicPlates() : void =
        for (Plate : RelicPlates):
            Plate.Enable()
            spawn{ WatchRelicPlate(Plate) }

    # One async watcher per plate, so we know WHICH plate fired —
    # a Subscribe handler only receives the ?agent, never the device.
    WatchRelicPlate(Plate : trigger_device)<suspends> : void =
        MaybeAgent := Plate.TriggeredEvent.Await()
        Plate.Disable()   # one relic per plate
        if:
            Agent := MaybeAgent?
            P := player[Agent]
            CurrentPhase = dive_phase.Diving
            Carried := RelicsCarried[P]
            set RelicsCarried[P] = Carried + 1
        then:
            Announcer.Show(Agent, Msg("Relic grabbed! Carrying {Carried + 1} - get it to the barge."), ?DisplayTime := 3.0)

    # ---------- Trigger handlers (dive-trigger-volumes) ----------

    # Crossing the dive gate mid-round pushes the first-person cam.
    OnDiveGate(MaybeAgent : ?agent) : void =
        if (Agent := MaybeAgent?, CurrentPhase = dive_phase.Diving):
            DiveCam.AddTo(Agent)

    # The bank plate converts carried relics into banked relics.
    OnBankPlate(MaybeAgent : ?agent) : void =
        if:
            Agent := MaybeAgent?
            P := player[Agent]
            CurrentPhase = dive_phase.Diving
            Carried := RelicsCarried[P]
            Carried > 0
            Banked := RelicsBanked[P]
            set RelicsBanked[P] = Banked + Carried
            set RelicsCarried[P] = 0
        then:
            DiveCam.RemoveFrom(Agent)   # back to third person on deck
            Announcer.Show(Agent, Msg("Banked {Banked + Carried} relics!"), ?DisplayTime := 3.0)
            RelicBankedEvent.Signal()

    # Completes only when some diver has banked enough. If nobody ever
    # does, the air-timer branch of the race cancels this loop.
    AwaitBankedOut()<suspends> : void =
        loop:
            RelicBankedEvent.Await()
            Best := BestBanked()
            if (Best >= RelicsToWin):
                break

    BestBanked() : int =
        var Best : int = 0
        for (P -> Count : RelicsBanked, Count > Best):
            set Best = Count
        return Best

    # ---------- Wrap-up ----------

    # Fold this round's haul into the persistent ledger (east-volcano).
    BankRoundIntoLedger() : void =
        for (P -> Count : RelicsBanked, Count > 0):
            var Total : int = Count
            if (Previous := RelicLedger[P]):
                set Total = Previous + Count
            if (set RelicLedger[P] = Total) {}
            Print("Ledger updated: {Total} relics banked all-time.")

    # Dispose every relic prop still on the seafloor (south-shores).
    CleanupRelics() : void =
        for (Relic : SpawnedRelics, Relic.IsValid[]):
            Relic.Dispose()
        set SpawnedRelics = array{}

    CrownChampion() : void =
        var MaybeChampion : ?player = false
        var BestCount : int = 0
        for (P -> Count : RelicsBanked, Count > BestCount):
            set MaybeChampion = option{P}
            set BestCount = Count
        if (Champion := MaybeChampion?):
            Announcer.Show(Msg("Deep champion! {BestCount} relics banked."), ?DisplayTime := 5.0)
            RoundEnd.Activate(Champion)
        else:
            Announcer.Show(Msg("The sea keeps its treasure. Nobody banked a relic."), ?DisplayTime := 5.0)

    # ---------- Lifecycle edges ----------

    OnDiversWiped() : void =
        Print("All divers down - the tenders hold the barge.")
        TenderLoadout.EndRound()

    # deeps lesson 5: sweep a leaver out of every tally so ghost
    # entries never skew BestBanked or the champion pick.
    OnPlayerLeft(Leaver : player) : void =
        set RelicsCarried = RemoveKey(RelicsCarried, Leaver)
        set RelicsBanked = RemoveKey(RelicsBanked, Leaver)
        Print("A diver left the water. Tallies swept clean.")

    RemoveKey(Source : [player]int, Gone : player) : [player]int =
        var Rebuilt : [player]int = map{}
        for (P -> Count : Source, P <> Gone):
            if (set Rebuilt[P] = Count) {}
        return Rebuilt

Step through the load-bearing choices:

  1. The phase enum is law. Every trigger handler starts with CurrentPhase = dive_phase.Diving in its failure context. A plate stepped on during Debrief does nothing. This is the south-shores enum machine doing exactly what it was built for: transitions happen in ONE place (EnterPhase), and everything else just reads.
  2. The air supply is a race{}. One branch sleeps for AirSeconds; the other (AwaitBankedOut) loops on RelicBankedEvent.Await() until somebody's banked count reaches RelicsToWin. Whichever finishes first cancels the other — Inkbeard's west-coves timeout pattern, verbatim. No polling loop, no flag-checking every frame.
  3. One spawn{} watcher per relic plate. A Subscribe handler only receives the ?agent — it cannot tell you which plate fired. Awaiting each plate in its own spawned task gives every watcher its own Plate binding, so it can Disable() exactly the plate that was claimed.
  4. Carried is not banked. RelicsCarried and RelicsBanked are separate [player]int maps on purpose: the air timer wipes carried relics (they were never banked), which is the entire risk/reward tension of the mode.
  5. The camera follows the fiction. DiveGate pushes the first-person cam onto the crossing agent; the bank plate pops it (you are on deck now); RemoveFromAll() at Surfacing catches anyone the timer dragged up.
  6. Leavers are swept, not ignored. PlayerRemovedEvent rebuilds both tally maps without the leaver — the deeps lesson-5 habit. Skip this and a ghost entry can win your round.
  7. The ledger outlives the round. RelicLedger is a module-scoped weak_map(player, int) — Cinder's east-volcano persistable pattern. BankRoundIntoLedger folds each round's haul in, and it is still there next session.

Device wiring checklist

In the UEFN editor, before you press Launch Session:

  • [ ] DiveGate — trigger_device at the moon pool / dive-hole entry. Triggered by Player, unlimited triggers.
  • [ ] RelicPlates — one trigger_device per relic spot on the seafloor. Set Enabled at Game Start: No (Verse enables them when the dive starts) and place each one on a RelicMarkers prop so the spawned relic visually sits on its plate.
  • [ ] BankPlate — trigger_device on the barge deck, unlimited triggers.
  • [ ] DiverLoadout — team_settings_and_inventory_device on Team 1 (divers): grant fins/harpoon loadout, set spawn limits here.
  • [ ] TenderLoadout — second device on Team 2 (tenders): barge-keeper loadout.
  • [ ] DiveCam — gameplay_camera_first_person_device, defaults are fine.
  • [ ] Announcer — hud_message_device.
  • [ ] RoundEnd — end_game_device with Activating Team enabled, so Activate(Champion) ends the round in the champion's team's favor.
  • [ ] RelicAsset / RelicMarkers — your relic prop asset plus one placed marker prop per plate.
  • [ ] Island settings: 2 teams, team rotation off, round time comfortably above LobbySeconds + AirSeconds.

Imported from across the archipelago

This lesson is deliberately a reunion. What you are reusing, and where it was forged:

Pattern in this device Zone that taught it
race { Sleep(AirSeconds) … } timeout + event().Await() over polling + spawn{} watchers west-coves async module (Inkbeard's trench)
dive_phase enum + EnterPhase single doorway + PhaseLabel case data south-shores enum-with-data phase machine (Coral's beach)
SpawnProp / IsValid[] / Dispose relic rounds south-shores spawn-prop
RunRound() start/play/win/reset spine center-village custom-round-logic module (Professor AweShucks)
Module-scoped weak_map(player, int) persistent ledger east-volcano persistable pattern (Cinder)
Trigger plates, team loadouts, first-person cam, leave-cleanup the-deeps lessons 4–7

Common patterns

A low-air warning as a third race branch. A branch that never finishes can still do work along the way — a classic west-coves trick for layered timers:

# Give divers a shrinking-air warning without touching the round spine:
# run the countdown as a second race branch that never wins.
race:
    block:
        Sleep(AirSeconds)
        Announcer.Show(Msg("Air spent!"), ?DisplayTime := 5.0)
    AwaitBankedOut()
    block:
        # Warning track: fires at 2/3 air, then keeps waiting forever
        # so it can never be the branch that ends the race.
        Sleep(AirSeconds * 0.66)
        Announcer.Show(Msg("Air is running low - surface soon!"), ?DisplayTime := 4.0)
        Sleep(Inf)

Team victory instead of an individual champion. team_settings_and_inventory_device.EndRound() declares that device's team the winner, which suits a co-op dive better than crowning one diver:

# Prefer a team victory over an individual champion? Let the loadout
# device end the round for the whole diver team instead:
CrownDiverTeam() : void =
    if (BestBanked() >= RelicsToWin):
        # EndRound on the team's own settings device declares that
        # team the winner - no end_game_device needed.
        DiverLoadout.EndRound()
    else:
        TenderLoadout.EndRound()

Where this goes next

This device IS Phase 1 of the zone capstone. athenaeum-depths (the-deeps lesson 21) imports it wholesale and fuses it with everything the scene-graph half of the zone teaches: the relic props become scene-graph entities with glow/sound components, banking feeds a relic economy, and a sequencer + data-layer reveal opens the inner Athenaeum when the ledger crosses its threshold. Keep this file clean — future-you is its next importer.

And your ledger already persists, so every dive from here on is banking toward that door.

Build your own lesson with sunken_dive_round

Generate a personalized, step-by-step lesson plan built around this object — grounded in this exact reference and our compile-verified knowledge base.

Build a lesson →