Script a wave spawner

The classic custom map: waves of creeps arrive, you hold them off, the waves grow. The script is about thirty lines of Go.

You need a map with two definitions, a unit named Footman and one named Grunt (see Units from Warcraft III models), a few Footmen placed for the red player (player 1), and Go installed. Open Triggers (Ctrl 3), New script, Open in editor.

The script

package main

import (
	"w4/event"
	"w4/game"
	"w4/player"
)

var (
	wave  = 0
	kills = 0
)

func spawnWave() {
	wave++
	count := 2 + wave*2
	for i := 0; i < count; i++ {
		// Creeps belong to player.NeutralAggressive: they fight everyone.
		x := 2560 + float64(i%6)*192
		y := 2560 + float64(i/6)*192
		grunt := game.SpawnUnit("Grunt", x, y, player.NeutralAggressive)
		grunt.Move(0, 0) // Towards the middle of the map, where the player's units are.
	}
	game.Printf("Wave %d: %d grunts", wave, count)
}

func main() {
	event.OnMapInit(func() {
		game.Print("Hold the line. The first wave comes in 20 seconds.")
	})

	event.Every(20, spawnWave)

	event.OnUnitDied(func(u game.Unit) {
		if u.Owner() == player.NeutralAggressive {
			kills++
			game.Printf("Kills: %d", kills)
		}
	})
}

Press Play. After twenty seconds the first wave of four grunts walks in from (40, 40) towards (12, 12); every twenty seconds a bigger one follows, and every grunt you kill is counted in the log window (F12).

How it works

  • event.Every(20, spawnWave) calls the function every twenty seconds of game time. Game time runs at the game's speed and stops with the game. Use it and not the wall clock, so every computer agrees (see Multiplayer rules).
  • game.SpawnUnit names a definition from the Objects module. With a typo in the name there is no unit, and the log window (F12) says "there is no definition named ..." instead of the wave quietly not coming.
  • player.NeutralAggressive is the aggressive neutral owner: its units attack every player. player.Neutral owns the passive neutrals. The twelve players are player.Red, player.Blue and so on (Players and owners).
  • u.Move(x, y) walks the unit to a point (world units from the middle of the map). Grunts that meet a Footman on the way fight, because they are enemies; an idle hostile unit attacks what comes in sight.
  • event.OnUnitDied fires for every death, your Footmen included, so the handler checks the owner.

Make it a game

A score on the screen. Replace the Printf in the death handler with a line of text in the top right corner. It replaces itself every time because the id is the same. Add "fmt" and "w4/ui" to the imports (w4/player is there already):

ui.Text(player.Everyone, "kills", fmt.Sprintf("Kills: %d", kills), ui.TopRight, 24, 24, ui.Size(22))

A quest entry. In OnMapInit, ui.Quest(player.Everyone, "hold", "Hold the line", "Survive 10 waves"), and when wave == 10 in spawnWave, ui.QuestDone("hold"). The quest panel at the right of the screen shows it, green when done.

Lose condition. In the death handler, after a Footman dies, count the player's living units:

alive := 0
for _, v := range game.Units() {
	if v.Owner() == player.Red && v.Alive() {
		alive++
	}
}
if alive == 0 {
	ui.QuestFailed("hold")
	game.Print("The line is broken.")
}

A command to cheat with. game.Command("wave", "", "Sends the next wave now", func(p player.ID, args string) { spawnWave() }) adds /wave to the chat; it is listed by /help and completed with Tab.

The whole API is three short pages: package event, package game, package ui. The scripting section explains how they fit together, starting with Events and timers.