UNPKG

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