Haxball Extended Room works on top of the standard Haxball API, adding new features such as modulation, types and common functionalities.
-
No need to create your own command handler -- add new commands for your room with ease.
-
Players are now organized into classes. Changing someone's admin status is as simple as setting
player.admin
to true. Make players bigger or smaller changing theplayer.radius
property. The same is true for discs! -
Pretty logging system with CSS. Logs are automatically handled by the API.
-
Simplified names.
-
A convenient module system. Stop working with a single massive Javascript file -- modularize your Haxball room.
npm install haxball-extended-room
Importing (ES6):
import { Room } from "haxball-extended-room";
Rooms made with Haxball Extended Room can be either coupled with Haxball.js and run from Node.js, or compiled using a bundler into a browser JS file. Here I'll be compiling it using ESBuild.
This section will use JavaScript, but elsewhere TypeScript will be used.
First make sure you have npm installed. Create a folder and start a new project with:
npm init
Create a src folder where we'll insert our code and make a new bot.js file there. Install ESBuild:
npm install --save-exact esbuild
Install Haxball Extended Room if you haven't done it yet:
npm install haxball-extended-room
The structure of our project should look like this:
📦 project-name
┣ 📂src
┃ ┗ 📜bot.js
┣ 📂node_modules
┣ 📜package-lock.json
┗ 📜package.json
- Change your
package.json
scripts to this:
"scripts": {
"build": "esbuild ./src/bot.js --bundle --outfile=./dist/bundle.js"
}
Now we can get up and running with Haxball development. Open the bot.js file and write this:
const HER = require("haxball-extended-room");
const Room = HER.Room;
new Room({
roomName: "My room",
maxPlayers: 16,
public: false
});
Now let's compile it:
npm run build
Now you can now copy the results from dist/bundle.js
and paste them onto the Haxball Headless site.
For performance reasons, I recommended running your room with Node.js using the Haxball.js package.
First we install TypeScript and Haxball.js:
npm install typescript haxball.js
And write a main.ts file like this:
import HaxballJS from "haxball.js";
import { Room } from "haxball-extended-room";
const token = process.argv[2];
HaxballJS.then((HBInit) => {
const room = new Room({
roomName: "My room",
maxPlayers: 16,
public: false,
token
}, HBInit);
room.onRoomLink = link => console.log(link);
});
What all rooms in Haxball have in common is commands, but most of them aren't more than a bunch of if and elses. Commands in HER are real classes with many options such as:
- name, aliases, description, usage
- roles for a permission system
- decide whether delete the user's message
- advanced argument system for commands
An example of a command:
room.command({
name: "kick",
aliases: ["disconnect", "out"],
desc: "Kicks a player from the room",
usage: "leave #id reason",
roles: ["admin"],
deleteMessage: true, // this is default
func: ($: CommandExecInfo) => {
const playerID = $.arguments[0].replace("#", "").toNumber();
const reason = $.arguments[1] ?? "";
const player = room.players[arg];
if (player) player.kick(reason);
}
});
Player is a class that extends AbstractDisc -- the class from which both the Player and Disc classes are derived. Therefore, it contains options to change the player's disc such as player.radius
as well as specific player options like player.admin
and even some very helpful methods -- player.ban()
and player.kick()
summarize it pretty well.
If that weren't enough, it also contains ways by which you can get the player's IP (player.ip
)!
Some of the features are:
- get conn (
player.conn
) and IP (player.ip
) - special permission roles with
player.roles
(alsoaddRole
,hasRole
andremoveRole
methods) as well as settings with theplayer.settings
property - set a player's avatar using
player.setAvatar()
- send a private message to a player using
player.reply()
- get a player's team (
player.team
) and make them admin (player.admin = true
) - change the player's disc with many disc properties such as
radius
,xgravity
,xspeed
,cGroup
,position
and much more
Modularize your room with Module.
Modules allow for loose coupling between different elements of your room.
Modules are classes with a @Module
decorator (you may need to allow decorators in your TS compiler to make them work). The room object is passed when the module is instantiated. You can create events and commands inside a module.
Events are created with a @Event
decorator and a class method with the same name as the event. Commands are made of @ModuleCommand()
decorators and the name of the class method they precede will be the command's name.
An example of a welcome module:
import { Module, Room, createEvent, Player, Colors, ChatStyle } from "haxball-extended-room";
/**
* Sends a welcome message.
*/
@Module export class WelcomeModule {
constructor(private $: Room) {}
/** Events */
@Event onPlayerJoin(player: Player) {
this.$.send({
message: `Hey ${player.name}!`,
color: Colors.MediumSeaGreen,
style: ChatStyle.Bold
});
}
}
And a leave command module:
import { Module, Room, ModuleCommand, CommandExecInfo } from "haxball-extended-room";
/**
* Adds a good-bye command to the room.
*/
@Module export class LeaveModule {
constructor(private $: Room) {}
/** Commands */
@ModuleCommand({
aliases: ["bb", "cya", "gn"]
})
leave($: CommandExecInfo) {
$.player.kick("Bye!");
}
}
You can also create custom room events with the @CustomEvent
decorator:
@Module export class AFKModule {
constructor(private $: Room) {}
@ModuleCommand()
afk($: CommandExecInfo) {
if ($.player.settings.afk) {
$.player.settings.afk = false;
$.room.customEvents.emit('onPlayerUnAfk', $.player);
} else {
$.player.settings.afk = true;
$.room.customEvents.emit('onPlayerAfk', $.player);
}
}
@CustomEvent onPlayerAfk(player: Player) {
console.log(`${player.name} is AFK!`)
}
@CustomEvent onPlayerUnAfk(player: Player) {
console.log(`${player.name} is not AFK anymore!`)
}
}
This API should work on modern browsers like Chrome, Firefox, Safari and other browsers based on them. IE is not supported.
Check the Haxball official API documentation for more information.
This is the main class by which you can control the room, the players, the events, the map as well as add commands and modules to the room.
Once instantiated, it will be added to the window
object.
Creates the room with the Haxball's RoomConfigObject
properties. The only difference here is that noPlayer
is true by default.
const room = new Room({
roomName: "My room",
maxPlayers: 16,
public: false
});
This property is defined when instantiating the room and is read-only.
Gets the name of the room.
This property is defined when instantiating the room and is read-only.
Gets the name of the host player. This will be undefined
if the noPlayer
property is set to true
.
This property is defined when instantiating the room and is read-only.
Gets the maximum number of players that can join the room.
This property is defined when instantiating the room and is read-only.
This will be
undefined
if thegeo
property has not been set at instantiation.
Gets the Room's geolocation.
This property is defined when instantiating the room and is read-only.
This will be
undefined
if thetoken
property has not been set at instantiation.
Gets the room's token.
This property is defined when instantiating the room and is read-only.
Defaults to
true
.
Whether the room has a bot player. In Haxball's API this is false
by default, but it's recommended to set it to true
. In the Haxball Extended Room's API it's true
by default, and it's not only strongly recommended but a false
noPlayer is also deprecated. Learn more here.
Room's shared state.
This is useful if you want to share state between modules.
Example:
room.state.chatmuted = true;
room.onPlayerChat = function (player, message) {
if (room.state.chatmuted) return false;
}
Node.js event emitter for the implementation of custom events.
Example:
room.customEvents.emit('onPlayerAfk', player);
room.customEvents.on('onPlayerAfk', (player: Player) => onPlayerAFK(player));
Defaults to
true
.
Whether to use the room's own extended logger.
This is setter only.
The message a player receives when they don't have enough permissions to run a command.
This is getter only.
The list of online players.
Each player is represented by a number property in the PlayerList object, which is their ID.
When a player leaves the room, the property is deleted.
Example:
// Gets the name of the player whose ID is 1.
room.players[1].name;
// Gives admin to the player whose ID is 5.
room.players[5].admin = true;
This is getter only.
The list of discs in the current map. Excludes the players' discs.
This is getter only.
This will be
null
if no game is in progress.
The room's scores object.
This is read only.
A list of all Haxball's collision flags.
This is getter only.
This will be
null
if no game is in progress.
The ball disc.
This is getter only.
This will be
null
if no game is in progress.
The total number of discs in game.
This is getter only.
The list of commands.
To add or remove commands use room.command()
and room.removeCommand()
.
This will be
null
if not password is set at the moment.
Gets the room's password.
Lock the room with a password using room.setPassword()
.
Defaults to "!".
The prefix for the room's commands.
This is getter only.
The list of modules loaded to the room.
This is getter only.
Gets the Haxball's native room object.
This is getter only.
Whether the room is paused or not.
Adds a command to the room.
Deletes a command from the room.
Adds a module to the room.
Removes a module from the room.
Returns whether a game is in progress.
This is deprecated!
This method was intended to work with noPlayer: false
, but nowadays noPlayer: false
is not recommended anymore and is only mantained due to backwards compatibility by the Haxball API. Use send()
instead.
Unbans a player based on their past ID.
Unbans all banned players.
If a game is in progress this method does nothing.
Changes the score limit of the room.
If a game is in progress this method does nothing.
Changes the time limit of the room.
If a game is in progress this method does nothing.
Changes the room stadium.
This method combines both setCustomStadium and setDefaultStadium.
stadium
should be either an HBS map in JSON or a default stadium name.
Locks the teams.
When teams are locked players cannot move themselves unless an admin moves them.
Unlock the teams.
Changes the uniform of a team.
The team
property can be set to "all"
to set colors for both teams.
If a game is in progress this method does nothing.
Starts the game if none is in progress.
If a game is not in progress this method does nothing.
Stops the game in progress.
If a game is not in progress this method does nothing.
Pauses the game.
If a game is not in progress this method does nothing.
Unpauses the game.
Starts recording a Haxball Replay. Don't forget to call stopRecording()
to prevent a memory leak.
Returns null if no recording is in progress.
Stops the recording.
Locks the room with a password.
Clears the room password.
Forces players to solve a captcha before joining the room. Good to prevent automated attacks.
Disables the captcha requirement.
First all players listed are removed, then they are reinserted in the same order they appear in the list.
moveToTop
is whether players should be inserted at the top or at the bottom of the list.
Sends an announcement.
Example:
room.send({ message: "Hello world!" });
room.send({ message: "What a beautiful day!", color: Colors.Yellow, style: ChatStyle.Bold });
Limits the number of kicks in a period of time. Good to prevent cheating.
min
is the minimum number of logic-frames between two kicks. It is impossible to kick faster than this.
rate
is similar to min but lets players save up extra kicks to use them later depending on the value of burst.
burst
determines how many extra kicks the player is able to save up.
This is a room event and is setter only.
Event called when a player joins the room.
This is a room event and is setter only.
Event called when a player leaves the room.
This is a room event and is setter only.
Event called when a team wins the game.
This is a room event and is setter only.
Event called when a player sends a message.
If the event function returns false
the message will not be sent.
This is a room event and is setter only.
Event called when a player kicks the ball.
This is a room event and is setter only.
Event called when a team scores a goal.
This is a room event and is setter only.
byPlayer
will be null if the game is started programatically (such as theRoom.start()
method).
Event called when the a game is started.
This is a room event and is setter only.
byPlayer
will be null if the game is started programatically (such as theRoom.stop()
method).
Event called when the game is stopped.
This is a room event and is setter only.
byPlayer
will be null if the player's admin status is changed programatically (such as thePlayer.admin
property).
Event called when a player's admin status is changed.
This is a room event and is setter only.
byPlayer
will be null if the player is moved programatically (such as theplayer.team
property).
Event called when a player is moved to another team.
This is a room event and is setter only.
byPlayer
will be null if the player is kicked programatically (such as thePlayer.kick()
method).
Event called when a player has been kicked from the room.
This event is always called after the onPlayerLeave event.
Warning: bans have been moved to onPlayerBanned!
This is a room event and is setter only.
byPlayer
will be null if the player is kicked programatically (such as thePlayer.ban()
method).
Event called when a player is banned from the room.
This event is always called after the onPlayerLeave event.
This is a room event and is setter only.
Event called when the game ticks (60 times per second).
This event will not called if no game is in progress or the game is paused.
This is a room event and is setter only.
byPlayer
will be null if the game is paused programatically (such as thepause()
method).
Event called when the game is paused.
This is a room event and is setter only.
byPlayer
will be null if the game is unpaused programatically (such as thepause()
method).
Event called when the game is unpaused.
After this event there's a timer before the game is fully unpaused, to detect when the game has really resumed you can listen for the first Room.onGameTick
event after this event is called.
This is a room event and is setter only.
Event called when the discs' positions are reset after a goal.
This is a room event and is setter only.
Event called when a player gives signs of activity, such as pressing a key.
This is useful for detecting inactive players.
This is a room event and is setter only.
byPlayer
will be null if the stadium is changed programatically (such as thesetStadium()
method).
Event called when the stadium is changed.
This is a room event and is setter only.
Event called when the room link is obtained.
This is a room event and is setter only.
Event called when the kick rate is set.
This class represents a player in the room. You can get a list of all online players with room.players
. To get a specific player, use room.players[playerID]
.
Players are discs, so all discs properties (except Disc.color
and Disc.index
) also applies to players. When the player is not in a game, the disc properties will return undefined.
This property is read-only.
Gets the player's name.
This property is read-only.
Gets the player's ID. Each time a player joins the room, a new ID will be assigned. IDs never change.
This property is read-only.
Can be null if the ID validation fails.
The player's public ID. Players can view their own IDs here.
The public ID is useful to validate whether a player is who they claim to be, but can't be used to verify whether that player isn't someone else.
Which means it's useful for implementing user accounts, but not useful for implementing a banning system.
This property is read-only.
A string that uniquely identifies the player's connection.
If two players are in the same network then they will have equal conns.
This property is based on the player's IP. A decoded version with the full IP can be found in the ip
property.
This property is read-only.
The player's IP, decoded from the conn
property.
Player custom settings.
This is useful if you want to create your own properties for your players.
Example:
// Creates a prefix for your players' messages
room.onPlayerJoin = function (player) {
// Set up prefix setting
player.settings.prefix = "[Top 1]";
}
room.onPlayerChat = function (player, message) {
// Override the player's message with the new prefix setting
room.send({ message: `${player.settings.prefix} ${player.name}: ${message}` });
return false;
}
Overrides the player's avatar.
Removes the overrider for the player's avatar.
Kicks the player from the room.
Bans the player from joining again.
Haxball bans are IP bans, so if the player changes their IP, they will be able to join the room again.
Sends a private message to the player.
Checks whether a player is in a kickable distance relative to the specified disc.
Attaches a new role to the player.
Removes a player's role.
Checks whether a player has the specified role.
This is getter only.
The player's roles.
Roles are used as a permission system by commands.
If a command has been defined with a certain role, it'll check whether the player has it too.
The "admin" role is restricted and will be automatically assigned to players with admin status.
The player's team.
The player's admin status.
The player's position on the map.
The player's tag (name #id
).
The player's mention (@player
).
This class represents a disc on the map. You can get all available discs using Room.discs
.
Properties may return number | null | undefined
depending on whether the disc is on the map at the moment. Generally, if they're not on the map they will return undefined
.
Players are discs, so all discs properties (except Disc.color
and Disc.index
) also applies to players. When the player is not in a game, the disc properties will return undefined.
The x coordinate of the disc's position.
The y coordinate of the disc's position.
The x coordinate of the disc's speed vector.
The y coordinate of the disc's speed vector.
The x coordinate of the disc's gravity vector.
The y coordinate of the disc's gravity vector.
The disc's radius.
The disc's bouncing coefficient.
The inverse of the disc's mass.
The disc's damping factor.
The disc's collision mask.
Represents which groups the disc can collide with.
The disc's collision groups.
The disc's color.
Set the value to -1 to make the disc transparent.
Returns null if one of the discs is not in the game.
The distance between two discs.
Whether two discs are colliding.
Same as Player.settings
.
A class that represents a command. You can add a command to the room using Room.command()
and remove it with Room.removeCommand()
. To get all commands on the room use Room.commands
.
This property is read-only.
Gets the command's name.
The command's aliases.
The permission roles.
If a player doesn't have all the specified roles, they will be blocked from running the command.
The command's description.
Useful for a help command.
The command's usage.
Useful for a help command or an error message.
Defaults to
true
.
Whether to delete the player's message.
The command's function.
A CommandFunc
is a function with a CommandExecInfo
parameter: (info: CommandExecInfo) => void
.
Whether the player is allowed to run this command based on their roles.
Runs the command.
A special class that works as a list of players. You can get the room's player list using Room.players
.
Every property with a number as a key is a Player. The key is their ID: [id: number]: Player
. When a player leaves the property is deleted.
You can also iterate over this object.
How many players are on the list.
Adds a player to the list.
Removes a player from the list.
player
can be a Player, a native PlayerObject or a player ID.
Gets a player from the list.
If you're using this to find players by their ID, consider using players[id]
instead.
predicate
can be a player ID or a function.
Gets multiple players based on a filtering function.
Gets an array with all players on the list.
Gets the players according to their order in the native getPlayerList()
method.
Gets the first player on the list.
Gets the last player on the list.
Gets a player by their name.
Gets a player by their public ID.
Gets players by their conn or IP.
Kicks all players on the list.
Bans all players on the list.
Gets the room's spectators.
Gets the red team's players.
Gets the blue team's players.
Gets the red and blue teams' players.
Gets all players with admin status.
Sends a private message to all players on the list.
String representation of a PlayerList.
Modules are classes that contains a @Module
decorator.
When a module is loaded using Room.module()
, three arguments are passed in the constructor: the room object, a ModuleSettings object and a Translator function.
Inside modules you can create new commands using the @ModuleCommand()
decorator and assign new events with the @Event
decorator.
This should be passed as the second parameter of the Room.module()
method.
It can contain two properties:
Some settings you want the module to have. For example, an AFK command module could be called like this:
room.module(AFKModule, {
settings: {
blockAFKInGame: true,
}
});
And in the AFK command:
@ModuleCommand({
usage: "afk"
})
afk($: CommandExecInfo): void {
if ($.room.state.blockAFKInGame && $.room.isGameInProgress() && $.player.team !== Teams.Spectators) {
return $.player.reply({ message: "You can't be AFK mid game!" });
}
}
ModuleSettings
is defined as [setting: string]: string | boolean | number | {}
.
An option to translate a module's messages. Modules that have a Translator function will replace the original string by a languagePack's property value if the property key matches the Translator name parameter.
For example:
Inside the module:
$.room.send({ message: this.translate("%% is not AFK anymore!", "UN_AFK", $.player.name) });
When calling the Room.module()
method:
room.module(AFKModule, {
languagePack: {
"UN_AFK": "%% ya no es AFK!" // Now Spanish speakers will be able to understand our module!
}
});
(original: string, name: string, ...params: string[]) => string
A function passed as a argument to the module's constructor.
With this you can create multilingual module.
An example:
this.$.send({ message: this.translate("%% scored a goal! %% - %%", "GOAL_SCORED", player.name, scores.red, scores.blue) });
Decorators are used to declare metadata information. There are three special decorators we can use to create modules:
Declares that a class is a module.
@Module class AFKModule { }
Transforms a Module class' method into a command. The method's name is the command's name and the method itself is the func property.
@ModuleCommand()
help($: CommandExecInfo): void {}
@ModuleCommand({
usage: "afk"
desc: "Becomes AFK."
})
help($: CommandExecInfo): void {}
The room event whose name matches a method's name with this decorator will execute that method. Methods are always executed before events defined in the Room object.
@Event onPlayerJoin () {
this.updateAdmins();
}
A custom event that can be called with room.customEvents.emit()
.
@CustomEvent onPlayerAfk(player: Player) {
console.log(`${player.name} is AFK!`);
}
Bot.ts
import { Room } from "haxball-extended-room";
import { AFKModule } from "AFKModule";
const room = new Room({
roomName: "My room",
maxPlayers: 16
});
room.module(AFKModule);
AFKModule.ts
import { Module, Room, ModuleSettings, Translator, Command, CommandExecInfo, Teams, Event, Player } from "haxball-extended-room";
@Module export class AFKModule {
constructor(private $: Room, private settings: ModuleSettings, private translate: Translator) {}
/** Commands */
@ModuleCommand({
usage: "afk"
})
afk($: CommandExecInfo): void {
if ($.room.state.blockAFKInGame && $.room.isGameInProgress() && $.player.team !== Teams.Spectators) {
return $.player.reply({ message: this.translate("You cannot be AFK mid-game!", "AFK_ERR_1") });
}
if ($.player.settings.afk) {
$.player.settings.afk = false;
$.room.send({ message: this.translate("%% is not AFK anymore!", "UN_AFK", $.player.name) });
$.room.customEvents.emit('onPlayerUnAfk', $.player);
} else {
if ($.room.state.afkable === false) {
return $.player.reply({ message: this.translate("You cannot be AFK now!", "AFK_ERR_3") });
}
$.player.settings.afk = true;
$.room.send({ message: this.translate("%% is now AFK!", "AFK", $.player.name) });
$.room.customEvents.emit('onPlayerAfk', $.player);
if ($.player.team !== Teams.Spectators) $.player.team = Teams.Spectators;
}
}
/** Events */
@Event onPlayerTeamChange(changedPlayer: Player) {
if (changedPlayer.team !== Teams.Spectators) {
if (changedPlayer.settings.afk) {
changedPlayer.team = Teams.Spectators;
this.$.send({ message: this.translate("%% is AFK and cannot be moved!", "AFK_ERR_2", changedPlayer.name) });
}
}
}
}
The command's name.
The command's aliases.
The command's description. Useful for a help command.
The command's usage. Useful for a help command or an error message.
The permission roles.
If a player doesn't have all the specified roles, they will be blocked from running the command.
Whether to delete the player's message.
The command's function.
The player who ran the command.
The player's message.
The room object.
When the command was executed.
A list of arguments passed to the command.
RegExp:
/^\d+$/
Whether it is a valid number.
RegExp:
/^(y(es)?|n(o)?)/i
Whether it is a "yes" or a "no".
RegExp:
/^[a-zA-Z0-9_@.!*$?&%-]{1,16}$/i
Whether it is a valid password string according to our standards.
RegExp:
/^[a-zA-Z0-9\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u024F]*$/i
Whether it is alphanumeric (with * and $ as exceptions).
RegExp:
/^[a-zA-Z0-9\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u024F_@.!*$?&%-]*$/i
Includes @.!?&%- to the extended option.
Parses the argument to a number.
Returns the argument itself.
A message to be sent.
The ID of the player this message is directed to. If undefined then it will be sent to every player in the room.
The color of the message.
Preferably this should be a color integer. For example, 0xFFFFFF
(which returns 16777215
) is the white color.
The style of the message. It can be "normal"
, "bold"
, "italic"
, "small"
, "small-bold"
or "small-italic"
.
Default is "normal"
.
The sound the message will produce. 0
is none, 1
is normal and 2
is notification.
The colors of a team.
This site may help you with team colors.
The angle of the stripes.
The color of the player's avatar.
The colors of the stripes. It can contain up to 3 colors, which correspond to 3 stripes.
Helpful enums for Haxball development.
These are also available on the window object.
Red, Blue and Spectators teams.
Haxball's default stadiums.
A list of sounds for room announcements.
A list of styles for room announcements.
A huge list of colors.