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

BunnyNabbit / classicborne / 20791765724

07 Jan 2026 06:14PM UTC coverage: 52.797% (+0.04%) from 52.755%
20791765724

Pull #17

github

web-flow
Merge 84c147c53 into ee3912173
Pull Request #17: Use player.getDisplayUsername for drones and list

114 of 118 branches covered (96.61%)

Branch coverage included in aggregate %.

5 of 12 new or added lines in 3 files covered. (41.67%)

858 of 1723 relevant lines covered (49.8%)

894.62 hits per line

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

56.72
/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

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

×
329
                if (this === BaseLevel) {
×
330
                        BaseLevel.loadIntoUniverse(player.universe, spaceName, defaults).then(async (level) => {
×
331
                                level.addPlayer(player, [60, 8, 4], [162, 254])
×
332
                        })
×
333
                }
×
334
        }
×
335
        /** @type {Vector3} */
4✔
336
        static bounds = [64, 64, 64]
4✔
337
        /** @todo Yet to be documented. */
4✔
338
        static environment = {
4✔
339
                sidesId: 7,
4✔
340
                edgeId: 250,
4✔
341
                edgeHeight: 0,
4✔
342
                cloudsHeight: 256,
4✔
343
        }
4✔
344
        /** @todo Yet to be documented. */
4✔
345
        static blockset = []
4✔
346
        /** @todo Yet to be documented. */
4✔
347
        static template = new EmptyTemplate()
4✔
348
        /**@todo Yet to be documented.
4✔
349
         *
4✔
350
         * @deprecated
4✔
351
         * @see {BaseLevel.bounds}
4✔
352
         */
4✔
353
        static standardBounds = this.bounds
4✔
354
        /** @todo Yet to be documented. */
4✔
355
        static commands = []
4✔
356
        /** @todo Yet to be documented. */
4✔
357
        static commandInterpreterClass = BaseLevelCommandInterpreter
4✔
358
}
4✔
359

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

© 2026 Coveralls, Inc