Wave Arenas
Server-defined wave arenas driven directly by the QuestLines Core engine
(net.evilcraft.questlines.arena.WaveArenaEngine). No optional dependency.
Trigger from quests, track progress with named trackers, fire rewards via core action
lists. Stored as JSON in mods/QuestLines/wavearenas/ and authored via
/ql waveeditor.
How It Works
The engine drives a per-player phase machine (COUNTDOWN → SPAWNING → ACTIVE → WAVE_CLEAR → INTERVAL → COMPLETED, with FAILED reachable from any phase) and spawns NPCs around the arena center on the world thread. Player death, timeout, leash break, manual abort, and disconnect all transition to FAILED with the relevant reason. Sessions are ephemeral — they do not survive a server restart.
Two modes are supported:
- Fixed — explicit per-wave mob lists. Wave 1 spawns these mobs, wave 2 spawns those, and so on.
- Generated — random rolls from a weighted mob pool. Wave size
scales by
BaseCount×CountScaling(wave-1), and every Nth wave can be promoted to a boss wave drawn from boss-tagged pool entries.
The editor never stores the arena center — that's passed by the quest that
starts the arena (see the startArena action below). The same arena can be
reused from many quests, each handing off a different center point.
Editor
Open the editor with /ql waveeditor (alias /ql we). Requires
questlines.admin. The left column lists every arena and exposes ADD /
DUPLICATE / DELETE; the right column edits the currently selected arena.
Mob entries are picked through a category dropdown plus a searchable NPC-type dropdown
backed by a bundled NPC catalog (340+ ids across 22 categories: Beast, Boss, Critter,
Elemental, Flying_Beast, Flying_Critter, Flying_Wildlife, Human, Intelligent, Livestock,
Pets, Scifi, Swimming_Beast, Swimming_Critter, Swimming_Wildlife, Undead, Void,
Wildlife, and a few showcase / test groups). Any NPC type id is accepted —
including types your server registers beyond the bundled catalog — but unknown
ids produce a validation warning on /ql reload.
Arena Fields
| Field | Type | Description |
|---|---|---|
Id | string | Stable id used by startArena:id. Must be filename-safe (letters, digits, _, -). |
DisplayName | string | Shown in the per-player Arena HUD during the run. |
DisplayColor | string | Hex color (e.g. #cc44cc). Reserved; not currently surfaced by the Core HUD but stored for downstream integrations. |
WaveCount | int | Number of waves in the run. |
TimeLimitSeconds | int | Run-wide time limit. <= 0 → unlimited (HUD shows "unlimited"). |
SpawnRadius | float | Spawn radius around the arena center (passed by the start action). |
IntervalSeconds | int | Pause between waves. |
CountdownSeconds | int | Countdown shown before each wave starts. |
BaseLevel | int | Mob level for wave 1. Surfaced in the HUD. |
LevelScaling | float | Level increment per wave. BaseLevel + round(LevelScaling × (wave-1)). |
LeashDistance | float | Max horizontal distance from arena center. Stepping outside fails the run as LEFT_ZONE. <= 0 disables the check. |
CompletionActions | string[] | Core action strings fired when the run completes successfully (any action: giveitem:, elxp:, rpgxp:, title:, chat:, ...). |
FailActions | string[] | Core action strings fired on any failure (player death, timeout, manual abort, leash break, disconnect). |
InstanceBlacklist | string[] | Substrings of world ids in which the arena refuses to start. |
BlockedMessage | string | Shown when the blacklist trips (reserved for UI integrations). |
ZoneParticleId | string | Particle id sprayed at the arena boundary while ACTIVE. |
ZoneParticleScale | float | Scale multiplier for the boundary particle. |
Mode | Fixed | Generated | Selects which set of mode-specific fields the engine consumes. |
Fixed Mode
Each wave is an explicit list of (NpcTypeId, Count) entries. Use ADD WAVE
to grow the wave list, select a wave, then add mobs from the entity picker. WaveCount
is independent of the number of authored waves — if you author fewer waves than
WaveCount, the validator warns and the extras don't play.
Generated Mode
Each pool entry is (NpcTypeId, Weight, MinWave, MaxWave, Boss). Wave
size grows with BaseCount × CountScalingwave-1, drawn
from non-Boss entries. When BossEveryN > 0, every Nth wave
additionally rolls one Boss-tagged entry that spawns alongside the regular mobs. If
no Boss entry is eligible (empty Boss pool, or all gated out by MinWave/MaxWave),
the wave is just the regular roll. When BossEveryN = 0 the Boss flag is
ignored and all entries roll together on every wave. MinWave gates an
entry from below — e.g. an Alpha_Rex with MinWave = 9 won't
roll until wave 9. MaxWave gates from above — e.g. a low-level
Grunt with MaxWave = 5 stops rolling after wave 5.
MaxWave = 0 means no upper bound.
Triggering from a Quest
Use the startArena action on any response or page action list:
startArena:shadow_trial // arena centered on the player
startArena:shadow_trial:120:64:-340 // explicit center (supports ~-relative)
failArena // abort the player's current arena
Track progress via named trackers fed by the engine:
track:wavesCleared:arena_wave:shadow_trial // increment on every wave clear
track:runsDone:arena_complete:shadow_trial // increment on full completion
{track:runsDone} runs cleared // text variable for objectives
Reload Behavior
The engine is a single instance owned by QuestLinesPlugin and reads
arena configs from DirectoryWaveArenaConfig on demand at
startArena time — there is no separate registration step. Edits made
via the editor go live the moment they're saved. /ql reload rebuilds
the in-memory config; in-flight sessions are unaffected.
Validation runs on every reload and on /ql validate. Errors are logged to
console; the reload itself never blocks on validation failure.
Example File
// mods/QuestLines/wavearenas/shadow_trial.json
{
"Id": "shadow_trial",
"DisplayName": "Shadow Trial",
"DisplayColor": "#cc44cc",
"WaveCount": 5,
"TimeLimitSeconds": 300,
"SpawnRadius": 8.0,
"IntervalSeconds": 10,
"CountdownSeconds": 3,
"InstanceBlacklist": ["instance-"],
"BlockedMessage": "You cannot start a Shadow Trial inside a dungeon.",
"ZoneParticleId": "",
"ZoneParticleScale": 0.0,
"BaseLevel": 3,
"LevelScaling": 0.5,
"LeashDistance": 24.0,
"CompletionActions": [
"giveitem:Coin:200",
"elxp:give:1500",
"chat:&aShadow Trial cleared! Reward sent."
],
"FailActions": [
"chat:&cThe shadows reclaim you. Try again."
],
"Mode": "Fixed",
"Waves": [
{ "Entries": [ {"NpcTypeId": "Goblin_Scrapper", "Count": 3}, {"NpcTypeId": "Skeleton", "Count": 2} ] },
{ "Entries": [ {"NpcTypeId": "Goblin_Lobber", "Count": 4}, {"NpcTypeId": "Skeleton_Giant", "Count": 1} ] },
{ "Entries": [ {"NpcTypeId": "Zombie_Aberrant", "Count": 6} ] }
]
}