Appearance
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
| Member | Notes |
|---|---|
id | Server-assigned, unique per login. A relog gets a new id |
account | The account name. Stable, so use it as a key for anything persistent |
nick | Display 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
nullbefore the first level). Useplayer.level.namefor the file name, or act on the level directly (player.level.putnpc(...),.shoot(...),.onwall(...)). On a gmap, it's the gmap, andx/yare gmap-global. Usegmaptolevelto find the member level. See Levels & gmaps. - Scripts can't set
x/ydirectly. The typings don't mark themreadonly, 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:
| Member | Notes |
|---|---|
chat | The 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, body | Image names like 'head0.png'. '' means the client default. Persisted |
colors | Five '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.
| Member | Access | Notes |
|---|---|---|
id, name, account | read | name and account are both the account name |
nick | read | Change 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 |
level | read | The current level (or gmap) name as a string, not an object |
x, y | read/write | Tiles. Writes are batched and clamped to the level bounds when they apply |
dir | read/write | 'up' | 'down' | 'left' | 'right' |
chat | read/write | Assigning shows the bubble locally and sends it to everyone in the level |
head, body, colors | read/write | Like the server's, synced to everyone and saved to the account. Invalid names are dropped |
ani | read | The gani actually playing, including replacements (see below) |
replacedAnis | read | Current replaceAni table, e.g. { idle: 'handgun-idle' } |
hasWeapon(name) | call | Whether 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/clearAnisare direct calls.player.replacedAnisreflects them immediately. The visible switch happens on the next frame.player.anireports 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). Comparewho.id === player.idto spot yourself. Clientside NPConPlayerEntersgets the same shape. - Server-published data. For a roster, have a server script publish it. For example, a
playerlistweapon can mirror joins and leaves into a global collection every client reads.