Skip to Content
Modlock

Players and heroes

List the players, read their heroes and move them through the match.

A player is a person in a seat on the server, known by the seat's slot, and a bot is a player too. Whoever takes the seat next is a new player. A pawn is the player's hero as it is on this frame.

List the players

The player list contains every connection. Each connection has a player, a name, a Steam ID, whether it is a bot, and a generation, which changes when someone new takes the slot.

import { command, players } from 'modlock'

command('who', (player) => {
  for (const { name, bot } of players()) {
    player.chat(bot ? `${name} (bot)` : name)
  }
})
modlock.command("who", function(player)
	for _, connection in modlock.players() do
		player:chat(if connection.bot then `{connection.name} (bot)` else connection.name)
	end
end)
@modlock.command("who")
def who(player: modlock.Player, args: str) -> None:
    for connection in modlock.players():
        player.chat(f"{connection.name} (bot)" if connection.bot else connection.name)
mod.Command("who", func(p mod.Player, args string) {
	connections, _ := mod.Players()
	for _, c := range connections {
		name := c.GetName()
		if c.GetBot() {
			name += " (bot)"
		}
		_ = p.Chat(name)
	}
})

A player's connection has their name, Steam ID, whether they are a bot and whether they finished joining. player.connection() returns it, or nothing when nobody holds the slot:

import { command } from 'modlock'

command('me', (player) => {
  player.chat(player.connection()?.name ?? 'Nobody')
})
modlock.command("me", function(player)
	local connection = player:connection()
	player:chat(if connection then connection.name else "Nobody")
end)
@modlock.command("me")
def me(player: modlock.Player, args: str) -> None:
    connection = player.connection()
    player.chat(connection.name if connection is not None else "Nobody")
mod.Command("me", func(p mod.Player, args string) {
	name := "Nobody"
	if c := p.Connection(); c != nil {
		name = c.GetName()
	}
	_ = p.Chat(name)
})

You can also find a connection from a Steam ID, and make a Player from a slot:

LanguageConnection from a Steam IDPlayer from a slot
TypeScriptconnection(steamId), with a bigintPlayer.of(slot)
Luaumodlock.connection(steamId), with a stringmodlock.Player.new(slot)
Pythonmodlock.connection(steam_id), a number above 65535modlock.Player(slot)
Gomod.ConnectionOfSteamID(steamID)mod.PlayerOf(slot)

Each gives the player who holds the slot now. In Go, the zero mod.Player names nobody. The same connection call also takes a player or a slot.

Roles

A connection's roles say whether the player builds or tests the mod. Use a role only to decide what your mod shows, such as a menu still in testing. A check that guards saved data runs where the data is stored, not on a role.

RoleWho
developerBuilds the mod.
testerTries the mod's unfinished features.

modlock dev and modlock play give every local player both. A platform that hosts the mod sends its own roles over the control link, one table per mod.

Keep a value for each player

A Player works as a key in a map, table or dict, so your mod can keep a value for each player:

import { command, onPlayerLeave, type Player } from 'modlock'

const greetings = new Map<Player, number>()

command('hi', (player) => {
  const count = (greetings.get(player) ?? 0) + 1
  greetings.set(player, count)
  player.chat(`Hello ${count} times`)
})

onPlayerLeave(({ connection }) => {
  greetings.delete(connection.player)
})
local greetings: { [modlock.Player]: number } = {}

modlock.command("hi", function(player)
	local count = (greetings[player] or 0) + 1
	greetings[player] = count
	player:chat(`Hello {count} times`)
end)

modlock.onPlayerLeave(function(left)
	greetings[left.connection.player] = nil
end)
greetings: dict[modlock.Player, int] = {}


@modlock.command("hi")
def hi(player: modlock.Player, args: str) -> None:
    count = greetings.get(player, 0) + 1
    greetings[player] = count
    player.chat(f"Hello {count} times")


@modlock.on_player_leave
def left(event: modlock.PlayerLeftEvent) -> None:
    greetings.pop(event.connection.player, None)
var greetings = map[mod.Player]int{}

func init() {
	mod.Command("hi", func(p mod.Player, args string) {
		greetings[p]++
		_ = p.Chat(fmt.Sprintf("Hello %d times", greetings[p]))
	})
	mod.OnPlayerLeave(func(left *mod.PlayerLeftEvent) {
		delete(greetings, mod.PlayerOf(left.GetConnection().GetPlayer()))
	})
}

The leave handler keys by connection.player, because a connection does not work as a key: Python cannot hash one, and in the other languages the same player's connection can be a new value in each list. A person who takes a slot another left is a new Player, so they never inherit the leaver's entry. The leave arrives before the player list that drops the leaver, so connection.player still finds their entry, and deleting it keeps the map from holding everyone who ever played.

Read the hero

A pawn has the hero's health, team, position, aim and souls. No pawn exists until the hero spawns, so check for one. A hero at zero health is dead.

const pawn = player.pawn()
if (pawn !== undefined && pawn.health > 0) {
  player.centerText(`${pawn.health} / ${pawn.maxHealth}`)
}
local pawn = player:pawn()
if pawn and pawn.health > 0 then
	player:centerText(`{pawn.health} / {pawn.maxHealth}`)
end
pawn = player.pawn()
if pawn is not None and pawn.health > 0:
    player.center_text(f"{pawn.health} / {pawn.max_health}")
pawn, err := p.Pawn()
if err == nil && pawn != nil && pawn.GetHealth() > 0 {
	_ = p.CenterText(fmt.Sprintf("%d / %d", pawn.GetHealth(), pawn.GetMaxHealth()))
}

Move the hero through the match

