'use strict';
const GameEntity = require('./GameEntity');
const Logger = require('./Logger');
/**
* @property {Area} area Area room is in
* @property {{x: number, y: number, z: number}} [coordinates] Defined in yml with array [x, y, z]. Retrieved with coordinates.x, coordinates.y, ...
* @property {Array<number>} defaultItems Default list of item ids that should load in this room
* @property {Array<number>} defaultNpcs Default list of npc ids that should load in this room
* @property {string} description Room description seen on 'look'
* @property {Array<object>} exits Exits out of this room { id: number, direction: string }
* @property {number} id Area-relative id (vnum)
* @property {Set} items Items currently in the room
* @property {Set} npcs Npcs currently in the room
* @property {Set} players Players currently in the room
* @property {string} script Name of custom script attached to this room
* @property {string} title Title shown on look/scan
* @property {object} doors Doors restricting access to this room. See documentation for format
*
* @extends GameEntity
*/
class Room extends GameEntity {
constructor(area, def) {
super();
const required = ['title', 'description', 'id'];
for (const prop of required) {
if (!(prop in def)) {
throw new Error(`ERROR: AREA[${area.name}] Room does not have required property ${prop}`);
}
}
this.def = def;
this.area = area;
this.defaultItems = def.items || [];
this.defaultNpcs = def.npcs || [];
this.metadata = def.metadata || {};
this.script = def.script;
this.behaviors = new Map(Object.entries(def.behaviors || {}));
this.coordinates = Array.isArray(def.coordinates) && def.coordinates.length === 3 ? {
x: def.coordinates[0],
y: def.coordinates[1],
z: def.coordinates[2],
} : null;
this.description = def.description;
this.entityReference = this.area.name + ':' + def.id;
this.exits = def.exits || [];
this.id = def.id;
this.title = def.title;
// create by-val copies of the doors config so the lock/unlock don't accidentally modify the original definition
this.doors = new Map(Object.entries(JSON.parse(JSON.stringify(def.doors || {}))));
this.defaultDoors = def.doors;
this.items = new Set();
this.npcs = new Set();
this.players = new Set();
/**
* spawnedNpcs keeps track of NPCs even when they leave the room for the purposes of respawn. So if we spawn NPC A
* into the room and it walks away we don't want to respawn the NPC until it's killed or otherwise removed from the
* area
*/
this.spawnedNpcs = new Set();
}
/**
* Emits event on self and proxies certain events to other entities in the room.
* @param {string} eventName
* @param {...*} args
* @return {void}
*/
emit(eventName, ...args) {
super.emit(eventName, ...args);
const proxiedEvents = [
'playerEnter',
'playerLeave',
'npcEnter',
'npcLeave'
];
if (proxiedEvents.includes(eventName)) {
const entities = [...this.npcs, ...this.players, ...this.items];
for (const entity of entities) {
entity.emit(eventName, ...args);
}
}
}
/**
* @param {Player} player
*/
addPlayer(player) {
this.players.add(player);
}
/**
* @param {Player} player
*/
removePlayer(player) {
this.players.delete(player);
}
/**
* @param {Npc} npc
*/
addNpc(npc) {
this.npcs.add(npc);
npc.room = this;
this.area.addNpc(npc);
}
/**
* @param {Npc} npc
* @param {boolean} removeSpawn
*/
removeNpc(npc, removeSpawn = false) {
this.npcs.delete(npc);
if (removeSpawn) {
this.spawnedNpcs.delete(npc);
}
npc.room = null;
}
/**
* @param {Item} item
*/
addItem(item) {
this.items.add(item);
item.room = this;
}
/**
* @param {Item} item
*/
removeItem(item) {
this.items.delete(item);
item.room = null;
}
/**
* Get exits for a room. Both inferred from coordinates and defined in the
* 'exits' property.
*
* @return {Array<{ id: string, direction: string, inferred: boolean, room: Room= }>}
*/
getExits() {
const exits = JSON.parse(JSON.stringify(this.exits)).map(exit => {
exit.inferred = false;
return exit;
});
if (!this.area || !this.coordinates) {
return exits;
}
const adjacents = [
{ dir: 'west', coord: [-1, 0, 0] },
{ dir: 'east', coord: [1, 0, 0] },
{ dir: 'north', coord: [0, 1, 0] },
{ dir: 'south', coord: [0, -1, 0] },
{ dir: 'up', coord: [0, 0, 1] },
{ dir: 'down', coord: [0, 0, -1] },
{ dir: 'northeast', coord: [1, 1, 0] },
{ dir: 'northwest', coord: [-1, 1, 0] },
{ dir: 'southeast', coord: [1, -1, 0] },
{ dir: 'southwest', coord: [-1, -1, 0] },
];
for (const adj of adjacents) {
const [x, y, z] = adj.coord;
const room = this.area.getRoomAtCoordinates(
this.coordinates.x + x,
this.coordinates.y + y,
this.coordinates.z + z
);
if (room && !exits.find(ex => ex.direction === adj.dir)) {
exits.push({ roomId: room.entityReference, direction: adj.dir, inferred: true });
}
}
return exits;
}
/**
* Get the exit definition of a room's exit by searching the exit name
* @param {string} exitName exit name search
* @return {false|Object}
*/
findExit(exitName) {
const exits = this.getExits();
if (!exits.length) {
return false;
}
const roomExit = exits.find(ex => ex.direction.indexOf(exitName) === 0);
return roomExit || false;
}
/**
* Get the exit definition of a room's exit to a given room
* @param {Room} nextRoom
* @return {false|Object}
*/
getExitToRoom(nextRoom) {
const exits = this.getExits();
if (!exits.length) {
return false;
}
const roomExit = exits.find(ex => ex.roomId === nextRoom.entityReference);
return roomExit || false;
}
/**
* Check to see if this room has a door preventing movement from `fromRoom` to here
* @param {Room} fromRoom
* @return {boolean}
*/
hasDoor(fromRoom) {
return this.doors.has(fromRoom.entityReference);
}
/**
* @param {Room} fromRoom
* @return {{lockedBy: EntityReference, locked: boolean, closed: boolean}}
*/
getDoor(fromRoom) {
if (!fromRoom) {
return null;
}
return this.doors.get(fromRoom.entityReference);
}
/**
* Check to see of the door for `fromRoom` is locked
* @param {Room} fromRoom
* @return {boolean}
*/
isDoorLocked(fromRoom) {
const door = this.getDoor(fromRoom);
if (!door) {
return false;
}
return door.locked;
}
/**
* @param {Room} fromRoom
*/
openDoor(fromRoom) {
const door = this.getDoor(fromRoom);
if (!door) {
return;
}
door.closed = false;
}
/**
* @param {Room} fromRoom
*/
closeDoor(fromRoom) {
const door = this.getDoor(fromRoom);
if (!door) {
return;
}
door.closed = true;
}
/**
* @param {Room} fromRoom
*/
unlockDoor(fromRoom) {
const door = this.getDoor(fromRoom);
if (!door) {
return;
}
door.locked = false;
}
/**
* @param {Room} fromRoom
*/
lockDoor(fromRoom) {
const door = this.getDoor(fromRoom);
if (!door) {
return;
}
this.closeDoor(fromRoom);
door.locked = true;
}
/**
* @param {GameState} state
* @param {string} entityRef
* @return {Item} The newly created item
*/
spawnItem(state, entityRef) {
Logger.verbose(`\tSPAWN: Adding item [${entityRef}] to room [${this.title}]`);
const newItem = state.ItemFactory.create(this.area, entityRef);
newItem.hydrate(state);
newItem.sourceRoom = this;
state.ItemManager.add(newItem);
this.addItem(newItem);
/**
* @event Item#spawn
*/
newItem.emit('spawn');
return newItem;
}
/**
* @param {GameState} state
* @param {string} entityRef
* @fires Npc#spawn
* @return {Npc}
*/
spawnNpc(state, entityRef) {
Logger.verbose(`\tSPAWN: Adding npc [${entityRef}] to room [${this.title}]`);
const newNpc = state.MobFactory.create(this.area, entityRef);
newNpc.hydrate(state);
newNpc.sourceRoom = this;
this.area.addNpc(newNpc);
this.addNpc(newNpc);
this.spawnedNpcs.add(newNpc);
/**
* @event Npc#spawn
*/
newNpc.emit('spawn');
return newNpc;
}
hydrate(state) {
this.setupBehaviors(state.RoomBehaviorManager);
/**
* Fires when the room is created but before it has hydrated its default
* contents. Use the `ready` event if you need default items to be there.
* @event Room#spawn
*/
this.emit('spawn');
this.items = new Set();
// NOTE: This method effectively defines the fact that items/npcs do not
// persist through reboot unless they're stored on a player.
// If you would like to change that functionality this is the place
this.defaultItems.forEach(defaultItem => {
if (typeof defaultItem === 'string') {
defaultItem = { id: defaultItem };
}
this.spawnItem(state, defaultItem.id);
});
this.defaultNpcs.forEach(defaultNpc => {
if (typeof defaultNpc === 'string') {
defaultNpc = { id: defaultNpc };
}
try {
this.spawnNpc(state, defaultNpc.id);
} catch (err) {
Logger.error(err);
}
});
}
/**
* Used by Broadcaster
* @return {Array<Character>}
*/
getBroadcastTargets() {
return [this, ...this.players, ...this.npcs];
}
}
module.exports = Room;