'use strict';
const GameEntity = require('./GameEntity');
const AreaFloor = require('./AreaFloor');
/**
* Representation of an in game area
* See the {@link http://ranviermud.com/extending/areas/|Area guide} for documentation on how to
* actually build areas.
*
* @property {string} bundle Bundle this area comes from
* @property {string} name
* @property {string} title
* @property {string} script A custom script for this item
* @property {Map} map a Map object keyed by the floor z-index, each floor is an array with [x][y] indexes for coordinates.
* @property {Map<string, Room>} rooms Map of room id to Room
* @property {Set<Npc>} npcs Active NPCs that originate from this area. Note: this is NPCs that
* _originate_ from this area. An NPC may not actually be in this area at any given moment.
* @property {Object} info Area configuration
* @property {Number} lastRespawnTick milliseconds since last respawn tick. See {@link Area#update}
*
* @extends GameEntity
*/
class Area extends GameEntity {
constructor(bundle, name, manifest) {
super();
this.bundle = bundle;
this.name = name;
this.title = manifest.title;
this.metadata = manifest.metadata || {};
this.rooms = new Map();
this.npcs = new Set();
this.map = new Map();
this.script = manifest.script;
this.behaviors = new Map(Object.entries(manifest.behaviors || {}));
this.on('updateTick', state => {
this.update(state);
});
}
/**
* Get ranvier-root-relative path to this area
* @return {string}
*/
get areaPath() {
return `${this.bundle}/areas/${this.name}`;
}
/**
* Get an ordered list of floors in this area's map
* @return {Array<number>}
*/
get floors() {
return [...this.map.keys()].sort();
}
/**
* @param {string} id Room id
* @return {Room|undefined}
*/
getRoomById(id) {
return this.rooms.get(id);
}
/**
* @param {Room} room
* @fires Area#roomAdded
*/
addRoom(room) {
this.rooms.set(room.id, room);
if (room.coordinates) {
this.addRoomToMap(room);
}
/**
* @event Area#roomAdded
* @param {Room} room
*/
this.emit('roomAdded', room);
}
/**
* @param {Room} room
* @fires Area#roomRemoved
*/
removeRoom(room) {
this.rooms.delete(room.id);
/**
* @event Area#roomRemoved
* @param {Room} room
*/
this.emit('roomRemoved', room);
}
/**
* @param {Room} room
* @throws Error
*/
addRoomToMap(room) {
if (!room.coordinates) {
throw new Error('Room does not have coordinates');
}
const {x, y, z} = room.coordinates;
if (!this.map.has(z)) {
this.map.set(z, new AreaFloor(z));
}
const floor = this.map.get(z);
floor.addRoom(x, y, room);
}
/**
* find a room at the given coordinates for this area
* @param {number} x
* @param {number} y
* @param {number} z
* @return {Room|boolean}
*/
getRoomAtCoordinates(x, y, z) {
const floor = this.map.get(z);
return floor && floor.getRoom(x, y);
}
/**
* @param {Npc} npc
*/
addNpc(npc) {
this.npcs.add(npc);
}
/**
* Removes an NPC from the area. NOTE: This must manually remove the NPC from its room as well
* @param {Npc} npc
*/
removeNpc(npc) {
this.npcs.delete(npc);
}
/**
* This method is automatically called every N milliseconds where N is defined in the
* `setInterval` call to `GameState.AreaMAnager.tickAll` in the `ranvier` executable. It, in turn,
* will fire the `updateTick` event on all its rooms and npcs
*
* @param {GameState} state
* @fires Room#updateTick
* @fires Npc#updateTick
*/
update(state) {
for(const [id, room] of this.rooms) {
/**
* @see Area#update
* @event Room#updateTick
*/
room.emit('updateTick');
}
for (const npc of this.npcs) {
/**
* @see Area#update
* @event Npc#updateTick
*/
npc.emit('updateTick');
}
}
hydrate(state) {
this.setupBehaviors(state.AreaBehaviorManager);
const { rooms } = state.AreaFactory.getDefinition(this.name);
for (const roomRef of rooms) {
const room = state.RoomFactory.create(this, roomRef);
this.addRoom(room);
state.RoomManager.addRoom(room);
room.hydrate(state);
/**
* Fires after the room is hydrated and added to its area
* @event Room#ready
*/
room.emit('ready');
}
}
/**
* Get all possible broadcast targets within an area. This includes all npcs,
* players, rooms, and the area itself
* @return {Array<Broadcastable>}
*/
getBroadcastTargets() {
const roomTargets = [...this.rooms].reduce((acc, [, room]) => acc.concat(room.getBroadcastTargets()), []);
return [this, ...roomTargets];
}
}
module.exports = Area;