Pick a hero and a team for a player, with any hero name. The teams are 2 and 3. The game places the selected hero on a later frame, and one call does not always seat the player. player.seat and Cast ask again until the hero appears.

player.selectHero('abrams', 2)
player.respawn()
player.teleport({ x: 0, y: 0, z: 256 }, { pitch: 0, yaw: 90, roll: 0 })
player.freeze(true)
player.adjustSouls(500)
player.heal(250)
player:selectHero("abrams", 2)
player:respawn()
player:teleport({ x = 0, y = 0, z = 256 }, { pitch = 0, yaw = 90, roll = 0 })
player:freeze(true)
player:adjustSouls(500)
player:heal(250)
player.select_hero("abrams", 2)
player.respawn()
player.teleport(modlock.Vector(0, 0, 256), modlock.Angles(0, 90, 0))
player.freeze(True)
player.adjust_souls(500)
player.heal(250)
_, _ = p.SelectHero("abrams", 2)
_ = p.Respawn()
_ = p.Teleport(&mod.Vector{Z: 256}, &mod.Angles{Yaw: 90}, nil)
_ = p.Freeze(nil) // nil freezes; pass a pointer to false to release
_ = p.AdjustSouls(500, nil)
_ = p.Heal(250)

Add bots

A bot is a player like any other, and fills a seat for testing or play.

import { addBot } from 'modlock'

const bot = addBot({ name: 'Sparring partner', team: 3 })
bot?.removeBot()
local bot = modlock.addBot({ name = "Sparring partner", team = 3 })
if bot then
	bot:removeBot()
end
bot = modlock.add_bot(modlock.BotOptions(name="Sparring partner", team=3))
if bot is not None:
    bot.remove_bot()
bot, err := mod.AddBot(&mod.BotOptions{Name: "Sparring partner", Team: 3})
if err == nil {
	_ = bot.RemoveBot()
}

Every call

Python spells each call in snake_case, and Go capitalizes it.

CallEffect
players()List the connected players and bots.
player.connection(), connection(x)One player's connection, or nothing for an empty slot.
new Cast({ join, leave, bots }), cast.frame, members, want, pawnKeep one member per human and get them the hero they want.
player.pawn()Read the hero: health, team, position, aim and stamina.
player.chat, centerText, announceShow text to one player.
player.sound(sound, { volume, pitch, listeners }), adjustSound(id, volume, pitch), stopSound(id)Play a sound on the hero, change it while it plays, or stop it.
player.selectHero, respawn, teleport, setVelocity, freeze, killMove the hero through the match.
player.zipline(lane)Put the hero on the yellow, green, blue or purple lane's zipline.
player.spectate()Move the player to the spectators, without a hero.
player.watch(target, chase?), freeCamera(position, facing), unwatch()Watch a hero, chase it or fly freely, and return to the hero.
player.moveCamera(options), clearCamera(context?)Pull back, widen, offset or aim the camera, as abilities do.
player.clearItems, giveItem(item, imbue?), buyItem(item, imbue?, source?), restoreStamina, setStamina, refreshAbilityChange what the hero carries; see Abilities and items.
player.abilities(), setAbility, replaceAbility, trainAbility, giveModifier, holdModifierRead, change or train abilities, their timers and charges, modifiers.
abilityBarPresses, abilityDisplayAnswer and redraw the ability bar; see Abilities and items.
player.setting, setSetting, settingOn(player, key), settingNumber(player, key)Read or change the player's settings.
player.addMetric(name, value, label?)Add to the player's total of a metric.
reportScore(player, board, value, options?), reportMatch(board, places, options?)Report a score or a match's places to a board.
readBoard(board, read?)Read a page of a board, such as its top five.
player.handoff(text), handedOff()Hand the player's state to the next build, and read it back.
player.watchMovement(true)Add the hero's movement, landings and wall jumps to each frame.
playCamera(player, options), cameraPulse, cameraShakePlay curves on the camera, TypeScript only; see Camera moves.
setOrbitCamera(player, enabled)Switch the hero to the hideout's zoom camera and HUD, or back.
addBot(options), player.removeBot()Add or remove a bot player.

Heroes

Every call that takes a hero, such as selectHero, addBot, precache and a Cast's want, accepts any of three names for it:

  • the game's name, such as hero_nano;
  • that name without hero_, such as nano;
  • the title in any case, such as Calico or lady geist.

A command that takes a hero from chat matches it loosely with findHero:

import { command, findHero } from 'modlock'

command('hero', (player, args) => {
  const hero = findHero(args)
  if (hero) player.selectHero(hero.name, 2)
})
modlock.command("hero", function(player, args)
	local hero = modlock.findHero(args)
	if hero then
		player:selectHero(hero.name, 2)
	end
end)
@modlock.command("hero")
def hero(player: modlock.Player, args: str) -> None:
    found = modlock.find_hero(args)
    if found is not None:
        player.select_hero(found.name, 2)
mod.Command("hero", func(p mod.Player, args string) {
	if hero, ok := mod.FindHero(args); ok {
		_, _ = p.SelectHero(hero.Name, 2)
	}
})
CallReturnsLanguages
heroesThe released heroes by title, each with its game name, title and fixed id. Python spells it HEROES.All
heroNamed(name)The hero whose name matches exactly, as the calls above read names, or nothing. A misspelled name finds no hero rather than another one.All
findHero(typed)The hero a player typed: an exact name or title first, then the first title that starts with or contains it, such as geist.All
findTitled(list, typed)The same match over your own list of entries with name and title.TypeScript
HookWatches one Bebop's hook. update(player, now) each frame reports the hero it holds and each new throw, keeps it fully upgraded and sets its cooldown.TypeScript

On this page