
Volcano Survival Capstone: The Emberpeak Trials — Compose the Wave Engine, Add the Shrinking Zone, Wire the Lives
Overview
Every zone on Verse Island plays its own game mode — the zone's capstone. The East Volcano's is Volcano Survival: The Emberpeak Trials — endless waves, escalating difficulty, a shrinking safe zone. This is that mode's build story, and its lesson is composition. The zombie-survival module derived a complete wave-survival loop from scratch. Volcano Survival re-derives none of it. It is a capstone, not a flagship: it imports that module's wave vocabulary and loot pipeline as published, adds one genuinely new mechanic — a shrinking safe zone, built so another mode can lift it whole — and wires the two together through a hook the wave module documented but nothing had ever called. Four new files, verified as one unit with the three already-shipped files they compose, on the real UEFN compiler.
The design mandate, in one line: mine the wave patterns, compose them — don't re-derive. Every piece of the wave vocabulary — wave_state, wave_phase, wave_config, player_lives, wave_triggers — is the exact zombie-survival type, imported, never copied or re-declared. Loot still drops through zs_loot.RollLoot on every elimination. The only new logic is what the zone demands: an endless escalation curve with no final win-wave, the shrinking zone itself, and best-wave scoring.
| File | Responsibility | Depends on |
|---|---|---|
vs_endless.verse |
the endless escalation curve + the outer no-win-wave loop | zombie-survival's zs_types |
vs_safe_zone.verse |
the shrinking zone: scene-graph scale + a damage-tick poll | nothing — deliberately standalone |
vs_best_wave.verse |
best-wave record: one pure transform + a storage-agnostic seam | nothing |
vs_mode_device.verse |
the capstone wiring: nests, zone marker, HUD, both bridges | all three above + zs_types/zs_loot |
The dependency arrow still points one way — device → logic modules → shared types — plus one cross-module arrow into zombie-survival.
The mode, piece by piece
For the wave engine's own depth — the phase rules, the custom-brain zombie, the loot roll's internals — read the zombie-survival module guide; this article doesn't re-teach it. What each file below adds is a composition seam.
vs_endless.verse — escalation as a formula, and a loop that never wins
The source module answers "how do waves grow" with an authored array of wave_config rows. An endless mode has no fixed length to author rows for, so vs_endless answers the same question with a formula: an endless_curve of eight @editable numbers, and one pure function, GenerateWaveConfig, that turns a wave number into a fresh wave_config — bigger budget, tighter spawn interval, longer duration:
set Budget += Curve.BudgetPerWave
set Interval = Max(Interval - Curve.IntervalShrinkPerWave, Curve.IntervalFloorSeconds)
set Duration += Curve.DurationPerWave
set Step += 1
Accumulating in a plain counting loop, instead of multiplying an int delta into a float, sidesteps the int-to-float conversion question — the same accumulate-and-return shape the farming module's level computation uses.
Then comes this capstone's hardest-won lesson. The obvious single point of composition was the source engine's wave-clear counting leg — the one function that counts a wave down purely off elimination events, no polling. It can't be called from here: the helper carries no <public> marker, and the compiler rejects the access even with both files compiled together in one unit. Only the module's single entry point, RunWaveLoop, is public — and calling it once per wave was rejected too: fed a single-wave array it declares a full-round win at that wave's tail, and an endless mode must never show a win, even briefly. The smallest reuse the real public surface allows is a five-line copy of the counting shape, in this file's own name:
WatchWaveClear(State : zs_types.wave_state, Triggers : zs_types.wave_triggers)<suspends> : void =
loop:
Triggers.ZombieEliminatedEvent.Await()
set State.AliveCount = State.AliveCount - 1
if (State.AliveCount <= 0):
break
The outer loop mirrors the source engine's per-wave race exactly — announce, activate, then three racing legs — first to finish decides the wave:
race:
WatchWaveClear(State, Triggers)
block:
Sleep(Config.WaveDurationSeconds)
block:
Triggers.AllPlayersDownEvent.Await()
set GameLost = true
The one structural difference: wave_phase.Won is never assigned. The loop ends only on team defeat; each survived wave just increments the number and generates a harder config.
vs_safe_zone.verse — a zone built to travel
The Emberpeak zone is the capstone's one net-new mechanic, and it is written to leave: no wave types, no zombie imports, zero dependencies — so a sibling mode (say, a coastal race with a storm timeout) can lift the file whole. Its boundary with the rest of the world is a two-line interface:
zone_damage_sink<public> := interface:
OnPlayerOutsideZone<public>(Player : player) : void
The zone never decides what happens to a player caught outside — a lost life, a score penalty, a damage tick — only that it happened. That's the same sink idiom every module in this series uses for its consequences, restated for a scene-graph mechanic.
The shrink itself is deliberately multiply-only. Tuning is expressed as scale fractions (MinScale, ShrinkStepScale, both 0.0–1.0), not world-unit radii converted back and forth by division, and the transform-facing scale and the damage-facing radius are updated in lockstep by the same step — never one derived from the other at read time:
if (State.CurrentScale > Config.MinScale):
NextScale := Max(State.CurrentScale - Config.ShrinkStepScale, Config.MinScale)
set State.CurrentScale = NextScale
set State.CurrentRadius = Config.StartRadius * NextScale
ApplyScale(NextScale)
The scale change is a plain scene-graph transform — read the entity's transform, rebuild it with the same translation and rotation and a uniform scale, write it back:
ApplyScale(Factor : float)<transacts> : void =
CurrentTransform := Entity.GetGlobalTransform()
NewTransform := transform:
Translation := CurrentTransform.Translation
Rotation := CurrentTransform.Rotation
Scale := vector3{Forward := Factor, Left := Factor, Up := Factor}
Entity.SetGlobalTransform(NewTransform)
The zone's other racing leg is the damage-tick poll: every couple of seconds, any player farther from the center than CurrentRadius is reported through the sink — the honest primitive, since this marker is a scene-graph component, not a trigger volume, with no native "player left the zone" event to await.
vs_best_wave.verse — scoring without spending storage
Best-wave scoring is one persisted shape (best_wave_record, a plain struct with a Version field for add-only evolution), one seam (best_wave_sink), and one pure transform:
RecordWave<public>(Record : best_wave_record, WaveReached : int) : best_wave_record =
var Result : best_wave_record = Record
if (WaveReached > Record.BestWave):
Updated : best_wave_record = best_wave_record{BestWave := WaveReached, Version := Record.Version}
set Result = Updated
Result
The pre-declared var Result, with if: used purely as a statement, is the confirmed workaround for a real compiler landmine: constructing a struct archetype directly as the value of an if/else expression branch fails, even off a plain comparison. Note what this module does not own: any storage. Your island gets at most two persistent player maps, and that budget belongs to your project, not to a reusable mode — this set ships a session-only in-memory stand-in; a real project embeds best_wave_record as one more field on its single player-data envelope.
vs_mode_device.verse — where the composition happens
The device file is where the three new pieces meet the source module, through two small bridge classes. The first is the payoff of the whole composition story. The wave module shipped a documented extension hook for "a player went down" — player_lives.ReportDown — that nothing had ever called. The zone-consequence bridge is its first real caller:
OnPlayerOutsideZone<override>(Player : player) : void =
HUD.Show(Player, ZoneWarningMsg())
# ReportDown returns a plain `logic` (not an option) — the postfix
# `?` query is the confirmed idiom to branch on a stored logic value
# (economy article key line 4 / GOTCHA-shape: `if (IsOwned(...)?):`).
AllDown := Lives.ReportDown(Player)
if (AllDown?):
Triggers.AllPlayersDownEvent.Signal()
Read what that buys: no parallel health system. A player caught outside the zone loses a life through the same team-survival pressure zombie eliminations already race against — the zone is simply a second way to trigger the pressure the mode already has. The second bridge is the in-memory best_wave_sink implementation, a plain per-player map behind the seam.
OnBegin then composes the whole mode in one race:
Everything else mirrors the source module's device wiring: four creature nests plus one custom-brain nest all subscribe elimination to one shared handler, which signals the wave counter and rolls loot through zs_loot.RollLoot — rarity picks one of three granters. The zone marker is found in the scene (one reserved mesh entity), constructed in code with its config and sink, and attached — the sink has no default, so the marker can't be placed bare in the editor and silently do nothing.
Walkthrough
Assemble the engine. In your project's module folder, keep the three zombie-survival files this mode composes (zs_types, zs_wave_manager, zs_loot) alongside the four new ones — the mode compiles as one unit with them. Place the mode device, link the four creature nests, the brain nest, a player spawner, the wave banner, the tracker, the zone-warning HUD, and the three rarity granters. The escalation curve's eight numbers all live in the Details panel — steeper BudgetPerWave, faster IntervalShrinkPerWave, no new code.
Add the zone. Reserve one mesh entity under your scene root as the zone marker — the caldera's center. On game start the device finds it, attaches the shrink component, and the zone starts at StartRadius, stepping down by ShrinkStepScale every ShrinkIntervalSeconds toward MinScale, visibly scaling the marker as it goes.
Wire the lives. Set StartingLives; the device assigns them to every player at start. Both pressures — a zombie takedown of the last life, a zone tick on the last life — converge on the same all-players-down event the wave loop races against. On defeat, the device reports each player's wave through the best-wave seam and shows a personalized banner with their record.
Verify it together. This mode's compile truth is a combined check: its four files plus the exact three files it composes, checksum-pinned so what was verified is provably what ships, run green on the real UEFN compiler — twice, once during the build and again in a final independent check before it shipped. One honest gap, flagged not papered over: the in-world full-loop smoke test (waves escalate, zone shrinks, damage outside, best-wave records) hadn't been run when this shipped — the explicit follow-up before the mode counts as fully done.
Gotchas
Only a module's <public> surface is composable. The defining lesson of this capstone: a dependency's internal helper stays invisible to your files even when everything compiles together in one unit — the access check still applies. Reuse the public API; where you need an internal shape, re-derive the minimal version in your own file (that's all WatchWaveClear is). Never widen the other module's access for a consumer's convenience. The zombie-survival module guide's own Gotchas collect the engine-side lessons this one builds on.
A composed mode is verified as one unit, with its dependencies pinned. Compiling the new files alone fails on every cross-module identifier by design (Unknown identifier zs_types) — expected, not a defect. The verdict that matters is the combined run, and it's only trustworthy if the dependency files are byte-for-byte the versions you actually ship — pin them by checksum. It also caught two real defects a solo check never could — the next two entries.
A missing using fails late, at the first call site. GetFortCharacter[] needs using { /Fortnite.com/Characters }; the zone file's first draft shipped without it and failed with Unknown member GetFortCharacter in player.
Forgetting the module: wrapper makes the whole file invisible. The zone file's first draft never wrapped its body in vs_safe_zone<public> := module: — so the module didn't exist, and every cross-file reference failed Unknown identifier vs_safe_zone, including a cascading "could not find a parent function" on the bridge's <override>, whose parent interface couldn't resolve. If a whole file's worth of names goes missing at once, check the wrapper first.
Shrink by multiplication, never division. Express zone tuning as scale fractions and update scale and radius in lockstep from the same step. These guides have no proven-safe precedent for float division inside a restricted context — multiply-only sidesteps the question entirely.
An archetype is not an if-expression value. Building a struct directly as the value of an if/else branch fails regardless of decoration — pre-declare a var, construct the struct as its own statement, set it, and return the var as the tail expression (RecordWave is the worked example).
No new persistent maps. Best-wave and zone-consequence state are plain in-memory maps on purpose. Persistent progress belongs as one field on your project's single player-data envelope — the pattern the farming module documents — never a fresh persistent map a reusable mode declares for itself.
Get the complete code — free
You've read the full walkthrough. The complete, copy-paste-ready Verse solution is free for members — sign in to unlock it.
Free with your BrainDead.TV / BrainDeadGuild Discord account. The walkthrough above is always free.
Turn this into a guided course
Add The first game-mode capstone in this curriculum: a complete, playable endless-survival mode composed from the already-shipped zombie-survival wave engine and loot pipeline, plus exactly one new reusable mechanic — a scene-graph shrinking safe zone with a multiply-only scale step and a consequence seam — wired into the engine's existing team-lives pressure through its first real ReportDown caller, and verified as one combined, dependency-pinned unit on the real UEFN compiler. to your free study plan — we'll suggest related pages and stitch the lot into one compile-checked, self-guided lesson with worked examples and quizzes.
Original guide generated by Verse Island from the Verse/UEFN knowledge base, with references to the Epic Games sources above. Code is validated against the knowledge base.