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

FieldTypeDescription
IdstringStable id used by startArena:id. Must be filename-safe (letters, digits, _, -).
DisplayNamestringShown in the per-player Arena HUD during the run.
DisplayColorstringHex color (e.g. #cc44cc). Reserved; not currently surfaced by the Core HUD but stored for downstream integrations.
WaveCountintNumber of waves in the run.
TimeLimitSecondsintRun-wide time limit. <= 0 → unlimited (HUD shows "unlimited").
SpawnRadiusfloatSpawn radius around the arena center (passed by the start action).
IntervalSecondsintPause between waves.
CountdownSecondsintCountdown shown before each wave starts.
BaseLevelintMob level for wave 1. Surfaced in the HUD.
LevelScalingfloatLevel increment per wave. BaseLevel + round(LevelScaling × (wave-1)).
LeashDistancefloatMax horizontal distance from arena center. Stepping outside fails the run as LEFT_ZONE. <= 0 disables the check.
CompletionActionsstring[]Core action strings fired when the run completes successfully (any action: giveitem:, elxp:, rpgxp:, title:, chat:, ...).
FailActionsstring[]Core action strings fired on any failure (player death, timeout, manual abort, leash break, disconnect).
InstanceBlackliststring[]Substrings of world ids in which the arena refuses to start.
BlockedMessagestringShown when the blacklist trips (reserved for UI integrations).
ZoneParticleIdstringParticle id sprayed at the arena boundary while ACTIVE.
ZoneParticleScalefloatScale multiplier for the boundary particle.
ModeFixed | GeneratedSelects 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} ] }
  ]
}