Events and timers

An event is something the game tells your script about. You give it a function, the game calls it at the right moment. All of this is in the w4/event package.

Register in main

The game runs main once, before the map starts. That is where you register. At that point the map has no units yet, so do not look for any in main itself. Do it in MapInit.

package main

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

func main() {
	event.OnMapInit(func() {
		game.Print("the map started")
	})
	event.OnUnitDied(func(u game.Unit) {
		game.Printf("unit %d died", u.ID())
	})
	event.Every(30, func() {
		game.Printf("%.0f seconds in", game.Time())
	})
}

An init function works as well as main. If you have several files, each can register its own handlers in its own init.

The events

Call When it fires Your function
event.OnMapInit Once, on the first step of the game func()
event.OnUnitDied For every unit that dies, whoever owns it func(game.Unit)
event.OnUnitDamaged Whenever a unit loses health, from a blow or from Unit.Damage func(unit, attacker game.Unit, amount float64)
event.OnEnter(region, fn) A unit walks into the region func(game.Unit)
event.OnLeave(region, fn) A unit leaves the region, or dies in it func(game.Unit)
event.Every(seconds, fn) Every seconds of game time func()
event.After(seconds, fn) Once, seconds of game time from now func()

You can register as many handlers as you like for one event. They run in the order you registered them.

event.Register(kind, fn) is the same thing with the event as a value: event.Register(event.MapInit, func() {...}) or event.Register(event.UnitDied, func(u game.Unit) {...}). If the function has the wrong shape, nothing is registered and the log says so. The On... functions are checked by the compiler, so you will usually want those.

Hooks for everything the game does, and handles

Every On... function returns an event.Handle. You can ignore it (a bare call still works, so older scripts are unchanged) or keep it to switch the hook off and on later:

var once event.Handle
once = event.OnLevelUp(func(hero game.Unit) {
	game.Printf("first level-up: %v", hero)
	once.Remove() // a one-shot
})

h := event.OnDamage(func(e event.DamageEvent) { game.Printf("%d magic", e.Magic) })
event.After(30, h.Disable) // stop listening after half a minute

Disable, Enable, Remove and Enabled take effect at once: a hook that an earlier hook of the same event switched off does not run for that event. A removed hook is gone for good. A handle is a small number that is never reused in a game, and the zero event.Handle does nothing.

Besides the events in the table, these report what the simulation does, one call per event, in the order the events happened, after the death, damage and region hooks of the same step and before the timers: OnShot, OnHit, OnDamage (the physical, magic and true parts of one blow and whether it was critical), OnMiss, OnImmune, OnBlocked, OnHealed, OnManaChanged, OnXpGained, OnStunned, OnLevelUp, OnAbilityCast, OnSpawned, OnBuilt, OnTrained, OnUpgraded, OnLoaded, OnUnloaded, OnGoldGained, OnStat, OnWardPlaced, OnWardExpired, OnWardDestroyed, OnWardRevealed, OnUtilitySwapped, OnUtilityUsed, OnUtilityRecharged, OnPlayerJoined, OnPlayerLeft, OnSeatChanged, OnPaused, OnResumed, OnUnitRevealed, OnAbilityHit, OnCastInterrupted, OnItemBought, OnItemSold, OnItemUsed, OnItemDropped and OnItemPickedUp. The Scripting API reference lists each with its event type and an example. OnMiss, OnImmune and OnBlocked are ready but nothing in the rules sends them yet. OnHealed and OnStunned come from an ability's effects (heal, stun); OnAbilityHit fires once for every unit an ability with effects reaches.

A kind of event costs nothing while no enabled hook listens to it: the game only passes on the kinds your script has hooks for, and when you disable or remove the last hook of a kind it stops sending it. The hooks are part of your script's saved state, so a checkpoint, a save and a replay keep them exactly as they were.

Map start

OnMapInit is the place for the opening of a game: spawn the starting units, show the first interface, add the quests.

Unit died

The handler gets the unit that died. Its position and owner can still be read, Alive() is false. The event fires for every death, yours and the enemy's, so check the owner if you only care about some of them:

event.OnUnitDied(func(u game.Unit) {
	if u.Owner() == player.NeutralAggressive {
		game.Print("a creep died")
	}
})

More about reading units in Units.

Unit damaged

The handler gets the unit, who struck it and how much health it lost. A blow that was more than the unit had left counts only what it had. When nobody struck (Unit.Damage from a script), the attacker is a unit that is not alive, so check attacker.Alive() before you use it. A unit that dies of a blow is heard here first and then in OnUnitDied.

event.OnUnitDamaged(func(u, attacker game.Unit, amount float64) {
	if attacker.Alive() && u.Health() < u.MaxHealth()/2 {
		game.Printf("%v is below half health", u)
	}
})

Regions

A region is an area the game watches for you: game.NewRegion(x, y, width, height) is a rectangle with its middle at (x, y), game.NewCircle(x, y, radius) a circle. Positions are world units from the middle of the map, like everywhere else. Make regions in MapInit or any time later, then listen with event.OnEnter and event.OnLeave:

event.OnMapInit(func() {
	gate := game.NewRegion(0, 640, 512, 256)
	event.OnEnter(gate, func(u game.Unit) {
		if u.Owner() == player.NeutralAggressive {
			game.Print("the creeps are at the gate")
		}
	})
	event.OnLeave(gate, func(u game.Unit) {
		game.Printf("%v left the gate area", u)
	})
})

Units that stand in a region when you make it do not count as entering, and a dead unit counts as outside, so a unit that dies in the region is heard in OnLeave. A region also answers questions at any time: Contains(x, y), Has(unit) and Units() for who is inside now. Remove() stops the watching. The game checks every step, and the events arrive in region order and then unit order, the same on every computer.

Timers

event.Every(seconds, fn) calls fn every seconds of game time, and the first call comes after that long. event.After(seconds, fn) calls it once. Game time is not the clock on your wall. The simulation runs 30 steps a second (event.StepsPerSecond), and the game's time stops when the game stops.

The shortest timer is one step, a thirtieth of a second. Anything shorter is rounded up to that.

A timer counts from the moment you make it, so you can start one from inside a handler, a quarter of a second after a unit died, say. Both calls give back a *event.Timer; Stop cancels it, and stopping one that already ran is fine:

func main() {
	event.OnUnitDied(func(u game.Unit) {
		event.After(3, func() {
			game.SpawnUnit("Ghoul", u.X(), u.Y(), player.NeutralAggressive)
		})
	})

	wave := event.Every(30, spawnWave)
	event.After(300, wave.Stop) // five minutes of waves
}

Where handlers run

Handlers run inside the simulation, at a fixed step, on every computer in the game. What you change through the API happens everywhere in the same way. That is why a script must not do anything that differs between machines. The rules are on Multiplayer rules.

Not there yet

Orders, regions drawn in the editor (script-made regions work today), chat and key presses are not events yet. See What is next.