Skip to content

Players ​

Players look different depending on which side you're scripting:

  • Serverside, each player-scoped event hands you a Player object for the player involved. The server has authority, so it can warp players, grant weapons, check staff rights and write any flag.
  • Clientside, the global player is always the local player, the one running the script. Other players appear only as read-only ChatPlayer snapshots in events like onPlayerChats.

Serverside: the Player object ​

You get a Player as the first argument of onPlayerJoined, onPlayerLeft, onActionServerSide, and player-carrying NPC events (onPlayerEnters, onPlayerChats, onAction<Name>).

Identity ​

MemberNotes
idServer-assigned, unique per login. A relog gets a new id
accountThe account name. Stable, so use it as a key for anything persistent
nickDisplay name under the player, read/write. Defaults to the account. Assign '' to reset. Sanitized like chat and trimmed to 40 characters

Position and level (snapshots) ​

x, y (tiles) and level are captured when the event fired. They don't update while your handler runs, and a Player kept across an await or in a timer may be stale.

  • player.level is a ServerLevel object (or null before the first level). Use player.level.name for the file name, or act on the level directly (player.level.putnpc(...), .shoot(...), .onwall(...)). On a gmap, it's the gmap, and x/y are gmap-global. Use gmaptolevel to find the member level. See Levels & gmaps.
  • Scripts can't set x/y directly. The typings don't mark them readonly, but the host has no setter. To move a player, warp them.

Warping ​

player.warpto(level, x, y) is the only way for scripts to move a player to another level, or to reposition them within one. There's no clientside warp. It follows the same path as link touches and RC warps: warping to a gmap member level lands on the gmap with translated coordinates, and the client switches over once it has downloaded the destination. It returns false if the level doesn't exist.

ts
// weapons/travel.ts: a client asks to go home; the server decides.
function onActionServerSide(player: Player, action: string) {
    if (action === 'home')
        player.warpto(serverOptions.startLevel, serverOptions.startX, serverOptions.startY)
}

Appearance and animation (live) ​

These read and write the player's current state. Writes apply immediately for everyone in their level:

MemberNotes
chatThe chat bubble. Assigning shows it to everyone in the level, the player included. '' clears it. It fades 5 s after the player moves. See Chat & PMs
ani / setAni(name)Current .gan animation name, no extension. player.setAni('sword') is the same as assigning ani
head, bodyImage names like 'head0.png'. '' means the client default. Persisted
colorsFive 'r,g,b' strings: [0] skin, [1] coat, [2] sleeves, [3] shoes, [4] belt. Assign one slot (player.colors[1] = '255,0,0') or an array. Invalid values are ignored. Persisted
ts
function onPlayerJoined(player: Player) {
    if (player.account === 'Astram') {
        player.colors[1] = '200,40,40'   // red coat for the admin
        player.chat = 'The admin has arrived'
    }
}

Weapons ​

addWeapon, removeWeapon and hasWeapon manage the player's weapon grants, which are persisted in their account. See Weapons.

Flags ​

player.client and player.clientr are the player's flag bags. Both are persisted in the account file. client is also writable by the player's own client, and clientr is server-write-only. See Flags.

Staff rights ​

player.hasright(mode, path) checks this player's folder rights on a data/-relative path ('r', 'w' or 'rw'). Use it to gate staff-only chat commands or tools in-game:

ts
function onActionServerSide(player: Player, action: string, level: string) {
    if (action === 'resetlevel' && !player.hasright('w', 'levels/' + level)) return
    // ...
}

Staff-list accounts without a rights record pass every check. Everyone else without a record fails. See Staff rights & server options.

Clientside: the local player ​

The global player is the player running the script. Reads are live.

MemberAccessNotes
id, name, accountreadname and account are both the account name
nickreadChange it with the built-in setnick <name> chat command, or from a server script. Your own name tag is hidden, and showname flashes it for 5 s
levelreadThe current level (or gmap) name as a string, not an object
x, yread/writeTiles. Writes are batched and clamped to the level bounds when they apply
dirread/write'up' | 'down' | 'left' | 'right'
chatread/writeAssigning shows the bubble locally and sends it to everyone in the level
head, body, colorsread/writeLike the server's, synced to everyone and saved to the account. Invalid names are dropped
anireadThe gani actually playing, including replacements (see below)
replacedAnisreadCurrent replaceAni table, e.g. { idle: 'handgun-idle' }
hasWeapon(name)callWhether the server has granted the weapon (true even while it downloads)

Assigning anything else throws. Writes to x/y/dir/chat/head/body/colors are batched. You read your own writes back immediately, and many writes in one handler fold into one update.

Moving the player by script usually goes with turning off the built-in arrow-key movement. See Camera, input & movement.

Animations: setAni, replaceAni, clearAnis ​

setAni(name) plays a .gan on the local player, and the server relays it to everyone else in the level. Re-setting the current animation restarts it, unless it's a continuous one like the default walk. When a non-looping animation ends, its SETBACKTO chain (usually back to idle) resumes the built-in idle/walk switching.

ts
function onKeyPressed(key: string) {
    if (key === 'S') setAni('sword')
}

replaceAni(from, to) swaps a default animation for this player. Wherever from would play (setAni(from), the built-in idle/walk switching, a SETBACKTO, a server-set animation), to plays instead, and everyone else sees to. This is how a gun changes your idle and walk poses:

ts
function onEquipped() {
    replaceAni('idle', 'handgun-idle')
    replaceAni('walk', 'handgun-walk')
}

function onUnequipped() {
    clearAnis('idle', 'walk')   // or clearAnis() to reset every replacement
}

Rules worth knowing:

  • Replacements are one level deep and don't chain. replaceAni(from, from) removes one.
  • They last until clearAnis or the end of the session. Removing the weapon that set them doesn't undo them, so clean up yourself.
  • replaceAni/clearAnis are direct calls. player.replacedAnis reflects them immediately. The visible switch happens on the next frame.
  • player.ani reports the gani actually playing. With idle replaced, it reads 'handgun-idle', not 'idle'. Compare against the logical name like this:
ts
const isIdle = player.ani === (player.replacedAnis.idle ?? 'idle')

On the server, player.setAni / player.ani = ... plays an animation on any player. On the local player it goes through the same replacement table.

Other players on the client ​

Clientside scripts don't get a list of other players or objects to control them. What you get:

  • Event snapshots. onPlayerChats(player, chat) hands you a frozen ChatPlayer (id, name, nick, x, y, dir, chat). Compare who.id === player.id to spot yourself. Clientside NPC onPlayerEnters gets the same shape.
  • Server-published data. For a roster, have a server script publish it. For example, a playerlist weapon can mirror joins and leaves into a global collection every client reads.