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:
| Language | Connection from a Steam ID | Player from a slot |
|---|---|---|
| TypeScript | connection(steamId), with a bigint | Player.of(slot) |
| Luau | modlock.connection(steamId), with a string | modlock.Player.new(slot) |
| Python | modlock.connection(steam_id), a number above 65535 | modlock.Player(slot) |
| Go | mod.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.
| Role | Who |
|---|---|
developer | Builds the mod. |
tester | Tries 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}`)
endpawn = 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()
endbot = 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.
| Call | Effect |
|---|---|
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, pawn | Keep 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, announce | Show 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, kill | Move 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, refreshAbility | Change what the hero carries; see Abilities and items. |
player.abilities(), setAbility, replaceAbility, trainAbility, giveModifier, holdModifier | Read, change or train abilities, their timers and charges, modifiers. |
abilityBarPresses, abilityDisplay | Answer 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, cameraShake | Play 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 asnano; - the title in any case, such as
Calicoorlady 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)
}
})| Call | Returns | Languages |
|---|---|---|
heroes | The 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 |
Hook | Watches 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 |