UNPKG

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