bonktools 2.3.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/bot.js ADDED
@@ -0,0 +1,1498 @@
1
+ /**
2
+ * Main Bot class for BonkBot
3
+ */
4
+ const EventEmitter = require('events');
5
+ const io = require('socket.io-client');
6
+ const axios = require('axios');
7
+ const https = require('https');
8
+ const {
9
+ createLogger
10
+ } = require('./utils/logger');
11
+ const {
12
+ DEFAULT_SERVER,
13
+ DEFAULT_AVATAR,
14
+ GAMEMODE_NAMES,
15
+ ENGINE_NAMES,
16
+ TEAM_NAMES,
17
+ CLIENT_MESSAGE_TYPES,
18
+ API,
19
+ SERVER_MESSAGE_TYPES
20
+ } = require('./utils/constants');
21
+ const {
22
+ parsePacket
23
+ } = require('./packet');
24
+ const { LOG_LEVELS } = require("./utils/logger");
25
+
26
+ // Create logger
27
+ const logger = createLogger('BonkBot');
28
+
29
+ // Create HTTPS agent for axios with certificate verification disabled
30
+ // NOTE: bonk.io servers use Sectigo certificates with incomplete chains
31
+ const httpsAgent = new https.Agent({
32
+ rejectUnauthorized: false
33
+ });
34
+
35
+ /**
36
+ * Main BonkBot class
37
+ */
38
+ class BonkBot {
39
+ /**
40
+ * Create a new BonkBot instance
41
+ * @param {Object} options - Bot options
42
+ * @param {Object} options.account - Account information
43
+ * @param {string} [options.account.username] - Username
44
+ * @param {string} [options.account.password] - Password
45
+ * @param {boolean} [options.account.guest=true] - Whether the account is a guest
46
+ * @param {string} [options.avatar] - Bot avatar
47
+ * @param {string} [options.server=b2ny1] - Server to connect to
48
+ * @param {string} [options.bypass] - Pass bypass
49
+ * @param {string} [options.token] - Authentication token
50
+ * @param {string} [options.peerID] - Peer ID
51
+ * @param {number} [options.logLevel=LOG_LEVELS.INFO] - Log level
52
+ */
53
+ constructor(options = {}) {
54
+ // Set log level
55
+ if (options.logLevel !== undefined) {
56
+ logger.setLevel(options.logLevel);
57
+ }
58
+
59
+ logger.info('Creating new BonkBot instance');
60
+
61
+ // Initialize properties
62
+ this.PROTOCOL_VERSION = options.PROTOCOL_VERSION
63
+ this.HARDCODED_PROTOCOL_VERSION = 49; // dont change this, pass the property to the function
64
+
65
+ if(this.PROTOCOL_VERSION == undefined){
66
+ logger.warn("You should really set the PROTOCOL_VERSION to the correct one in the createBot() options object, without it the bot may fail to start")
67
+ logger.warn("Defaulting PROTOCOL_VERSION to: " + this.HARDCODED_PROTOCOL_VERSION)
68
+ this.PROTOCOL_VERSION = this.HARDCODED_PROTOCOL_VERSION;
69
+ }
70
+
71
+ this.account = this.validateAccount(options.account || {});
72
+ this.avatar = options.avatar || DEFAULT_AVATAR;
73
+ this.server = options.server || DEFAULT_SERVER;
74
+ this.bypass = options.bypass;
75
+ this.token = options.token;
76
+ this.peerID = options.peerID || this.generatePeerId();
77
+
78
+
79
+ // Create event emitter
80
+ this.events = new EventEmitter();
81
+
82
+ // Socket connection
83
+ this.socket = null;
84
+ this.connected = false;
85
+ this.keepAliveTimer = null;
86
+ this.timeSyncCount = 1;
87
+
88
+ this.location = {
89
+ lat: 0,
90
+ long: 0,
91
+ country: "US",
92
+ server: "b2ny1"
93
+ }
94
+
95
+ this.timeSync = {
96
+ count: 0,
97
+ last_sync: 0,
98
+ last_sync_id: 0,
99
+ latency: 0
100
+ }
101
+
102
+ // Room information
103
+ this.room = {
104
+ id: null,
105
+ name: null,
106
+ address: null,
107
+ server: this.server,
108
+ bypass: this.bypass,
109
+ teamsLocked: false,
110
+ password: null,
111
+ countdown: false,
112
+
113
+ state: false,
114
+ map: false,
115
+ inGame: false,
116
+ roundStartTime: 0,
117
+ rounds: 3,
118
+
119
+ gt: false, // ?
120
+
121
+ quickplay: false,
122
+ teams: false,
123
+ mode: false,
124
+ balance: false,
125
+ inputs: false,
126
+ framecount: false,
127
+ stateID: false,
128
+ admin: false,
129
+ random: false,
130
+ };
131
+
132
+ // Game state
133
+ this.game = {
134
+ id: null,
135
+ host: null,
136
+ banned: false
137
+ };
138
+
139
+ // Player tracking
140
+ this.players = new Map();
141
+ }
142
+
143
+ /**
144
+ * Initialize the bot
145
+ * @returns {Promise<BonkBot>} This bot instance
146
+ */
147
+ async init() {
148
+ logger.info('Initializing BonkBot');
149
+
150
+ try {
151
+ // Get authentication token if needed
152
+ if (!this.account.guest && !this.token) {
153
+ this.token = await this.getToken(this.account.username, this.account.password);
154
+ }
155
+
156
+ // Get server information if using default
157
+ if (!this.server || this.server === DEFAULT_SERVER) {
158
+ logger.info('Getting server information');
159
+ const serverInfo = await this.getServerInfo();
160
+ this.server = serverInfo.server;
161
+
162
+ this.location = serverInfo;
163
+
164
+ this.room.server = this.server;
165
+ logger.info(`Using server: ${this.server}`);
166
+ }
167
+
168
+ logger.info('BonkBot initialized');
169
+ this.events.emit('ready');
170
+
171
+ return this;
172
+ } catch (error) {
173
+ logger.error('Failed to initialize BonkBot', error);
174
+ throw error;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Start the keep alive timer
180
+ * @private
181
+ */
182
+ startKeepAlive() {
183
+ this.keepAliveTimer = setInterval(() => {
184
+ if (this.connected) {
185
+ // Verify the socket is still connected before sending keep-alive
186
+ if (this.socket && this.socket.connected) {
187
+ this.sendTimesync();
188
+ } else {
189
+ logger.warn('Keep-alive detected disconnected socket');
190
+
191
+ this.stopBot();
192
+ this.events.emit('disconnect');
193
+ }
194
+ }
195
+ }, 5000);
196
+ }
197
+
198
+ /**
199
+ * Set the room address
200
+ * @param {Object} addressInfo - Room address information
201
+ */
202
+ setAddress(addressInfo) {
203
+ logger.info('Setting room address');
204
+
205
+ if (!addressInfo.address || !addressInfo.roomname || !addressInfo.server) {
206
+ throw new Error('Invalid room address information');
207
+ }
208
+
209
+ this.room.address = addressInfo.address;
210
+ this.room.name = addressInfo.roomname;
211
+ this.room.server = addressInfo.server;
212
+ this.room.bypass = addressInfo.bypass || '';
213
+
214
+ // Update server if different
215
+ if (this.server !== addressInfo.server) {
216
+ this.server = addressInfo.server;
217
+ }
218
+
219
+ logger.info(`Set room address: ${this.room.name} (${this.room.address})`);
220
+ }
221
+
222
+ /**
223
+ * Connect to the server
224
+ * @returns {Promise<BonkBot>} This bot instance
225
+ */
226
+ async connect() {
227
+ if (this.connected) {
228
+ logger.warn('Already connected, disconnecting first');
229
+ this.disconnect();
230
+ }
231
+
232
+ // Disable TLS certificate verification for bonk.io servers
233
+ // NOTE: This is required because bonk.io uses Sectigo certificates with incomplete chains
234
+ // and socket.io-client v2.x with engine.io-client v3.x doesn't properly pass
235
+ // rejectUnauthorized option to the underlying ws library
236
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
237
+
238
+ logger.info(`Connecting to server: ${this.server}`);
239
+
240
+ const socketAddr = `https://${this.server}.bonk.io`;
241
+
242
+ return new Promise((resolve, reject) => {
243
+ try {
244
+ // Configure socket.io options
245
+ const socketOptions = {
246
+ transports: ['websocket'],
247
+ reconnection: false,
248
+ timeout: 10000,
249
+ forceNew: true,
250
+ path: '/socket.io',
251
+ // NOTE: socket.io-client v2.x with engine.io-client v3.x has a bug where
252
+ // custom CA certificates cannot be properly passed to the websocket transport.
253
+ // As a workaround, we disable certificate verification for bonk.io servers
254
+ // which use Sectigo certificates with incomplete chains.
255
+ rejectUnauthorized: false
256
+ };
257
+
258
+ logger.info('Creating Socket.IO connection with certificate verification disabled');
259
+ this.socket = io(socketAddr, socketOptions);
260
+
261
+ // Set up connection timeout
262
+ const timeout = setTimeout(() => {
263
+ if (!this.connected) {
264
+ reject(new Error(`Connection timeout to server: ${this.server}`));
265
+ this.stopBot();
266
+ }
267
+ }, 10000);
268
+
269
+ // Connection opened
270
+ this.socket.on('connect', () => {
271
+ logger.info('Socket.IO connection established');
272
+
273
+ clearTimeout(timeout);
274
+ this.connected = true;
275
+
276
+ // Set up event handlers
277
+ this.setupSocketEvents();
278
+
279
+ // Start keep alive timer
280
+ this.startKeepAlive();
281
+
282
+ // Emit connect event
283
+ this.events.emit('connect');
284
+
285
+ resolve(this);
286
+ });
287
+
288
+ // Connection error
289
+ this.socket.on('connect_error', (error) => {
290
+ logger.error('Socket.IO connection error:', error.message || error);
291
+
292
+ if (!this.connected) {
293
+ clearTimeout(timeout);
294
+ const errorMsg = error.message || error.description?.message || 'Unknown error';
295
+ reject(new Error(`Failed to connect to server: ${errorMsg}`));
296
+ }
297
+
298
+ this.events.emit('error', error);
299
+ });
300
+
301
+ // Connection error (fallback handler)
302
+ this.socket.on('error', (error) => {
303
+ logger.error('Socket.IO error:', error.message || error);
304
+
305
+ if (!this.connected) {
306
+ clearTimeout(timeout);
307
+ const errorMsg = error.message || error.description?.message || 'Unknown error';
308
+ reject(new Error(`Failed to connect to server: ${errorMsg}`));
309
+ }
310
+
311
+ this.events.emit('error', error);
312
+ });
313
+
314
+ // Connection closed
315
+ this.socket.on('disconnect', (reason) => {
316
+ logger.info(`Socket.IO connection closed: ${reason}`);
317
+
318
+ if (!this.connected) {
319
+ clearTimeout(timeout);
320
+ reject(new Error(`Connection closed before fully established: ${reason}`));
321
+ }
322
+
323
+ this.stopBot();
324
+ this.events.emit('disconnect');
325
+ });
326
+ } catch (error) {
327
+ reject(new Error(`Failed to create Socket.IO connection: ${error.message}`));
328
+ }
329
+ });
330
+ }
331
+
332
+ /**
333
+ * Disconnect from the server
334
+ */
335
+ disconnect() {
336
+ if (!this.connected) {
337
+ logger.warn('Not connected, nothing to disconnect');
338
+ return;
339
+ }
340
+
341
+ logger.info('Disconnecting from server');
342
+
343
+ this.stopBot();
344
+
345
+ if (this.socket) {
346
+ this.socket.disconnect();
347
+ this.socket = null;
348
+ }
349
+ return true;
350
+ }
351
+
352
+
353
+ async getAddressFromUrl(url) {
354
+ const regex = /\/(\d{6})([a-zA-Z0-9]{5})?$/;
355
+ const match = url.match(regex);
356
+
357
+ if (!match) {
358
+ return null;
359
+ }
360
+
361
+ const id = match[1];
362
+ const bypass = match[2] || "";
363
+
364
+ const data = new URLSearchParams();
365
+ data.append('joinID', id);
366
+
367
+ try {
368
+ const response = await axios.post(API.AUTOJOIN, data.toString(), {
369
+ headers: {
370
+ 'Content-Type': 'application/x-www-form-urlencoded'
371
+ },
372
+ httpsAgent: httpsAgent
373
+ });
374
+
375
+ const result = response.data;
376
+
377
+ if (result.r == "success") {
378
+ result.bypass = bypass;
379
+ }
380
+
381
+ return result;
382
+ } catch (error) {
383
+ console.error('Error getting join link:', error);
384
+ throw error; // Re-throw to allow caller to handle the error
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Join a room
390
+ * @param {Object} [options] - Join options
391
+ * @returns {Promise<void>} Resolves when joined
392
+ */
393
+ async joinRoom(options = {}) {
394
+ if (!this.connected) {
395
+ throw new Error('Not connected to server');
396
+ }
397
+
398
+ if (!this.room.address) {
399
+ throw new Error('Room address not set');
400
+ }
401
+
402
+ logger.info(`Joining room: ${this.room.name} (${this.room.address})`);
403
+
404
+ // Prepare join data
405
+ const joinData = {
406
+ joinID: this.room.address,
407
+ roomPassword: options.password ? options.password.toString() : '',
408
+ guest: this.account.guest,
409
+ dbid: 2,
410
+ version: this.PROTOCOL_VERSION,
411
+ peerID: options.peerID || this.peerID,
412
+ bypass: this.room.bypass || '',
413
+ avatar: this.avatar
414
+ };
415
+
416
+ // Add account-specific data
417
+ if (this.account.guest) {
418
+ joinData.guestName = this.account.username;
419
+ } else {
420
+ joinData.token = this.token;
421
+ }
422
+
423
+ // Send join message
424
+ await this.sendMessage(CLIENT_MESSAGE_TYPES.JOIN_ROOM, joinData);
425
+
426
+ logger.info(`Join request sent for room: ${this.room.name}`);
427
+ }
428
+
429
+ /**
430
+ * Create a new room
431
+ * @param {Object} [options] - Room options
432
+ * @returns {Promise<Object>} Room address information
433
+ */
434
+ async createRoom(options = {}) {
435
+ if (!this.connected) {
436
+ throw new Error('Not connected to server');
437
+ }
438
+
439
+ logger.info('Creating new room');
440
+
441
+ // Set room info
442
+ this.room.name = options.roomname || `BonkBot Room ${Math.floor(Math.random() * 1000)}`;
443
+ this.room.maxPlayers = options.maxplayers || 8;
444
+ this.room.password = options.roompassword || '';
445
+
446
+ // Prepare create data
447
+ const createData = {
448
+ peerID: options.peerID ?? this.peerID,
449
+ roomName: options.roomName ?? this.room.name,
450
+ maxPlayers: options.maxPlayers ?? this.room.maxPlayers,
451
+ password: options.password ?? this.room.password,
452
+ dbid: options.dbid ?? 11822936,
453
+ guest: options.guest ?? this.account.guest,
454
+ minLevel: options.minLevel ?? 0,
455
+ maxLevel: options.maxLevel ?? 999,
456
+ latitude: options.latitude ?? this.location.lat,
457
+ longitude: options.longitude ?? this.location.long,
458
+ country: options.country ?? this.location.country,
459
+ version: this.PROTOCOL_VERSION,
460
+ hidden: options.hidden ? 1 : 0,
461
+ quick: options.quick ?? false,
462
+ mode: options.mode ?? 'custom',
463
+ token: options.token ?? (this.token || ''),
464
+ avatar: options.avatar ?? this.avatar,
465
+ };
466
+ if(createData.guest){
467
+ createData.guestName = this.account.username;
468
+ }
469
+
470
+ // Send create message
471
+ await this.sendMessage(CLIENT_MESSAGE_TYPES.CREATE_ROOM, createData);
472
+
473
+ // set the only player to yorself
474
+ this.players.set(0, {
475
+ peerID: createData.peerID,
476
+ guest: createData.guest,
477
+ team: 1,
478
+ teamName: TEAM_NAMES[1],
479
+ level: 0,
480
+ ready: false,
481
+ tabbed: false,
482
+ avatar: createData.avatar,
483
+ id: 0,
484
+ username: createData.guestName,
485
+ xp: 0,
486
+ ping: 0,
487
+ balance: 0,
488
+ host: true,
489
+ movement: {
490
+ input: 0,
491
+ frame: 0,
492
+ sequence: 0
493
+ }
494
+ });
495
+
496
+ this.game.id = 0
497
+ this.game.host = 0
498
+
499
+ logger.info(`Room creation request sent: ${this.room.name}`);
500
+
501
+ this.events.emit('CREATE_ROOM');
502
+
503
+ // Return room info
504
+ return {
505
+ address: this.room.address,
506
+ roomname: this.room.name,
507
+ server: this.room.server,
508
+ bypass: this.room.bypass
509
+ };
510
+ }
511
+
512
+ /**
513
+ * Get a player by ID
514
+ * @param {string|number} id - Player ID
515
+ * @returns {Object|null} Player object or null if not found
516
+ */
517
+ getPlayerByID(id) {
518
+ return this.players.get(id) || null;
519
+ }
520
+
521
+ /**
522
+ * Get a players id by username
523
+ * @param {string} username - Player username
524
+ * @param {boolean} guest - Whether the player is a guest
525
+ * @returns {number} Player ID
526
+ */
527
+ getPlayerIDByUsername(username, guest = false) {
528
+ for (const player of this.players.values()) {
529
+ console.log(player)
530
+ if (player.username == username && player.guest == guest) {
531
+ return player.id;
532
+ }
533
+ }
534
+
535
+ return -1;
536
+ }
537
+
538
+ /**
539
+ * Get all players
540
+ * @returns {Array<Object>} Array of player objects
541
+ */
542
+ getAllPlayers() {
543
+ const players = [];
544
+
545
+ for (const player of this.players.values()) {
546
+ players.push(player);
547
+ }
548
+
549
+ return players;
550
+ }
551
+
552
+
553
+
554
+ /**
555
+ * Automatically handle a packet
556
+ * @param {Object} packet - Packet to handle
557
+ */
558
+ async autoHandlePacket(packet) {
559
+ switch (packet.type) {
560
+
561
+ case 'ROOM_SHARE_LINK':
562
+ this.room.dbid = packet.roomId;
563
+ this.room.bypass = packet.roomBypass;
564
+
565
+ this.events.emit('ROOM_SHARE_LINK', { url: this.getShareLink() });
566
+ break;
567
+
568
+ case 'MAP_SWITCH':
569
+ this.room.map = packet.mapdata;
570
+ this.events.emit('MAP_SWITCH', { map: this.room.map });
571
+ break;
572
+
573
+ case 'MAP_SUGGEST':
574
+ const mapSuggestion = {
575
+ title: packet.maptitle,
576
+ author: packet.mapauthor,
577
+ player: this.players.get(packet.id)
578
+ }
579
+ this.events.emit('MAP_SUGGEST', mapSuggestion);
580
+ break;
581
+
582
+ case 'CHANGE_ROUNDS':
583
+ this.room.rounds = packet.rounds;
584
+ this.events.emit('CHANGE_ROUNDS', { rounds: packet.rounds });
585
+ break;
586
+
587
+ case 'COUNTDOWN':
588
+ this.room.countdown = packet.countdown;
589
+ this.events.emit('COUNTDOWN', { countdown: packet.countdown });
590
+ break;
591
+
592
+ case 'GAME_START':
593
+ // THIS NEEDS WORK
594
+ // COULD CAUSE DESYNC IF PLAYERS ARE FKING WITH THE STATE OBJ WITHOUT TELLING OTHERS
595
+
596
+ this.room.inGame = true;
597
+ this.room.roundStartTime = packet.timestamp;
598
+ this.room.state = packet.state;
599
+
600
+ this.room.map = packet.state.map;
601
+ this.room.gt = packet.state.gt;
602
+ this.room.rounds = packet.state.wl;
603
+ this.room.quickplay = packet.state.q;
604
+ this.room.teamsLocked = packet.state.tl;
605
+ this.room.teams = packet.state.tea;
606
+ this.room.engine = ENGINE_NAMES[packet.state.ga];
607
+ this.room.mode = GAMEMODE_NAMES[packet.state.mo];
608
+
609
+ // apply balances to all the players
610
+ // bal[playerid] = num
611
+ this.players.forEach((player) => {
612
+ player.balance = packet.state.bal[player.id] || 0;
613
+ this.players.set(player.id, player);
614
+ });
615
+
616
+ this.events.emit('GAME_START');
617
+ break;
618
+
619
+ case 'GAME_END':
620
+ this.room.inGame = false;
621
+ this.events.emit('GAME_END');
622
+ break;
623
+
624
+ case 'GAMEMODE_CHANGE':
625
+ this.room.mode = GAMEMODE_NAMES[packet.mode];
626
+ this.room.engine = ENGINE_NAMES[packet.engine];
627
+
628
+ this.events.emit('GAMEMODE_CHANGE', { mode: this.room.mode, engine: this.room.engine });
629
+ break;
630
+
631
+ case 'BALANCE_SET':
632
+ const playerBalance = this.players.get(packet.id);
633
+
634
+ if (playerBalance) {
635
+ playerBalance.balance = packet.balance;
636
+ this.players.set(packet.id, playerBalance);
637
+ }
638
+
639
+ this.events.emit('BALANCE_SET', { player: playerBalance, balance: packet.balance });
640
+ break;
641
+
642
+ case 'TIMESYNC':
643
+ this.timeSync.last_sync = packet.time;
644
+ this.timeSync.last_sync_id = packet.id;
645
+ this.timeSync.latency = Date.now() - this.timeSync.last_sync;
646
+ break;
647
+
648
+ case 'TEAM_CHANGE':
649
+ // Update player team
650
+ const playerTeam = this.players.get(packet.id);
651
+
652
+ if (playerTeam) {
653
+ playerTeam.team = packet.team;
654
+ playerTeam.teamName = TEAM_NAMES[packet.team];
655
+ this.players.set(packet.id, playerTeam);
656
+ }
657
+
658
+ // Emit team change event
659
+ this.events.emit('TEAM_CHANGE', { player: playerTeam, team: packet.team });
660
+ break;
661
+
662
+ case 'PLAYER_PINGS':
663
+ // Update player pings
664
+ // pings: { '0': 14 }
665
+ for (let [id, ping] of Object.entries(packet.pings)) {
666
+ id = parseInt(id)
667
+ const playerPing = this.players.get(id);
668
+ playerPing.ping = ping;
669
+ this.players.set(id, playerPing);
670
+ }
671
+
672
+ await this.sendMessage(CLIENT_MESSAGE_TYPES.PING_RESPONSE, { id: packet.pingId })
673
+ break;
674
+
675
+ case 'CHAT_MESSAGE':
676
+ // Handle chat message
677
+ const player = this.players.get(packet.id);
678
+
679
+ this.events.emit('CHAT_MESSAGE', { player: player, message: packet.message });
680
+ break;
681
+
682
+ case 'JOIN_ROOM':
683
+ // Set game info
684
+ this.game.id = packet.myid;
685
+ this.game.host = packet.hostid;
686
+ // Set room info
687
+ this.room.id = packet.roomid;
688
+ this.room.bypass = packet.roombypass;
689
+ this.room.teamsLocked = packet.teamsLocked;
690
+
691
+ // Add players
692
+ if (packet.playerdata && Array.isArray(packet.playerdata)) {
693
+ for (let i = 0; i < packet.playerdata.length; i++) {
694
+ const playerData = packet.playerdata[i];
695
+ if (playerData) {
696
+ playerData.id = i;
697
+
698
+ playerData.username = playerData.userName;
699
+ delete playerData.userName;
700
+
701
+ playerData.xp = this.levelToXP(playerData.level)
702
+ playerData.ping = 0;
703
+ playerData.balance = 0;
704
+ playerData.movement = {
705
+ input: 0,
706
+ frame: 0,
707
+ sequence: 0
708
+ }
709
+
710
+ this.players.set(i, playerData);
711
+ }
712
+ }
713
+ }
714
+
715
+ packet.players = this.players;
716
+ delete packet.playerdata;
717
+
718
+ // Emit join event
719
+ this.events.emit('JOIN', { game: this.game, room: this.room, players: this.players });
720
+ break;
721
+
722
+ case 'INITIAL_DATA':
723
+
724
+ this.room.engine = ENGINE_NAMES[packet.ga];
725
+ this.room.mode = GAMEMODE_NAMES[packet.mo];
726
+
727
+ this.room.gt = packet.gt;
728
+ this.room.rounds = packet.wl;
729
+ this.room.quickplay = packet.q;
730
+ this.room.teamsLocked = packet.tl;
731
+ this.room.teams = packet.tea;
732
+ this.room.framecount = packet.fc;
733
+ this.room.stateID = packet.stateID;
734
+ this.room.admin = packet.admin;
735
+ this.room.map = packet.gs ? packet.gs.map : null;
736
+ this.room.state = packet.state;
737
+ this.room.random = packet.random;
738
+
739
+ // apply balances to all the players
740
+ // bal[playerid] = num
741
+ this.players.forEach((player) => {
742
+ player.balance = packet.bal[player.id] || 0;
743
+ this.players.set(player.id, player);
744
+ });
745
+ break;
746
+
747
+ case 'PLAYER_JOIN':
748
+ // Add player
749
+ this.players.set(packet.id, {
750
+ peerID: packet.peerID,
751
+ guest: packet.guest,
752
+ team: 1,
753
+ teamName: TEAM_NAMES[1],
754
+ level: parseInt(packet.level),
755
+ ready: false,
756
+ tabbed: false,
757
+ avatar: packet.avatar,
758
+ id: packet.id,
759
+ username: packet.username,
760
+ xp: this.levelToXP(packet.level),
761
+ ping: 0,
762
+ balance: 0,
763
+ host: false,
764
+ movement: {
765
+ input: 0,
766
+ frame: 0,
767
+ sequence: 0
768
+ }
769
+ });
770
+
771
+ // check if the bot is host
772
+ const botPlayer = this.players.get(this.game.id);
773
+ if (botPlayer.host) {
774
+ await this.sendMessage(CLIENT_MESSAGE_TYPES.INFORM_IN_LOBBY, {
775
+ sid: packet.id,
776
+ gs: {
777
+ map: this.room.map || {
778
+ v: 13,
779
+ s: {
780
+ re: false,
781
+ nc: false,
782
+ pq: 1,
783
+ gd: 25,
784
+ fl: false
785
+ },
786
+ physics: {
787
+ shapes: [],
788
+ fixtures: [],
789
+ bodies: [],
790
+ bro: [],
791
+ joints: [],
792
+ ppm: 12
793
+ },
794
+ spawns: [],
795
+ capZones: [],
796
+ m: {
797
+ a: "BonkBot",
798
+ n: "Empty Map",
799
+ dbv: 2,
800
+ dbid: 767645,
801
+ authid: -1,
802
+ date: "",
803
+ rxid: 0,
804
+ rxn: "",
805
+ rxa: "",
806
+ rxdb: 1,
807
+ cr: ["uint32"],
808
+ pub: true,
809
+ mo: ""
810
+ }
811
+ },
812
+ gt: this.room.gt || 2,
813
+ wl: this.room.rounds || 3,
814
+ q: this.room.quickplay || false,
815
+ tl: this.room.teamsLocked || false,
816
+ tea: this.room.teams || false,
817
+ ga: "b",
818
+ mo: "b",
819
+ bal: this.getAllPlayers().reduce((balances, player) => {
820
+ if (player.balance) {
821
+ balances[player.id] = player.balance;
822
+ }
823
+ return balances;
824
+ }, {})
825
+ }
826
+ });
827
+ }
828
+
829
+ // Emit join event
830
+ this.events.emit('PLAYER_JOIN', { player: this.players.get(packet.id), id: packet.id });
831
+ break;
832
+
833
+ case 'PLAYER_LEAVE':
834
+ const deletedPlayer = this.players.get(packet.id);
835
+
836
+ // delete player
837
+ this.players.delete(packet.id);
838
+
839
+ // Emit leave event
840
+ this.events.emit('PLAYER_LEAVE', { player: deletedPlayer, id: packet.id });
841
+ break;
842
+
843
+ case 'HOST_TRANSFER':
844
+ // Update host
845
+ this.game.host = packet.newHost;
846
+
847
+ // find the player that is now host
848
+ this.players.forEach((player) => {
849
+ if (player.id === packet.newHost) {
850
+ player.host = true;
851
+ } else {
852
+ player.host = false;
853
+ }
854
+ this.players.set(player.id, player);
855
+ });
856
+
857
+ // Emit host transfer event
858
+ this.events.emit('HOST_TRANSFER', { oldHost: packet.oldHost, newHost: packet.newHost });
859
+ break;
860
+
861
+ case 'READY_CHANGE':
862
+ // Update player ready status
863
+ const playerReady = this.players.get(packet.id);
864
+
865
+ if (playerReady) {
866
+ playerReady.ready = packet.ready;
867
+
868
+ this.players.set(packet.id, playerReady);
869
+ }
870
+
871
+ // Emit ready change event
872
+ this.events.emit('READY_CHANGE', { player: playerReady, ready: packet.ready });
873
+ break;
874
+
875
+ case 'TEAMLOCK_TOGGLE':
876
+ // Update teams lock
877
+ this.room.teamsLocked = packet.teamsLocked;
878
+
879
+ // Emit teams lock event
880
+ this.events.emit('TEAMLOCK_TOGGLE', { teamsLocked: packet.teamsLocked });
881
+ break;
882
+
883
+ case 'PLAYER_TABBED':
884
+ // Update player tabbed status
885
+ const playerTabbed = this.players.get(packet.id);
886
+
887
+ if (playerTabbed) {
888
+ playerTabbed.tabbed = packet.tabbed;
889
+
890
+ this.players.set(packet.id, playerTabbed);
891
+ }
892
+
893
+ // Emit tabbed event
894
+ this.events.emit('PLAYER_TABBED', { player: playerTabbed, tabbed: packet.tabbed });
895
+ break;
896
+
897
+ case 'ROOM_NAME_UPDATE':
898
+ // Update room name
899
+ this.room.name = packet.name;
900
+
901
+ // Emit room name change event
902
+ this.events.emit('ROOM_NAME_UPDATE', { name: packet.name });
903
+ break;
904
+
905
+ case 'ROOM_ADDRESS':
906
+ // Update room address
907
+ this.room.address = packet.address;
908
+
909
+ // Emit room address event
910
+ this.events.emit('ROOM_ADDRESS', { address: packet.address });
911
+ break;
912
+
913
+ case 'PLAYER_KICK':
914
+ // Check if we were kicked
915
+ if (packet.id === this.game.id) {
916
+ this.game.banned = true; // might not act be banned
917
+ }
918
+
919
+ // Remove player
920
+ const kickedPlayer = this.players.get(packet.id);
921
+ this.players.delete(packet.id);
922
+
923
+ // Emit kick event
924
+ this.events.emit('PLAYER_KICK', kickedPlayer);
925
+ break;
926
+
927
+ case 'PLAYER_INPUT':
928
+ // Handle player input
929
+ const playerInput = this.players.get(packet.id);
930
+
931
+ if (playerInput) {
932
+ playerInput.movement = {
933
+ input: packet.input,
934
+ frame: packet.frame,
935
+ sequence: packet.sequence
936
+ }
937
+
938
+ this.players.set(packet.id, playerInput);
939
+ }
940
+
941
+ // Emit player input event
942
+ this.events.emit('PLAYER_INPUT', { player: playerInput, movement: playerInput.movement });
943
+ break;
944
+
945
+
946
+ default:
947
+ logger.debug(`No handler for packet type: ${packet.type}. Tell the developer about this for a fix!\nhttps://github.com/PixelMelt/BonkBot`, packet);
948
+ break;
949
+ }
950
+ }
951
+
952
+ /**
953
+ * Validate account information
954
+ * @private
955
+ * @param {Object} account - Account information
956
+ * @returns {Object} Validated account information
957
+ */
958
+ validateAccount(account) {
959
+ // Default to guest account
960
+ const validatedAccount = {
961
+ guest: true,
962
+ username: `BonkBot${Math.floor(Math.random() * 10000)}`
963
+ };
964
+
965
+ // Override with provided values
966
+ if (account.guest !== undefined) {
967
+ validatedAccount.guest = !!account.guest;
968
+ }
969
+
970
+ if (account.username) {
971
+ validatedAccount.username = account.username;
972
+ }
973
+
974
+ if (account.password) {
975
+ validatedAccount.password = account.password;
976
+ }
977
+
978
+ // Non-guest accounts must have a password
979
+ if (!validatedAccount.guest && !validatedAccount.password) {
980
+ logger.warn('Non-guest account must have a password, defaulting to guest');
981
+ validatedAccount.guest = true;
982
+ }
983
+
984
+ return validatedAccount;
985
+ }
986
+
987
+ /**
988
+ * Generate a random peer ID
989
+ * @private
990
+ * @returns {string} Random peer ID
991
+ */
992
+ generatePeerId() {
993
+ return Math.random().toString(36).substr(2, 10) + 'v00000';
994
+ }
995
+
996
+ /**
997
+ * Get authentication token
998
+ * @private
999
+ * @param {string} username - Username
1000
+ * @param {string} password - Password
1001
+ * @returns {Promise<string>} Authentication token
1002
+ */
1003
+ async getToken(username, password) {
1004
+ try {
1005
+ logger.info(`Getting token for user: ${username}`);
1006
+
1007
+ const response = await axios.post(API.LOGIN,
1008
+ `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&remember=true`, {
1009
+ headers: {
1010
+ 'Content-Type': 'application/x-www-form-urlencoded'
1011
+ },
1012
+ httpsAgent: httpsAgent
1013
+ }
1014
+ );
1015
+
1016
+ if (!response.data || !response.data.token) {
1017
+ throw new Error('Failed to get authentication token');
1018
+ }
1019
+
1020
+ logger.info('Successfully obtained authentication token');
1021
+ return response.data.token;
1022
+ } catch (error) {
1023
+ logger.error('Failed to authenticate', error);
1024
+ throw new Error(`Failed to authenticate: ${error.message}`);
1025
+ }
1026
+ }
1027
+
1028
+ /**
1029
+ * Get a room address from a room name
1030
+ * @param {string} roomName - Room name
1031
+ * @returns {Promise<Object>} Room address information
1032
+ */
1033
+ async getAddressFromRoomName(roomName) {
1034
+ logger.info(`Getting address for room: ${roomName}`);
1035
+
1036
+ try {
1037
+ // Find room by name
1038
+ const room = await this.getRoomByName(roomName, this.token);
1039
+
1040
+ if (!room) {
1041
+ throw new Error(`Room not found: ${roomName}`);
1042
+ }
1043
+
1044
+ // Get room address
1045
+ const address = await this.getRoomAddress(room.id);
1046
+
1047
+ const result = {
1048
+ roomname: room.roomname,
1049
+ address: address.address,
1050
+ server: address.server,
1051
+ bypass: ''
1052
+ };
1053
+
1054
+ logger.info(`Got address for room: ${roomName}`);
1055
+
1056
+ return result;
1057
+ } catch (error) {
1058
+ logger.error(`Failed to get address for room: ${roomName}`, error);
1059
+ throw error;
1060
+ }
1061
+ }
1062
+
1063
+ /**
1064
+ * Get server information
1065
+ * @private
1066
+ * @param {string} [token] - Authentication token
1067
+ * @returns {Promise<Object>} Server information
1068
+ */
1069
+ async getServerInfo() {
1070
+ try {
1071
+ const response = await axios.post(API.GET_ROOMS,
1072
+ `version=${this.PROTOCOL_VERSION}&gl=y&token=${this.token ?? ""}`, {
1073
+ headers: {
1074
+ 'Content-Type': 'application/x-www-form-urlencoded'
1075
+ },
1076
+ httpsAgent: httpsAgent
1077
+ }
1078
+ );
1079
+
1080
+ if (!response.data || !response.data.createserver) {
1081
+ throw new Error('Failed to get server information');
1082
+ }
1083
+
1084
+ return {
1085
+ server: response.data.createserver,
1086
+ lat: response.data.lat,
1087
+ long: response.data.long,
1088
+ country: response.data.country
1089
+ };
1090
+ } catch (error) {
1091
+ logger.error(error);
1092
+ throw new Error(`Failed to get server information: ${error.message}`);
1093
+ }
1094
+ }
1095
+
1096
+ /**
1097
+ * Get list of rooms
1098
+ * @private
1099
+ * @returns {Promise<Array>} List of rooms
1100
+ */
1101
+ async getRooms() {
1102
+ try {
1103
+ logger.info('Getting list of rooms');
1104
+
1105
+ const response = await axios.post(API.GET_ROOMS,
1106
+ `version=${this.PROTOCOL_VERSION}&gl=y&token=${this.token || ""}`, {
1107
+ headers: {
1108
+ 'Content-Type': 'application/x-www-form-urlencoded'
1109
+ },
1110
+ httpsAgent: httpsAgent
1111
+ }
1112
+ );
1113
+
1114
+ if (!response.data || !response.data.rooms) {
1115
+ throw new Error('Failed to get rooms');
1116
+ }
1117
+
1118
+ logger.info(`Retrieved ${response.data.rooms.length} rooms`);
1119
+ return response.data.rooms;
1120
+ } catch (error) {
1121
+ logger.error('Failed to get rooms', error);
1122
+ throw new Error(`Failed to get rooms: ${error.message}`);
1123
+ }
1124
+ }
1125
+
1126
+ /**
1127
+ * Change game mode
1128
+ * @param {string} mode - Game mode ('b' = classic, 'ar' = arrows, 'ard' = death arrows, 'sp' = grapple, 'v' = vtol, 'f' = football)
1129
+ * @param {string} engine - Game engine ('b' = bonk, 'f' = football)
1130
+ */
1131
+ async setGameMode(mode, engine = 'b') {
1132
+ this.checkConnection();
1133
+
1134
+ // Validar modo
1135
+ const validModes = ['b', 'ar', 'ard', 'sp', 'v', 'f'];
1136
+ const validEngines = ['b', 'f'];
1137
+
1138
+ if (!validModes.includes(mode)) {
1139
+ throw new Error(`Invalid mode. Valid modes: ${validModes.join(', ')}`);
1140
+ }
1141
+
1142
+ if (!validEngines.includes(engine)) {
1143
+ throw new Error(`Invalid engine. Valid engines: ${validEngines.join(', ')}`);
1144
+ }
1145
+
1146
+ await this.sendMessage(CLIENT_MESSAGE_TYPES.SEND_MODE, {
1147
+ ga: engine,
1148
+ mo: mode
1149
+ });
1150
+
1151
+ logger.info(`Game mode changed to: ${GAMEMODE_NAMES[mode]} (${ENGINE_NAMES[engine]})`);
1152
+ }
1153
+
1154
+ /**
1155
+ * Set Football mode
1156
+ */
1157
+ async setFootballMode() {
1158
+ await this.setGameMode('f', 'f');
1159
+ }
1160
+
1161
+ /**
1162
+ * Set Classic mode
1163
+ */
1164
+ async setClassicMode() {
1165
+ await this.setGameMode('b', 'b');
1166
+ }
1167
+
1168
+ /**
1169
+ * Set Arrows mode
1170
+ */
1171
+ async setArrowsMode() {
1172
+ await this.setGameMode('ar', 'b');
1173
+ }
1174
+
1175
+ /**
1176
+ * Set Death Arrows mode
1177
+ */
1178
+ async setDeathArrowsMode() {
1179
+ await this.setGameMode('ard', 'b');
1180
+ }
1181
+
1182
+ /**
1183
+ * Set Grapple mode
1184
+ */
1185
+ async setGrappleMode() {
1186
+ await this.setGameMode('sp', 'b');
1187
+ }
1188
+
1189
+ /**
1190
+ * Set VTOL mode
1191
+ */
1192
+ async setVTOLMode() {
1193
+ await this.setGameMode('v', 'b');
1194
+ }
1195
+
1196
+ /**
1197
+ * Get room by name
1198
+ * @private
1199
+ * @param {string} roomName - Room name
1200
+ * @returns {Promise<Object|null>} Room object or null if not found
1201
+ */
1202
+ async getRoomByName(roomName) {
1203
+ try {
1204
+ logger.info(`Searching for room: ${roomName}`);
1205
+
1206
+ const rooms = await this.getRooms(this.token);
1207
+
1208
+ for (const room of rooms) {
1209
+ if (room.roomname === roomName) {
1210
+ logger.info(`Found room: ${roomName}`);
1211
+ return room;
1212
+ }
1213
+ }
1214
+
1215
+ logger.info(`Room not found: ${roomName}`);
1216
+ return null;
1217
+ } catch (error) {
1218
+ logger.error(`Failed to find room: ${roomName}`, error);
1219
+ throw new Error(`Failed to find room: ${error.message}`);
1220
+ }
1221
+ }
1222
+
1223
+ /**
1224
+ * Get room address
1225
+ * @private
1226
+ * @param {string} id - Room ID
1227
+ * @returns {Promise<Object>} Room address
1228
+ */
1229
+ async getRoomAddress(id) {
1230
+ try {
1231
+ logger.info(`Getting address for room ID: ${id}`);
1232
+
1233
+ const response = await axios.post(API.GET_ROOM_ADDRESS,
1234
+ `id=${id}`, {
1235
+ headers: {
1236
+ 'Content-Type': 'application/x-www-form-urlencoded'
1237
+ },
1238
+ httpsAgent: httpsAgent
1239
+ }
1240
+ );
1241
+
1242
+ if (!response.data || !response.data.address) {
1243
+ throw new Error('Failed to get room address');
1244
+ }
1245
+
1246
+ logger.info(`Retrieved address for room ID: ${id}`);
1247
+ return response.data;
1248
+ } catch (error) {
1249
+ logger.error(`Failed to get address for room ID: ${id}`, error);
1250
+ throw new Error(`Failed to get room address: ${error.message}`);
1251
+ }
1252
+ }
1253
+
1254
+ /**
1255
+ * Set up socket event handlers
1256
+ * @private
1257
+ */
1258
+ setupSocketEvents() {
1259
+ // Register listeners for all message types we care about
1260
+ Object.values(SERVER_MESSAGE_TYPES).forEach(eventId => {
1261
+ this.socket.on(eventId, (...args) => {
1262
+ // Create a packet array with event ID as first element and args as the rest
1263
+ const packet = [eventId, ...args];
1264
+
1265
+ // Process the packet
1266
+ const parsedPacket = parsePacket(packet);
1267
+
1268
+
1269
+ // Emit events
1270
+ this.events.emit('PACKET', parsedPacket);
1271
+
1272
+ // logger.debug(`Received event ${eventId}`, parsedPacket);
1273
+ });
1274
+ });
1275
+
1276
+ // Handle standard Socket.IO events
1277
+ this.socket.on('connect', () => {
1278
+ this.events.emit('connect');
1279
+ });
1280
+
1281
+ this.socket.on('disconnect', (reason) => {
1282
+ this.events.emit('disconnect', reason);
1283
+ });
1284
+
1285
+ this.socket.on('error', (error) => {
1286
+ this.events.emit('error', error);
1287
+ });
1288
+ }
1289
+
1290
+
1291
+ /**
1292
+ * Send a message to the server using Socket.IO v2 protocol
1293
+ * @private
1294
+ * @param {number} eventId - Event ID from CLIENT_MESSAGE_TYPES
1295
+ * @param {any} data - Message data
1296
+ */
1297
+ async sendMessage(eventId, data) {
1298
+ this.checkConnection()
1299
+
1300
+ // if(eventId != 18 && eventId != 1){ // just for pix to not get annoyed by things
1301
+ // }
1302
+ logger.debug(`Sending message type ${eventId}`, data);
1303
+
1304
+ // For Socket.IO v2, we need to emit to the 'message' event with the event ID and data
1305
+ // The server expects a message in the format: [eventId, data]
1306
+ await this.socket.emit(eventId, data);
1307
+ return true
1308
+ }
1309
+
1310
+
1311
+
1312
+
1313
+ /**
1314
+ *
1315
+ * User callable methods (the bot api)
1316
+ *
1317
+ */
1318
+
1319
+
1320
+
1321
+
1322
+
1323
+
1324
+
1325
+ /**
1326
+ * Clean up resources
1327
+ * @private
1328
+ */
1329
+ stopBot() {
1330
+ this.connected = false;
1331
+
1332
+ if (this.keepAliveTimer) {
1333
+ clearInterval(this.keepAliveTimer);
1334
+ this.keepAliveTimer = null;
1335
+ }
1336
+ return true;
1337
+ }
1338
+
1339
+ getShareLink(){
1340
+ return "https://bonk.io/" + this.room.dbid + this.room.bypass;
1341
+ }
1342
+
1343
+
1344
+
1345
+
1346
+
1347
+ /**
1348
+ * Send a chat message
1349
+ * @param {string} message - Message to send
1350
+ */
1351
+ async chat(message) {
1352
+ this.checkConnection();
1353
+ await this.sendMessage(CLIENT_MESSAGE_TYPES.CHAT_MESSAGE, {
1354
+ message
1355
+ });
1356
+ }
1357
+
1358
+ /**
1359
+ * Set player ready status
1360
+ * @param {boolean} ready - Ready status
1361
+ */
1362
+ async ready(ready) {
1363
+ this.checkConnection();
1364
+ await this.sendMessage(CLIENT_MESSAGE_TYPES.SET_READY, {
1365
+ ready
1366
+ });
1367
+ }
1368
+
1369
+ /**
1370
+ * Join a team
1371
+ * @param {number} team - Team to join
1372
+ */
1373
+ async joinTeam(team) {
1374
+ this.checkConnection();
1375
+ await this.sendMessage(CLIENT_MESSAGE_TYPES.CHANGE_OWN_TEAM, {
1376
+ targetTeam: team
1377
+ });
1378
+ }
1379
+
1380
+ /**
1381
+ * Toggle teams lock
1382
+ * @param {boolean} locked - Whether teams are locked
1383
+ */
1384
+ async toggleTeams(locked) {
1385
+ this.checkConnection();
1386
+ await this.sendMessage(CLIENT_MESSAGE_TYPES.TEAM_LOCK, {
1387
+ teamLock: locked
1388
+ });
1389
+ }
1390
+
1391
+ /**
1392
+ * Ban a player
1393
+ * @param {string|number} playerId - Player ID to ban
1394
+ */
1395
+ async banPlayer(playerId) {
1396
+ this.checkConnection();
1397
+ await this.sendMessage(CLIENT_MESSAGE_TYPES.KICK_BAN_PLAYER, {
1398
+ banshortid: playerId
1399
+ });
1400
+ }
1401
+
1402
+ /**
1403
+ * Leave the game
1404
+ */
1405
+ async leaveGame() {
1406
+ this.checkConnection();
1407
+ await this.sendMessage(CLIENT_MESSAGE_TYPES.RETURN_TO_LOBBY);
1408
+ }
1409
+
1410
+ /**
1411
+ * Give host to another player
1412
+ * @param {string|number} playerId - Player ID to give host to
1413
+ */
1414
+ async giveHost(playerId) {
1415
+ this.checkConnection();
1416
+ await this.sendMessage(CLIENT_MESSAGE_TYPES.SEND_HOST_CHANGE, {
1417
+ id: playerId
1418
+ });
1419
+ }
1420
+
1421
+ /**
1422
+ * Get the host player
1423
+ * @returns {Object} Host player object
1424
+ */
1425
+ getHost(){
1426
+ return this.players.get(this.game.host);
1427
+ }
1428
+
1429
+ /**
1430
+ * Set number of rounds
1431
+ * @param {number} rounds - Number of rounds
1432
+ */
1433
+ async setRounds(rounds) {
1434
+ this.checkConnection();
1435
+ await this.sendMessage(CLIENT_MESSAGE_TYPES.SEND_ROUNDS, {
1436
+ w: rounds
1437
+ });
1438
+ }
1439
+
1440
+ /**
1441
+ * Send an input to the game
1442
+ * @param {Object} input - Input data
1443
+ */
1444
+ async sendInput(input) {
1445
+ this.checkConnection();
1446
+ await this.sendMessage(CLIENT_MESSAGE_TYPES.SEND_INPUTS, {
1447
+ i: input.input,
1448
+ f: input.frame,
1449
+ c: input.sequence
1450
+ });
1451
+ }
1452
+
1453
+ /**
1454
+ * Send a timesync message to the server
1455
+ * @private
1456
+ */
1457
+ async sendTimesync() {
1458
+ await this.sendMessage(CLIENT_MESSAGE_TYPES.TIMESYNC, {
1459
+ jsonrpc: "2.0",
1460
+ id: this.timeSync.count++,
1461
+ method: "timesync"
1462
+ });
1463
+ }
1464
+
1465
+ /**
1466
+ * Check if connected to server
1467
+ * @private
1468
+ * @throws {Error} If not connected
1469
+ */
1470
+ checkConnection() {
1471
+ if (!this.connected || !this.socket) {
1472
+ throw new Error('Not connected to server');
1473
+ }
1474
+ return true;
1475
+ }
1476
+
1477
+ /**
1478
+ * Converts level to XP total
1479
+ * @param {Number} level - The level to convert
1480
+ * @returns {Number} The total XP required for this level
1481
+ */
1482
+ levelToXP(level) {
1483
+ if (level < 1) return 0;
1484
+ return 100 * Math.pow(level - 1, 2);
1485
+ }
1486
+
1487
+ /**
1488
+ * Converts XP total to level
1489
+ * @param {Number} xp - The XP amount to convert
1490
+ * @returns {Number} The level for this XP amount
1491
+ */
1492
+ xpToLevel(xp) {
1493
+ if (xp < 0) return 1;
1494
+ return Math.floor(Math.sqrt(xp / 100) + 1);
1495
+ }
1496
+ }
1497
+
1498
+ module.exports = BonkBot;