• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

BunnyNabbit / classicborne / 29220597482

13 Jul 2026 02:57AM UTC coverage: 55.846% (+0.5%) from 55.317%
29220597482

push

github

web-flow
Merge pull request #57 from BunnyNabbit/remove-sonarqube-configuration

Remove SoanrQube configuration/CI

120 of 126 branches covered (95.24%)

Branch coverage included in aggregate %.

1012 of 1901 relevant lines covered (53.24%)

811.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.39
/class/level/BaseLevel.mjs
1
import { Drone } from "./drone/Drone.mjs"
4✔
2
import { Ego } from "./drone/Ego.mjs"
4✔
3
import { TypedEmitter } from "tiny-typed-emitter"
4✔
4
import { componentToHex } from "../../utils.mjs"
4✔
5
import { EmptyTemplate } from "./BaseTemplate.mjs"
4✔
6
import { BaseLevelCommandInterpreter } from "./BaseLevelCommandInterpreter.mjs"
4✔
7
/** @import {BasePlayer} from "../player/BasePlayer.mjs" */
4✔
8
/**@import {
4✔
9
 *   Vector2,
4✔
10
 *   Vector3
4✔
11
 * } from "../../types/arrayLikes.mjs"
4✔
12
 */
4✔
13
/** @import {Client} from "classicborne-server-protocol/class/Client.mjs" */
4✔
14
// /** @import { LevelCommand } from "./levelCommands.mjs" */
4✔
15
/** @import {BaseTemplate} from "./BaseTemplate.mjs" */
4✔
16
/** @import {BaseUniverse} from "../server/BaseUniverse.mjs" */
4✔
17
/** @import {ChangeRecord} from "./changeRecord/ChangeRecord.mjs" */
4✔
18
/** @import {NullChangeRecord} from "./changeRecord/NullChangeRecord.mjs" */
4✔
19

4✔
20
/**I'm a base class for levels, managing {@link BasePlayer | players}, {@link Drone | drones}, blocks and game logic.
4✔
21
 *
4✔
22
 * ## Interactions
4✔
23
 *
4✔
24
 * ### {@link BaseTemplate}
4✔
25
 *
4✔
26
 * - On creation, I accept a buffer of blocks (usually generated by a {@link BaseTemplate} subclass, e.g., {@link EmptyTemplate}).
4✔
27
 * - The template’s {@link BaseTemplate.generate} method is called to produce the buffer, which is passed to my constructor.
4✔
28
 *
4✔
29
 * @example Creating a level using an EmptyTemplate to generate an empty block buffer.
4✔
30
 *
4✔
31
 * ```js
4✔
32
 * import { BaseLevel } from "classicborne/class/level/BaseLevel.mjs"
4✔
33
 * import { EmptyTemplate } from "classicborne/class/level/BaseTemplate.mjs"
4✔
34
 *
4✔
35
 * const template = new EmptyTemplate()
4✔
36
 * const bounds = [64, 64, 64]
4✔
37
 * const blocks = template.generate(bounds)
4✔
38
 * const level = new BaseLevel(bounds, blocks)
4✔
39
 * ```
4✔
40
 *
4✔
41
 * @template {Record<string, any>} E
4✔
42
 * @extends {TypedEmitter<{ playerAdded: (player: BasePlayer) => void; playerRemoved: (player: BasePlayer) => void; loaded: () => void; unloaded: () => void; levelLoaded: () => void } & E>}
4✔
43
 */
