dot-npc-ai-director decides how many NPCs there are and where they arrive,
over a build-up / sustain / fade / relax cycle.
Requires dot-core and dot-npc, and deliberately
not dot-npc-ai: this decides how many and
where, and what each one does when it gets there is a brain’s business.
Usage
var director := DotNpcDirector.new()
director.spawner = npcs # a DotNpcSpawner
director.rules = DotNpcDirectorRules.new()
director.population = [&"zombie", &"zombie", &"zombie", &"brute"] # 1 brute in 4
director.spawn_points = nav.points # or the map's own list
director.flow = DotNpcDirectorFlow.new(map.critical_path)
add_child(director)
# Every simulated tick:
for player in players:
director.report_player(player.id, player.position, player.health / player.max_health)
director.tick(delta)
Health arrives as a fraction, not a damage number: a game that had to report damage would have to remember to do it from every damage path it has, and the one it forgets is the one that matters.
Why it is not a spawn timer
A timer gives every player the same game whether they are winning or losing. This is driven by what the players are experiencing — damage taken, enemies close, time without a break — so a team that is struggling gets a moment to recover and a team that is coasting gets something to do. The pacing needs no difficulty setting to do that.
On top of it, three opt-in layers (API 3, all off by default in DotNpcDirectorRules): a per-player skill estimate (skill_enabled, read back with skill_multiplier(id), fed by report_death, report_revive and report_kill), a difficulty dial or preset (difficulty_enabled; easy, normal, hard, nightmare) whose enemy_accuracy_multiplier() and enemy_damage_multiplier() the game applies to its own enemies, and a cooldown budget on special kinds (specials_enabled). The director knows no combat addon; it only publishes the numbers.
The relax needs both a clock and a distance
Time alone lets a team that has stopped to argue about a door be attacked on schedule. Distance alone lets a team that is sprinting be attacked every few seconds. And the distance is the least-travelled player’s, because one person scouting ahead has not moved the party anywhere.
A schedule for a hold-out
The stress cycle paces a team that moves; its relax waits for distance, which a team holding a place never travels. DotNpcDirectorWaves (API 2) is the other half, a fixed schedule in one of three styles a server owner picks:
| Style | Shape |
|---|---|
ROUNDS |
Quiet, wave 1, quiet, wave 2 and so on. Each wave is a count of enemies, cleared by killing them, and bigger than the last. |
CYCLE |
Day, night 1, day, night 2. A night is a length of time, survived rather than cleared; daylight() gives a ramp for the sky. |
CLOCK |
One stretch, harder as it goes; 0 seconds lasts as long as anybody does. |
It spawns nothing and owns nothing. The game calls begin(players) and tick(delta, alive, players), asks take(room) for how many of the wave to spawn now and give_back(n) for spawns it refused, and reads strength() for how hard this stretch is. A wave is released a few at a time at release_rate(), which rises with the wave, so wave 1 trickles in and wave 10 arrives as a flood instead of a hundred bodies in one frame at one door. stage_changed(stage, index), finished() and, when a wave set (API 4, a waves profile) resolves each wave, wave_started(spec) are the signals.
A ROUNDS wave ends when all of it is released and none is alive. Two backstops stop one body stuck behind a crate holding the breather off for ever: it ends overtime_seconds after its last release, and sooner, straggler_seconds (30) after the last release, once no more than straggler_count (3) are left. The stage change says so through overtime; what to do with the stragglers is the game’s call.
