UNPKG

14.3 kBJavaScriptView Raw
1/**
2* @overview AdvTxt is a text adventure engine, written in Javascript on Node.js.
3*
4* @author Nathan Wittstock <code@fardogllc.com>
5* @license MIT License - See file 'LICENSE' in this project.
6* @version 0.1.0
7* @extends EventEmitter
8*/
9'use strict';
10
11var i18n = new (require('i18n-2'))({ locales: ['en']});
12var _ = require('underscore');
13var Hoek = require('hoek');
14var Parser = require('./parser');
15var events = require('events');
16var util = require('util');
17var async = require('async');
18var debug = require('debug')('advtxt');
19
20var advtxt = {};
21
22
23/**
24 * Constructs a new AdvTxt Server.
25 *
26 * @since 0.0.1
27 * @constructor
28 */
29exports = module.exports = advtxt.Server = function() {
30 var self = this;
31
32 events.EventEmitter.call(self);
33
34 self.db = null;
35 self.initialized = false;
36}
37
38// Extend EventEmitter
39util.inherits(advtxt.Server, events.EventEmitter);
40
41/**
42 * Initializes AdvTxt
43 *
44 * @since 0.0.3
45 * @param {advtxtdb} db - AdvTxt DB adapter.
46 */
47advtxt.Server.prototype.initialize = function(db) {
48 var self = this;
49
50 self.db = db;
51 self.initialized = true;
52};
53
54
55/**
56 * Adds a reply to the replies array and emits an event
57 *
58 * @since 0.0.6
59 * @param {command} command - The Command object.
60 * @param {string} reply - The reply to be added.
61 *
62 */
63advtxt.Server.prototype.reply = function(command, reply) {
64 var self = this;
65
66 command.replies.push(reply);
67 self.emit('reply', reply);
68
69 return;
70};
71
72/**
73 * Moves the player's coordinates to another room, and then passes that off to
74 * the function that actually gets the room.
75 *
76 * @since 0.1.0
77 * @param {string} direction - A string representation of the direction we're
78 * moving in.
79 */
80advtxt.Server.prototype.calculateMove = function(direction) {
81 var self = this;
82
83 var move = [];
84 // right now we only process cardinal directions
85 if (direction === i18n.__('north') || direction === 'n')
86 move = [0, -1];
87 else if (direction === i18n.__('south') || direction === 's')
88 move = [0, 1];
89 else if (direction === i18n.__('east') || direction === 'e')
90 move = [1, 0];
91 else if (direction === i18n.__('west') || direction === 'w')
92 move = [-1, 0];
93 else
94 move = [0, 0];
95
96 // now we apply those moves to the player object
97 return move;
98}
99
100/**
101 * Resets the player within the map, optionally clearing all of their items and
102 * positioning them in the 0,0 spot.
103 *
104 * @since 0.0.2
105 * @param {command} command - The command object.
106 * @param {boolean} clearItems - Should we clear their items or not?
107 * @param {advTxtCallback} next - Function to call next
108 */
109advtxt.Server.prototype.resetPlayer = function(command, clearItems, next) {
110 var self = this;
111
112 // set player's position to 0,0
113 command.player.x = 0;
114 command.player.y = 0;
115
116 command.player.status = "alive";
117 command.move = true;
118
119 // output a different message if we're just moving, or if fully resetting
120 if (typeof clearItems !== 'undefined' && clearItems) {
121 command.player.items = {}; // clear the player's items array
122 command.items = true;
123 self.reply(command, i18n.__("Giving you a blank slate…"));
124 }
125 else {
126 self.reply(command, i18n.__("Moving you to the origin room…"));
127 }
128
129 debug('Now resetting player.');
130 // first we need to reset their position
131 self.updatePlayerPosition(command, function(err, command) {
132 var self = this;
133 debug('Position reset.');
134 // the position has been reset. Clear their items if requested.
135 self.updatePlayerItems(command, function(err, command) {
136 debug('Items cleared.');
137 next(null, command);
138 });
139 }.bind(self));
140}
141
142/**
143 * Standard callback for AdvTxt
144 *
145 * @since 0.1.0
146 * @callback advTxtCallback
147 * @param {Error} err - Error object if there was one, else null.
148 * @param {command} command - The command object.
149 */
150
151/**
152 * Updates a player's position in the database.
153 *
154 * @since 0.0.2
155 * @param {command} command - The representation of a command.
156 * @param {advTxtCallback} next
157 */
158advtxt.Server.prototype.updatePlayerPosition = function(command, next) {
159 var self = this;
160
161 debug("Updating Position");
162 var selector = {_id: command.player._id};
163 var data = {x: command.player.x, y: command.player.y};
164
165
166 self.db.update('player', selector, data, function(err, success){
167 if (err) next(err, command);
168 // if we were successful, lets update their location and send a response
169 if (!success) {
170 console.error("Couldn't update player position.");
171 console.error(command.player);
172 }
173
174 debug("Successfully updated position.");
175 next(null, command);
176 }.bind(self));
177}
178
179/**
180 * Updates the player's item collection in the database
181 *
182 * @since 0.0.1
183 * @param {command} command - The command object
184 * @param {advTxtCallback} next
185 */
186advtxt.Server.prototype.updatePlayerItems = function(command, next) {
187 var self = this;
188
189 var selector = {_id: command.player._id};
190 var data = {items: command.player.items};
191
192 self.db.update('player', selector, data, function(err, success) {
193 if (err) next(err, command);
194
195 // the player is saved, do nothing unless there's an error
196 if (!success) {
197 console.error("Couldn't save player: " + command.player.username);
198 console.error(command.player);
199 }
200
201 next(null, command);
202 }.bind(self));
203}
204
205/**
206 * Updates the player's status in the database
207 *
208 * @since 0.0.2
209 * @param {command} command - The command object
210 * @param {advTxtCallback} next
211 */
212advtxt.Server.prototype.updatePlayerStatus = function(command, next) {
213 var self = this;
214
215 var selector = {_id: command.player._id};
216 var data = {status: command.player.status};
217
218 self.db.update('player', selector, data, function(err, success) {
219 if (err) next(err, command);
220
221 if (!success) {
222 console.error("Couldn't save player: " + command.player.username);
223 console.error(command.player);
224 }
225
226 if (typeof next === 'function' && next) next(null, command);
227 }.bind(self));
228}
229
230
231/**
232 * Process a command that was received from the player.
233 *
234 * @since 0.0.1
235 * @param {command} command - The command object
236 * @param {advTxtCallback} next
237 */
238advtxt.Server.prototype.doCommand = function(command, next) {
239 var self = this;
240
241 var parser = new Parser(command);
242 if (!parser.fail) {
243 command.command = parser.command;
244 }
245 else {
246 next(new Error(__.i18n("I don't know what you mean.")), command);
247 }
248
249 // make things less verbose
250 var verb = command.command.verb;
251 var object = command.command.object;
252 var player = command.player;
253 var room = player.room;
254 var commands = parser.commands;
255
256 // if we've already performed the command
257 if (command.performed) {
258 next(null, command);
259 }
260
261 // set the command as performed
262 command.performed = true;
263
264 // we need to check for reset commands first, so we don't ignore them from a
265 // dead player
266 if (verb === commands.RESET && object === '') {
267 self.resetPlayer(command, false, next);
268 return;
269 }
270 else if (verb === commands.RESET && object === i18n.__('all')) {
271 self.resetPlayer(command, true, next);
272 return;
273 }
274 else if (verb === commands.GET) {
275 // would try to get an item
276 if (typeof room.items[object] !== 'undefined') {
277 var available = room.items[object].available(player);
278 if (available === true) {
279 // that item was available. get the item
280 player.items[object] = room.items[object].name;
281 self.reply(command, room.items[object].get(player));
282 // set the player's items as changed
283 command.items = true;
284 }
285 // that item wasn't available
286 else {
287 self.reply(command, available);
288 }
289 }
290 // there wasn't an item by that name
291 else {
292 self.reply(command, i18n.__('You can\'t find a "%s" in this room!', object));
293 }
294 }
295 else if (verb === commands.GO) {
296 // would try to move in a direction
297 if (typeof room.exits[object] !== 'undefined') {
298 var available = room.exits[object].available(player);
299 if (available === true) {
300 // that direction was available, play the "go" message, then move them
301 self.reply(command, room.exits[object].go(player));
302
303 // set the player to be moved
304 var move = self.calculateMove(object);
305
306 command.player.x += move[0];
307 command.player.y += move[1];
308
309 command.move = true;
310
311 // set the room to be announced
312 command.announceRoom = true;
313 }
314 // that direction wasn't available; give the reason
315 else {
316 self.reply(command, available);
317 }
318 }
319 // there wasn't a direction by that name
320 else {
321 // TODO give customized replies for actual directions
322 self.reply(command, i18n.__('You can\'t go "%s", it just doesn\'t work.', object));
323 }
324 }
325 // otherwise, try to run the command from our possible ones
326 else if (typeof room.commands[verb] !== 'undefined') {
327 self.reply(command, room.commands[verb](player));
328 }
329 // if they asked for the exits, list them
330 else if (verb === commands.EXITS) {
331 var exits = [];
332 for (var key in room.exits) {
333 exits.push(room.exits[key].name);
334 }
335
336 var exitNames = i18n.__('Available exits: ');
337 for (var i = 0; i < exits.length; i++) {
338 exitNames += exits[i];
339 if (i !== exits.length - 1) exitNames += i18n.__(", ");
340 }
341 self.reply(command, exitNames);
342 }
343 else {
344 self.reply(command, i18n.__('Sorry, I don\'t know how to "%s" in this room.', verb));
345 }
346
347 next(null, command);
348}
349
350/**
351 * Gets the room a player is in, and inserts that room into the command object.
352 * Responds to the player if they just entered the room.
353 *
354 * @since 0.0.1
355 * @param {command} command - The command object.
356 * @param {advTxtCallback} next
357 */
358advtxt.Server.prototype.getCurrentLocation = function(command, next) {
359 var self = this;
360
361 // save our player's status if we haven't seen it yet. this is so we can
362 // check for dead/win later
363 if (typeof command.status === 'undefined')
364 command.status = Hoek.clone(command.player.status);
365
366 self.db.findOne('room', {x: command.player.x, y: command.player.y, map: command.player.map}, function (err, room) {
367 if (err) next(err, command);
368
369 if (room) {
370 // we assign the room into the command object, so we remember everything
371 // about it
372 command.player.room = room;
373 // then we get the player into a local var, all of the commands that we
374 // eval use "player.room.xxxxx" as their namespace, so this will ensure
375 // they're assigned properly
376 var player = command.player;
377
378 // now we need to eval what was in the db
379 eval(player.room.commands);
380 eval(player.room.items);
381 eval(player.room.exits);
382
383 // if we just entered the room, we need to reply with its description and
384 // just exit afterward.
385 if (command.announceRoom) {
386 command.announceRoom = false;
387 self.reply(command, player.room.description);
388 }
389 // otherwise, process the command that was given
390 next(null, command);
391 }
392 });
393}
394
395/**
396 * Gets the player that is addressed in the command.
397 *
398 * @since 0.1.0
399 * @param {command} command - The command object.
400 * @param {advTxtCallback} next
401 */
402advtxt.Server.prototype.getPlayer = function(command, next) {
403 var self = this;
404
405 self.db.findOne('player', {username: command.player, map: "default"}, function(err, player) {
406 if (err) next(err, command);
407
408 if (player) {
409 command.player = player;
410 next(null, command);
411 }
412 else {
413 self.db.insertOne('player', {
414 username: command.player,
415 map: "default",
416 x: 0,
417 y: 0,
418 status: "alive",
419 room: {},
420 items: {}
421 }, function(err, player) {
422 if (err) throw err;
423
424 command.player = player;
425 command.announceRoom = true;
426
427 next(null, command);
428 });
429 }
430 });
431};
432
433/**
434 * Takes a raw command and runs it through the AdvTxt engine.
435 *
436 * @since 0.0.1
437 * @param {command} command - The command object.
438 */
439advtxt.Server.prototype.processCommand = function(command) {
440 var self = this;
441
442 // we haven't performed the command yet
443 command.performed = false;
444
445 async.waterfall([
446 function(next) {
447 // get the current player, we also need to pass the command object
448 self.getPlayer(command, next);
449 },
450 // get the player's location
451 self.getCurrentLocation.bind(self),
452 // perform their command
453 self.doCommand.bind(self),
454 // update their items if necessary
455 function(command, next) {
456 if (command.items) {
457 command.items = false;
458 self.updatePlayerItems(command, next);
459 }
460 else {
461 next(null, command);
462 }
463 },
464 // update their position if necessary
465 function(command, next) {
466 if (command.move) {
467 command.move = false;
468 self.updatePlayerPosition(command, next);
469 }
470 else {
471 next(null, command);
472 }
473 },
474 // get their location again, if we need to announce it
475 function(command, next) {
476 if (command.announceRoom) {
477 self.getCurrentLocation(command, next);
478 }
479 else {
480 next(null, command);
481 }
482 }
483 ], self.finalize.bind(self));
484
485
486}
487
488/**
489 * Cleans up at the end of a player command or move. Handles death/win messages
490 *
491 * @param {Error} err - An error message, or null
492 * @param {command} command - The command object.
493 */
494advtxt.Server.prototype.finalize = function(err, command) {
495 var self = this;
496
497 debug("Command Finalized");
498
499 if (err) {
500 console.error("An error occurred. What follows is the error, and the command");
501 console.error(err);
502 console.error(JSON.stringify(command));
503 }
504 else {
505 // this was the first time we saw the status as different, we need to send
506 // the first time message and save the player.
507 if (command.status !== command.player.status) {
508 if (command.player.status === 'dead') {
509 self.reply(command, i18n.__('You\'ve died! Send the command `reset all` to try again!'));
510 }
511 else if (command.player.status === 'win') {
512 self.reply(command, i18n.__('You\'ve won the game! How impressive! Send the command `reset all` to play again!'));
513 }
514 self.updatePlayerStatus(command);
515 }
516 else if (command.player.status === 'dead') {
517 self.reply(command, i18n.__('You\'re still dead! Send the command `reset all` to try again!'));
518 }
519 else if (command.player.status === 'win') {
520 self.reply(command, i18n.__('Yes, your win was glorious, but send the command `reset all` to play again!'));
521 }
522 }
523
524 // tell the original caller that we're done
525 command.done(command);
526}
527