4✔
44
export class BaseLevel extends TypedEmitter {
4✔
45
        /**Initializes a new instance of the BaseLevel class.
4✔
46
         *
4✔
47
         * @param {Vector3} bounds
4✔
48
         * @param {Buffer} blocks
4✔
49
         */
4✔
50
        constructor(bounds, blocks) {
4✔
51
                super()
4✔
52
                /**The players currently in the level.
4✔
53
                 *
4✔
54
                 * @type {BasePlayer[]}
4✔
55
                 */
4✔
56
                this.players = []
4✔
57
                /**The bounds of the level.
4✔
58
                 *
4✔
59
                 * @type {Vector3}
4✔
60
                 */
4✔
61
                this.bounds = bounds
4✔
62
                /**The buffer representing the level's blocks.
4✔
63
                 *
4✔
64
                 * @type {Buffer}
4✔
65
                 */
4✔
66
                this.blocks = blocks
4✔
67
                /**Usernames of who is allowed to build in the level. {@link BaseLevel.userHasPermission} checks against this list. If empty, everyone is allowed.
4✔
68
                 *
4✔
69
                 * @type {string[]}
4✔
70
                 */
4✔
71
                this.allowList = []
4✔
72
                /**The set of drones currently in the level.
4✔
73
                 *
4✔
74
                 * @type {Set<Drone>}
4✔
75
                 */
4✔
76
                this.drones = new Set()
4✔
77
                /**Drones which are linked to clients in the level.
4✔
78
                 *
4✔
79
                 * @type {Map<Client, Drone>}
4✔
80
                 */
4✔
81
                this.clientDrones = new Map()
4✔
82
                this.commandInterpreter = new /** @type {typeof BaseLevel} */ (this.constructor).commandInterpreterClass(this)
4✔
83
                /** @type {boolean} */
4✔
84
                this.loading
4✔
85
                /** @type {boolean} */
4✔
86
                this.blocking
4✔
87
                /** @type {ChangeRecord | NullChangeRecord} */
4✔
88
                this.changeRecord
4✔
89
        }
4✔
90
        /**Sends all {@link Drone | drones} in the level to the specified {@link BasePlayer | player}.
4✔
91
         *
4✔
92
         * @param {BasePlayer} player - The player to send the drones to.
4✔
93
         */
4✔
94
        sendDrones(player) {
4✔
95
                this.drones.forEach((drone) => {
×
96
                        player.droneTransmitter.addDrone(drone)
×
97
                })
×
98
        }
×
99
        /**Removes a {@link BasePlayer | player} from the level.
4✔
100
         *
4✔
101
         * @param {BasePlayer} player - The player to be removed.
4✔
102
         */
4✔
103
        removePlayer(player) {
4✔
104
                player.space = null
×
105
                const index = this.players.indexOf(player)
×
106
                if (index !== -1) this.players.splice(index, 1)
×
107
                const drone = this.clientDrones.get(player.client)
×
108
                this.clientDrones.delete(player.client)
×
109
                this.removeDrone(drone)
×
110
                player.droneTransmitter.clearDrones()
×
111
                this.emit("playerRemoved", player)
×
112
        }
×
113
        /**Removes a {@link Drone | drone} from the level.
4✔
114
         *
4✔
115
         * @param {Drone} drone - The drone to be removed.
4✔
116
         */
4✔
117
        removeDrone(drone) {
4✔
118
                drone.destroy()
×
119
                this.drones.delete(drone)
×
120
        }
×
121
        /**Adds a {@link Drone | drone} to the level.
4✔
122
         *
4✔
123
         * @param {Drone} drone - The drone to be added.
4✔
124
         */
4✔
125
        addDrone(drone) {
4✔
126
                this.players.forEach((player) => {
×
127
                        player.droneTransmitter.addDrone(drone)
×
128
                })
×
129
                this.drones.add(drone)
×
130
        }
×
131
        /**Adds a {@link BasePlayer | player} to the level.
4✔
132
         *
4✔
133
         * @param {BasePlayer} player - The player to be added.
4✔
134
         */
4✔
135
        addPlayer(player, position = [0, 0, 0], orientation = [0, 0]) {
4✔
136
                this.emit("playerAdded", player)
×
137
                player.space = this
×
138
                this.loadPlayer(player, position, orientation)
×
139
                this.sendDrones(player)
×
140
                const drone = new Drone(new Ego({ name: player.getDisplayName() }))
×
141
                this.clientDrones.set(player.client, drone)
×
142
                this.addDrone(drone)
×
143
                this.players.push(player)
×
144
                player.teleporting = false
×
145
        }
×
146
        /**Sends the level data to the specified {@link BasePlayer | player}. This is typically called on {@link BaseLevel.addPlayer}.
4✔
147
         *
4✔
148
         * @param {BasePlayer} player
4✔
149
         * @param {Vector3} [position=[0,0,0]] Default is `[0,0,0]`
4✔
150
         * @param {Vector2} [orientation=[0,0]] Default is `[0,0]`
4✔
151
         */
4✔
152
        loadPlayer(player, position = [0, 0, 0], orientation = [0, 0]) {
4✔
153
                player.client.loadLevel(
×
154
                        this.blocks,
×
155
                        ...this.bounds,
×
156
                        false,
×
157
                        () => {
×
158
                                player.client.setClickDistance(10000)
×
159
                                player.emit("levelLoaded")
×
160
                        },
×
161
                        () => {
×
162
                                if (this.blockset) BaseLevel.sendBlockset(player.client, this.blockset)
×
163
                                if (this.environment) player.client.extensions.get("LevelEnvironment").setEnvironmentProperties(this.environment)
×
164
                                if (this.texturePackUrl) player.client.extensions.get("LevelEnvironment").texturePackUrl(this.texturePackUrl)
×
165
                                player.client.extensions.get("BlockPermissions").setBlockPermission(7, 1, 1)
×
166
                                player.client.extensions.get("BlockPermissions").setBlockPermission(8, 1, 1)
×
167
                                player.client.extensions.get("BlockPermissions").setBlockPermission(9, 1, 1)
×
168
                                player.client.extensions.get("BlockPermissions").setBlockPermission(10, 1, 1)
×
169
                                player.client.extensions.get("BlockPermissions").setBlockPermission(11, 1, 1)
×
170
                                player.client.extensions.get("ExtendedPlayerList").configureSpawn(-1, player.authInfo.username, position[0], position[1], position[2], orientation[0], orientation[1], player.authInfo.username)
×
171
                        }
×
172
                )
×
173
        }
×
174
        /** Reloads all players in the level, typically used to refresh the level's state. */
4✔
175
        reload() {
4✔
176
                this.players.forEach((player) => {
×
177
                        const reloadedPosition = Array.from(player.position)
×
178
                        const heightOffset = 22 / 32 // Player spawn height is different from reported height. Offset by # fixed-point units.
×
179
                        reloadedPosition[1] -= heightOffset
×
180
                        this.loadPlayer(player, reloadedPosition, player.orientation)
×
181
                        player.droneTransmitter.resendDrones()
×
182
                })
×
183
        }
×
184
        /**Sets a block at the specified position in the level.
4✔
185
         *
4✔
186
         * @param {Vector3} position - The position to set the block at.
4✔
187
         * @param {number} block - The block type to set.
4✔
188
         * @param {BasePlayer[]} [excludePlayers=[]] Default is `[]`
4✔
189
         * @param {boolean} [saveToRecord=true] Default is `true`
4✔
190
         */
4✔
191
        setBlock(position, block, excludePlayers = [], saveToRecord = true) {
4✔
192
                this.blocks.writeUInt8(block, position[0] + this.bounds[0] * (position[2] + this.bounds[2] * position[1]))
4✔
193
                this.players.forEach((player) => {
4✔
194
                        if (!excludePlayers.includes(player)) player.client.setBlock(block, ...position)
×
195
                })
4✔
196
                if (saveToRecord) {
4✔
197
                        this.changeRecord.addBlockChange(position, block)
4✔
198
                        if (!this.changeRecord.draining && this.changeRecord.currentActionCount > 1024) this.changeRecord.flushChanges()
4!
199
                }
4✔
200
        }
4✔
201
        /**Sets a block at the specified position in the level without notifying players.
4✔
202
         *
4✔
203
         * @param {Vector3} position - The position to set the block at.
4✔
204
         * @param {number} block - The block type to set.
4✔
205
         */
4✔
206
        rawSetBlock(position, block) {
4✔
207
                this.blocks.writeUInt8(block, position[0] + this.bounds[0] * (position[2] + this.bounds[2] * position[1]))
×
208
        }
×
209
        /**Gets the block type at the specified position in the level.
4✔
210
         *
4✔
211
         * @param {Vector3} position - The position to get the block from.
4✔
212
         * @returns {number} The block type at the specified position.
4✔
213
         */
4✔
214
        getBlock(position) {
4✔
215
                return this.blocks.readUInt8(position[0] + this.bounds[0] * (position[2] + this.bounds[2] * position[1]))
8✔
216
        }
8✔
217
        /**Checks if a position is within the level bounds.
4✔
218
         *
4✔
219
         * @param {Vector3} position - The position to check.
4✔
220
         * @returns {boolean} Whether the position is within the level bounds.
4✔
221
         */
4✔
222
        withinLevelBounds(position) {
4✔
223
                if (position.some((num) => isNaN(num))) return false
×
224
                if (position[0] < 0 || position[1] < 0 || position[2] < 0) return false
×
225
                if (position[0] >= this.bounds[0] || position[1] >= this.bounds[1] || position[2] >= this.bounds[2]) return false
×
226
                return true
×
227
        }
×
228
        /**Checks if a user has permission to edit the level.
4✔
229
         *
4✔
230
         * @param {string} username - The username to check.
4✔
231
         * @returns {boolean} Whether the user has permission.
4✔
232
         */
4✔
233
        userHasPermission(username) {
4✔
234
                if (this.allowList.length == 0) return true
×
235
                if (this.allowList.includes(username)) return true
×
236
                return false
×
237
        }
×
238
        /**Gets a {link LevelCommand | command} class by its name or alias.
4✔
239
         *
4✔
240
         * @todo LevelCommand isn't in classicborne yet.
4✔
241
         *
4✔
242
         * @param {string} command
4✔
243
         * @returns {LevelCommand} The command class, or `null` if not found.
4✔
244
         */
4✔
245
        static getCommandClassFromName(command) {
4✔
246
                command = command.split(" ")
×
247
                const commandName = command[0].toLowerCase()
×
248
                let commandClass = this.commands.find((otherCommand) => otherCommand.name.toLowerCase() == commandName)
×
249
                if (!commandClass) commandClass = this.commands.find((otherCommand) => otherCommand.aliases.includes(commandName))
×
250
                return commandClass
×
251
        }
×
252
        /**Destroys the level, releasing any resources used for it.
4✔
253
         *
4✔
254
         * @param {boolean} [saveChanges=true] If true, I will flush any pending block changes to my {@link ChangeRecord} before disposing it. Default is `true`
4✔
255
         */
4✔
256
        async dispose(saveChanges = true) {
4✔
257
                if (!this.changeRecord.draining && this.changeRecord.dirty && saveChanges) {
×
258
                        await this.changeRecord.flushChanges()
×
259
                }
×
260
                await this.changeRecord.dispose()
×
261
                this.emit("unloaded")
×
262
                this.removeAllListeners()
×
263
                this.commandInterpreter.dispose()
×
264
        }
×
265
        /**Sends a block set to a client.
4✔
266
         *
4✔
267
         * @param {Client} client - The client to send the block set to.
4✔
268
         * @param {number[][]} blockset - The block set to send.
4✔
269
         */
4✔
270
        static sendBlockset(client, blockset) {
4✔
271
                for (let i = 0; i < blockset.length; i++) {
×
272
                        let walkSound = 5
×
273
                        let texture = 79
×
274
                        if (blockset[i][3] == 3) {
×
275
                                walkSound = 6
×
276
                                texture = 51
×
277
                        }
×
278
                        const block = {
×
279
                                id: i + 1,
×
280
                                name: `${blockset[i]
×
281
                                        .slice(0, 3)
×
282
                                        .map((component) => componentToHex(component))
×
283
                                        .join("")}#`,
×
284
                                fogDensity: 127,
×
285
                                fogR: blockset[i][0],
×
286
                                fogG: blockset[i][1],
×
287
                                fogB: blockset[i][2],
×
288
                                draw: blockset[i][3],
×
289
                                walkSound,
×
290
                                topTexture: texture,
×
291
                                leftTexture: texture,
×
292
                                rightTexture: texture,
×
293
                                frontTexture: texture,
×
294
                                backTexture: texture,
×
295
                                bottomTexture: texture,
×
296
                                transmitLight: 1,
×
297
                        }
×
298
                        client.extensions.get("BlockDefinitions").defineBlock(block)
×
299
                        client.extensions.get("BlockDefinitionsExtended").defineBlock(block)
×
300
                }
×
301
        }
×
302
        /**Loads a level into a {@link BaseUniverse} instance, creating it if it doesn't exist.
4✔
303
         *
4✔
304
         * @param {BaseUniverse} universe - The universe to load the level into.
4✔
305
         * @param {string} spaceName - The identifier of the level. This will be used for {@link BaseUniverse.levels}.
4✔
306
         * @param {Object} defaults - The default properties for the level.
4✔
307
         * @returns {Promise<BaseLevel>} A promise that resolves to the loaded level.
4✔
308
         */
4✔
309
        static async loadIntoUniverse(universe, spaceName, defaults) {
4✔
310
                const cached = universe.levels.get(spaceName)
×
311
                if (cached) return cached
×
312
                const bounds = defaults.bounds ?? this.bounds
×
313
                const template = defaults.template ?? this.template
×
314
                const templateBlocks = Buffer.from(await template.generate(bounds))
×
315
                const promise = new Promise(async (resolve) => {
×
316
                        const levelClass = defaults.levelClass ?? this
×
317
                        const level = new levelClass(bounds, templateBlocks, ...(defaults.arguments ?? []))
×
318
                        level.template = template
×
319
                        level.name = spaceName
×
320
                        level.blockset = defaults.blockset ?? this.blockset
×
321
                        level.environment = defaults.environment ?? this.environment
×
322
                        level.texturePackUrl = defaults.texturePackUrl ?? universe.serverConfiguration.texturePackUrl
×
323
                        level.allowList = defaults.allowList ?? []
×
324
                        level.universe = universe
×
325
                        let changeRecordClass
×
326
                        if (defaults.useNullChangeRecord) {
×
327
                                changeRecordClass = await import("./changeRecord/NullChangeRecord.mjs").then((module) => module.NullChangeRecord)
×
328
                        } else {
×
329
                                changeRecordClass = await import("./changeRecord/ChangeRecord.mjs").then((module) => module.ChangeRecord)
×
330
                        }
×
331
                        level.changeRecord = new changeRecordClass(`./blockRecords/${spaceName}/`, async () => {
×
332
                                await level.changeRecord.restoreBlockChangesToLevel(level)
×
333
                                level.emit("loaded")
×
334
                                resolve(level)
×
335
                        })
×
336
                })
×
337
                universe.levels.set(spaceName, promise)
×
338
                return promise
×
339
        }
×
340
        /**Teleports the player into the level. If level currently doesn't exist in universe, it'll be created.
4✔
341
         *
4✔
342
         * Levels extending Level are expected to override this method using this pattern:
4✔
343
         *
4✔
344
         * ```js
4✔
345
         *  static async teleportPlayer(player, spaceName) {
4✔
346
         *          if (super.teleportPlayer(player) === false) return // Removes player from any levels they are in. If it returns false, the player is still being teleported somewhere.
4✔
347
         *          Level.loadIntoUniverse(player.universe, spaceName, { // Create the level using its desired defaults.
4✔
348
         *                 levelClass: HubLevel,
4✔
349
         *          }).then(async (level) => { // Add player after it loads.
4✔
350
         *                  level.addPlayer(player, [60, 8, 4], [162, 254])
4✔
351
         *          })
4✔
352
         *  }
4✔
353
         * ```
4✔
354
         *
4✔
355
         * @param {BasePlayer} player - The player to teleport.
4✔
356
         * @param {string | null} [spaceName] - The identifier of the level to teleport to. Default is `null`
4✔
357
         * @param {{}?} [defaults={}] Default is `{}`
4✔
358
         */
4✔
359
        static async teleportPlayer(player, spaceName, defaults = {}) {
4✔
360
                if (player) {
×
361
                        if (player.teleporting == true) return false
×
362
                        player.teleporting = true
×
363
                        if (player.space) player.space.removePlayer(player)
×
364
                }
×
365

×
366
                if (this === BaseLevel) {
×
367
                        BaseLevel.loadIntoUniverse(player.universe, spaceName, defaults).then(async (level) => {
×
368
                                level.addPlayer(player, [60, 8, 4], [162, 254])
×
369
                        })
×
370
                }
×
371
        }
×
372
        /** @type {Vector3} */
4✔
373
        static bounds = [64, 64, 64]
4✔
374
        /**Defines the environmental properties of the level, such as sky and cloud settings.
4✔
375
         *
4✔
376
         * @todo Expose types from classicborne-server-protocol for better documentation.
4✔
377
         */
4✔
378
        static environment = {
4✔
379
                sidesId: 7,
4✔
380
                edgeId: 250,
4✔
381
                edgeHeight: 0,
4✔
382
                cloudsHeight: 256,
4✔
383
        }
4✔
384
        /**Defines the block set used in the level.
4✔
385
         *
4✔
386
         * @type {number[][]}
4✔
387
         */
4✔
388
        static blockset = []
4✔
389
        /** The template used to generate the level's blocks. */
4✔
390
        static template = new EmptyTemplate()
4✔
391
        /**Deprecated. Use {@link BaseLevel.bounds} instead.
4✔
392
         *
4✔
393
         * @deprecated
4✔
394
         */
4✔
395
        static standardBounds = this.bounds
4✔
396
        /**The {link LevelCommand | level commands} available in the level.
4✔
397
         *
4✔
398
         * @todo LevelCommand isn't in classicborne yet.
4✔
399
         */
4✔
400
        static commands = []
4✔
401
        /** The default {@link BaseLevelCommandInterpreter} class for the level. */
4✔
402
        static commandInterpreterClass = BaseLevelCommandInterpreter
4✔
403
}
4✔
404

4✔
405
export default BaseLevel
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc