From c8a3c8ce9ec0cc76999e0b1491458d5778812be9 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 15 Dec 2013 14:49:41 -0700 Subject: [PATCH] APIv2 some finalization of apiv2. Re-rigging some query/params vars, and bug-fixes --- dist/habitrpg-shared.js | 260 +++++++++++++++++++++++++++------------- script/index.coffee | 182 +++++++++++++++++++--------- 2 files changed, 303 insertions(+), 139 deletions(-) diff --git a/dist/habitrpg-shared.js b/dist/habitrpg-shared.js index 8bff9d9954..277259da30 100644 --- a/dist/habitrpg-shared.js +++ b/dist/habitrpg-shared.js @@ -10915,30 +10915,25 @@ var process=require("__browserify_process");(function() { * user.ops, which is super important: If a function is inside user.ops, it has magical properties. If you call it on the client it updates the user object in - the browser and when it's done it POSTs to the server, calling `src/controllers/user.js#OP_NAME` (the exact same name - of the op function). The first argument `req` is `{query, body, params}`, it's what the express controller function - expects. Because of this, we don't construct op functions in standard way (function(arg1,arg2,arg3){}), but instead - the first arg is {query, body, params} and the second is a callback(err, results). `query` is the most commonly-used, - but body is useful for updates/creates (eg, tasks), and params are used in those cases too (eg, {op:'updateTask',{params:{id:task.id}, body:task}}}) - This needs refinement: see http://stackoverflow.com/questions/4024271/rest-api-best-practices-where-to-put-parameters + the browser and when it's done it automatically POSTs to the server, calling src/controllers/user.js#OP_NAME (the exact same name + of the op function). The first argument req is {query, body, params}, it's what the express controller function + expects. This means we call our functions as if we were calling an Express route. Eg, instead of score(task, direction), + we call score({params:{id:task.id,direction:direction}}). This also forces us to think about our routes (whether to use + params, query, or body for variables). see http://stackoverflow.com/questions/4024271/rest-api-best-practices-where-to-put-parameters - If `src/controllers/user.js#OP_NAME` doesn't exist, it's automatically added. It runs the code in user.ops.OP_NAME + If `src/controllers/user.js#OP_NAME` doesn't exist on the server, it's automatically added. It runs the code in user.ops.OP_NAME to update the user model server-side, then performs `user.save()`. You can see this in action for `user.ops.buy`. That function doesn't exist on the server - so the client calls it, it updates user in the browser, auto-POSTs to server, server handles it by calling `user.ops.buy` again (to update user on the server), and then saves. We can do this for - everything that doesn't need any code different from what's in user.ops.OP_NAME for special-handling server-side. If we - *do* need special handling, just add `src/controllers/user.js#OP_NAME` to override the user.ops.OP_NAME, and just be + everything that doesn't need any code difference from what's in user.ops.OP_NAME for special-handling server-side. If we + *do* need special handling, just add `src/controllers/user.js#OP_NAME` to override the user.ops.OP_NAME, and be sure to call user.ops.OP_NAME at some point within the overridden function. TODO - * Migrate the remaining parts: Tags, party invitiations * Is this the best way to wrap the user object? I thought of using user.prototype, but user is an object not a Function. user on the server is a Mongoose model, so we can use prototype - but to do it on the client, we'd probably have to move to $resource for user * Move to $resource! - - BUGS - * Daily repeats don't have the days showing up (translations issue?) */ @@ -10948,23 +10943,105 @@ var process=require("__browserify_process");(function() { } user._wrapped = true; user.ops = { + update: function(req, cb) { + _.each(req.body, function(v, k) { + user.fns.dotSet(k, v); + return true; + }); + return typeof cb === "function" ? cb(null, req) : void 0; + }, sleep: function(req, cb) { user.preferences.sleep = !user.preferences.sleep; return cb(null, req); }, + revive: function(req, cb) { + var item, lostItem, lostStat; + _.merge(user.stats, { + hp: 50, + exp: 0, + gp: 0 + }); + if (user.stats.lvl > 1) { + user.stats.lvl--; + } + lostStat = user.fns.randomVal(_.reduce(['str', 'con', 'per', 'int'], (function(m, k) { + if (user.stats[k]) { + m[k] = k; + } + return m; + }), {})); + if (lostStat) { + user.stats[lostStat]--; + } + lostItem = user.fns.randomVal(_.reduce(user.items.gear.owned, (function(m, v, k) { + if (v) { + m["" + k] = "" + k; + } + return m; + }), {})); + if (item = content.gear.flat[lostItem]) { + user.items.gear.owned[lostItem] = false; + if (user.items.gear.equipped[item.type] === lostItem) { + user.items.gear.equipped[item.type] = "" + item.type + "_base_0"; + } + if (user.items.gear.costume[item.type] === lostItem) { + user.items.gear.costume[item.type] = "" + item.type + "_base_0"; + } + } + if (typeof user.markModified === "function") { + user.markModified('items.gear'); + } + return typeof cb === "function" ? cb((item ? { + code: 200, + message: "Your " + item.text + " broke." + } : null), req) : void 0; + }, + reset: function(req, cb) { + var gear; + user.habits = []; + user.dailys = []; + user.todos = []; + user.rewards = []; + user.stats.hp = 50; + user.stats.lvl = 1; + user.stats.gp = 0; + user.stats.exp = 0; + gear = user.items.gear; + _.each(['equipped', 'costume'], function(type) { + gear[type].armor = 'armor_base_0'; + gear[type].weapon = 'weapon_base_0'; + gear[type].head = 'head_base_0'; + return gear[type].shield = 'shield_base_0'; + }); + user.items.gear.owned = { + weapon_warrior_0: true + }; + if (typeof user.markModified === "function") { + user.markModified('items.gear.owned'); + } + user.preferences.costume = false; + return cb(null, req); + }, + reroll: function(req, cb) { + if (user.balance < 1) { + return cb({ + code: 401, + message: "Not enough gems." + }, req); + } + user.balance--; + _.each(user.tasks, function(task) { + return task.value = 0; + }); + user.stats.hp = 50; + return cb(null, req); + }, clearCompleted: function(req, cb) { user.todos = _.where(user.todos, { completed: false }); return cb(null, req); }, - update: function(req, cb) { - _.each(req.body, function(v, k) { - user.fns.dotSet(k, v); - return true; - }); - return typeof cb === "function" ? cb(null, req) : void 0; - }, sortTask: function(req, cb) { var from, id, task, to, _ref; id = req.params.id; @@ -10973,7 +11050,7 @@ var process=require("__browserify_process");(function() { if (!task) { return cb({ code: 404, - message: "No task found." + message: "Task not found." }); } if (!((to != null) && (from != null))) { @@ -10994,7 +11071,19 @@ var process=require("__browserify_process");(function() { return typeof cb === "function" ? cb(null, req) : void 0; }, deleteTask: function(req, cb) { - return typeof cb === "function" ? cb(null, req) : void 0; + var i, task, _ref; + task = user.tasks[(_ref = req.params) != null ? _ref.id : void 0]; + if (!task) { + return cb({ + code: 404, + message: 'Task not found' + }); + } + i = user[task.type + "s"].indexOf(task); + if (~i) { + user[task.type + "s"].splice(i, 1); + } + return cb(null, req); }, addTask: function(req, cb) { var task; @@ -11049,19 +11138,35 @@ var process=require("__browserify_process");(function() { return cb(null, req); }, feed: function(req, cb) { - var egg, evolve, food, message, pet, potion, userPets, _ref, _ref1, _ref2, _ref3; - _ref2 = [(_ref = req.query) != null ? _ref.pet : void 0, content.food[(_ref1 = req.query) != null ? _ref1.food : void 0]], pet = _ref2[0], food = _ref2[1]; - _ref3 = pet.split('-'), egg = _ref3[0], potion = _ref3[1]; - if (!(pet && food)) { - return cb("?food=__&pet=__ are both required in query, and must be valid"); + var egg, evolve, food, message, pet, potion, userPets, _ref, _ref1, _ref2; + _ref = req.params, pet = _ref.pet, food = _ref.food; + food = content.food[food]; + _ref1 = pet.split('-'), egg = _ref1[0], potion = _ref1[1]; + userPets = user.items.pets; + if (!userPets[pet]) { + return cb({ + code: 404, + message: ":pet not found in user.items.pets" + }); + } + if (!((_ref2 = user.items.food) != null ? _ref2[food.name] : void 0)) { + return cb({ + code: 404, + message: ":food not found in user.items.food" + }); } if (content.specialPets[pet]) { - return cb("Can't feed this pet."); + return cb({ + code: 401, + message: "Can't feed this pet." + }); } if (user.items.mounts[pet] && (userPets[pet] >= 50 || food.name === 'Saddle')) { - return cb("You already have that mount"); + return cb({ + code: 401, + message: "You already have that mount" + }); } - userPets = user.items.pets; message = ''; evolve = function() { userPets[pet] = 0; @@ -11091,9 +11196,32 @@ var process=require("__browserify_process");(function() { message: message }, req); }, + purchase: function(req, cb) { + var item, key, type, _ref; + _ref = req.params, type = _ref.type, key = _ref.key; + if (type !== 'eggs' && type !== 'hatchingPotions' && type !== 'food') { + return cb({ + code: 404, + message: ":type must be in [hatchingPotions,eggs,food]" + }, req); + } + item = content[type][key]; + if (!item) { + return cb({ + code: 404, + message: ":key not found for Content." + type + }, req); + } + if (!user.items[type][key]) { + user.items[type][key] = 0; + } + user.items[type][key]++; + user.balance -= item.value / 4; + return cb(null, req); + }, buy: function(req, cb) { var item, key, message, _ref; - key = req.query.key; + key = req.params.key; item = key === 'potion' ? content.potion : content.gear.flat[key]; if (!item) { return typeof cb === "function" ? cb({ @@ -11103,7 +11231,7 @@ var process=require("__browserify_process");(function() { } if (user.stats.gp < item.value) { return typeof cb === "function" ? cb({ - code: 200, + code: 401, message: 'Not enough gold.' }) : void 0; } @@ -11125,12 +11253,18 @@ var process=require("__browserify_process");(function() { }, sell: function(req, cb) { var key, type, _ref; - _ref = req.query, key = _ref.key, type = _ref.type; + _ref = req.params, key = _ref.key, type = _ref.type; if (type !== 'eggs' && type !== 'hatchingPotions' && type !== 'food') { - return cb("?type must by in [eggs, hatchingPotions, food]"); + return cb({ + code: 404, + message: ":type not found. Must bes in [eggs, hatchingPotions, food]" + }); } if (!user.items[type][key]) { - return cb("?key not found for user.items." + type); + return cb({ + code: 404, + message: ":key not found for user.items." + type + }); } user.items[type][key]--; user.stats.gp += content[type][key].value; @@ -11138,7 +11272,7 @@ var process=require("__browserify_process");(function() { }, equip: function(req, cb) { var item, key, message, type, _ref; - _ref = [req.query.type || 'equipped', req.query.key], type = _ref[0], key = _ref[1]; + _ref = [req.params.type || 'equipped', req.params.key], type = _ref[0], key = _ref[1]; switch (type) { case 'mount': user.items.currentMount = user.items.currentMount === key ? '' : key; @@ -11154,56 +11288,20 @@ var process=require("__browserify_process");(function() { } return typeof cb === "function" ? cb(message, req) : void 0; }, - revive: function(req, cb) { - var item, lostItem, lostStat; - _.merge(user.stats, { - hp: 50, - exp: 0, - gp: 0 - }); - if (user.stats.lvl > 1) { - user.stats.lvl--; - } - lostStat = user.fns.randomVal(_.reduce(['str', 'con', 'per', 'int'], (function(m, k) { - if (user.stats[k]) { - m[k] = k; - } - return m; - }), {})); - if (lostStat) { - user.stats[lostStat]--; - } - lostItem = user.fns.randomVal(_.reduce(user.items.gear.owned, (function(m, v, k) { - if (v) { - m["" + k] = "" + k; - } - return m; - }), {})); - if (item = content.gear.flat[lostItem]) { - user.items.gear.owned[lostItem] = false; - if (user.items.gear.equipped[item.type] === lostItem) { - user.items.gear.equipped[item.type] = "" + item.type + "_base_0"; - } - if (user.items.gear.costume[item.type] === lostItem) { - user.items.gear.costume[item.type] = "" + item.type + "_base_0"; - } - } - if (typeof user.markModified === "function") { - user.markModified('items.gear'); - } - return typeof cb === "function" ? cb((item ? { - code: 200, - message: "Your " + item.text + " broke." - } : null), req) : void 0; - }, hatch: function(req, cb) { var egg, hatchingPotion, pet, _ref; - _ref = req.query, egg = _ref.egg, hatchingPotion = _ref.hatchingPotion; + _ref = req.params, egg = _ref.egg, hatchingPotion = _ref.hatchingPotion; if (!(egg && hatchingPotion)) { - return cb("Please specify query.egg & query.hatchingPotion"); + return cb({ + code: 404, + message: "Please specify query.egg & query.hatchingPotion" + }); } if (!(user.items.eggs[egg] > 0 && user.items.hatchingPotions[hatchingPotion] > 0)) { - return cb("You're missing either that egg or that potion"); + return cb({ + code: 401, + message: "You're missing either that egg or that potion" + }); } pet = "" + egg + "-" + hatchingPotion; if (user.items.pets[pet]) { diff --git a/script/index.coffee b/script/index.coffee index 68957af3da..cd95194b96 100644 --- a/script/index.coffee +++ b/script/index.coffee @@ -276,30 +276,25 @@ User is now wrapped (both on client and server), adding a few new properties: * user.ops, which is super important: If a function is inside user.ops, it has magical properties. If you call it on the client it updates the user object in -the browser and when it's done it POSTs to the server, calling `src/controllers/user.js#OP_NAME` (the exact same name -of the op function). The first argument `req` is `{query, body, params}`, it's what the express controller function -expects. Because of this, we don't construct op functions in standard way (function(arg1,arg2,arg3){}), but instead -the first arg is {query, body, params} and the second is a callback(err, results). `query` is the most commonly-used, -but body is useful for updates/creates (eg, tasks), and params are used in those cases too (eg, {op:'updateTask',{params:{id:task.id}, body:task}}}) -This needs refinement: see http://stackoverflow.com/questions/4024271/rest-api-best-practices-where-to-put-parameters +the browser and when it's done it automatically POSTs to the server, calling src/controllers/user.js#OP_NAME (the exact same name +of the op function). The first argument req is {query, body, params}, it's what the express controller function +expects. This means we call our functions as if we were calling an Express route. Eg, instead of score(task, direction), +we call score({params:{id:task.id,direction:direction}}). This also forces us to think about our routes (whether to use +params, query, or body for variables). see http://stackoverflow.com/questions/4024271/rest-api-best-practices-where-to-put-parameters -If `src/controllers/user.js#OP_NAME` doesn't exist, it's automatically added. It runs the code in user.ops.OP_NAME +If `src/controllers/user.js#OP_NAME` doesn't exist on the server, it's automatically added. It runs the code in user.ops.OP_NAME to update the user model server-side, then performs `user.save()`. You can see this in action for `user.ops.buy`. That function doesn't exist on the server - so the client calls it, it updates user in the browser, auto-POSTs to server, server handles it by calling `user.ops.buy` again (to update user on the server), and then saves. We can do this for -everything that doesn't need any code different from what's in user.ops.OP_NAME for special-handling server-side. If we -*do* need special handling, just add `src/controllers/user.js#OP_NAME` to override the user.ops.OP_NAME, and just be +everything that doesn't need any code difference from what's in user.ops.OP_NAME for special-handling server-side. If we +*do* need special handling, just add `src/controllers/user.js#OP_NAME` to override the user.ops.OP_NAME, and be sure to call user.ops.OP_NAME at some point within the overridden function. TODO - * Migrate the remaining parts: Tags, party invitiations * Is this the best way to wrap the user object? I thought of using user.prototype, but user is an object not a Function. user on the server is a Mongoose model, so we can use prototype - but to do it on the client, we'd probably have to move to $resource for user * Move to $resource! - -BUGS - * Daily repeats don't have the days showing up (translations issue?) ### api.wrap = (user) -> return if user._wrapped @@ -311,24 +306,82 @@ api.wrap = (user) -> user.ops = - sleep: (req, cb) -> - user.preferences.sleep = !user.preferences.sleep - cb null, req - - clearCompleted: (req, cb) -> - user.todos = _.where(user.todos, {completed: false}) - cb null, req + # ------ + # User + # ------ update: (req, cb) -> _.each req.body, (v,k) -> user.fns.dotSet(k,v);true cb? null, req + sleep: (req, cb) -> + user.preferences.sleep = !user.preferences.sleep + cb null, req + + revive: (req, cb) -> + # Reset stats + _.merge user.stats, {hp:50, exp:0, gp:0} + user.stats.lvl-- if user.stats.lvl > 1 + + # Lose a stat point + lostStat = user.fns.randomVal _.reduce(['str','con','per','int'], ((m,k)->m[k]=k if user.stats[k];m), {}) + user.stats[lostStat]-- if lostStat + + # Lose a gear piece + # Note, they can actually lose item weapon_*_0 - it's 0 to buy back, no big deal + # Note the `""+` string-casting. Without this, when run on the server Mongoose returns funny objects + lostItem = user.fns.randomVal _.reduce(user.items.gear.owned, ((m,v,k)->m[""+k]=""+k if v;m), {}) + if item = content.gear.flat[lostItem] + user.items.gear.owned[lostItem] = false + user.items.gear.equipped[item.type] = "#{item.type}_base_0" if user.items.gear.equipped[item.type] is lostItem + user.items.gear.costume[item.type] = "#{item.type}_base_0" if user.items.gear.costume[item.type] is lostItem + user.markModified? 'items.gear' + cb? (if item then {code:200,message:"Your #{item.text} broke."} else null), req + + reset: (req, cb) -> + user.habits = [] + user.dailys = [] + user.todos = [] + user.rewards = [] + user.stats.hp = 50 + user.stats.lvl = 1 + user.stats.gp = 0 + user.stats.exp = 0 + # TODO handle MP + gear = user.items.gear + _.each ['equipped', 'costume'], (type) -> + gear[type].armor = 'armor_base_0' + gear[type].weapon = 'weapon_base_0' + gear[type].head = 'head_base_0' + gear[type].shield = 'shield_base_0' + user.items.gear.owned = {weapon_warrior_0:true} + user.markModified? 'items.gear.owned' + user.preferences.costume = false + cb null, req + + reroll: (req, cb) -> + if user.balance < 1 + return cb {code:401,message: "Not enough gems."}, req + user.balance-- + _.each user.tasks, (task) -> + task.value = 0 + user.stats.hp = 50 + cb null, req + + # ------ + # Tasks + # ------ + + clearCompleted: (req, cb) -> + user.todos = _.where(user.todos, {completed: false}) + cb null, req + sortTask: (req, cb) -> {id} = req.params {to, from} = req.query task = user.tasks[id] - return cb({code:404, message: "No task found."}) unless task + return cb({code:404, message: "Task not found."}) unless task return cb('?to=__&from=__ are required') unless to? and from? user["#{task.type}s"].splice to, 0, user["#{task.type}s"].splice(from, 1)[0] cb null, req @@ -340,7 +393,11 @@ api.wrap = (user) -> cb? null, req deleteTask: (req, cb) -> - cb? null, req + task = user.tasks[req.params?.id] + return cb({code:404,message:'Task not found'}) unless task + i = user[task.type + "s"].indexOf(task) + user[task.type + "s"].splice i, 1 if ~i + cb null, req addTask: (req, cb) -> task = api.taskDefaults(req.body) @@ -348,6 +405,10 @@ api.wrap = (user) -> cb? null, req task + # ------ + # Tags + # ------ + addTag: (req, cb) -> {name} = req.body user.tags ?= [] @@ -377,15 +438,21 @@ api.wrap = (user) -> user.markModified? type cb null, req + # ------ + # Inventory + # ------ + feed: (req, cb) -> - [pet, food] = [req.query?.pet, content.food[req.query?.food]] + {pet,food} = req.params + food = content.food[food] [egg, potion] = pet.split('-') - - return cb("?food=__&pet=__ are both required in query, and must be valid") unless pet and food # TODO validation on pet string - return cb("Can't feed this pet.") if content.specialPets[pet] - return cb("You already have that mount") if user.items.mounts[pet] and (userPets[pet] >= 50 or food.name is 'Saddle') - userPets = user.items.pets + + return cb({code:404, message:":pet not found in user.items.pets"}) unless userPets[pet] + return cb({code:404, message:":food not found in user.items.food"}) unless user.items.food?[food.name] + return cb({code:401, message:"Can't feed this pet."}) if content.specialPets[pet] + return cb({code:401, message:"You already have that mount"}) if user.items.mounts[pet] and (userPets[pet] >= 50 or food.name is 'Saddle') + message = '' evolve = -> userPets[pet] = 0 @@ -407,11 +474,23 @@ api.wrap = (user) -> user.items.food[food.name]-- cb {code:200, message}, req + # buy is for gear, purchase is for gem-purchaseables (i know, I know...) + purchase: (req, cb) -> + {type,key} = req.params + return cb({code:404,message:":type must be in [hatchingPotions,eggs,food]"},req) unless type in ['eggs','hatchingPotions', 'food'] + item = content[type][key] + return cb({code:404,message:":key not found for Content.#{type}"},req) unless item + user.items[type][key] = 0 unless user.items[type][key] + user.items[type][key]++ + user.balance -= (item.value / 4) + cb null, req + + # buy is for gear, purchase is for gem-purchaseables (i know, I know...) buy: (req, cb) -> - {key} = req.query + {key} = req.params item = if key is 'potion' then content.potion else content.gear.flat[key] return cb?({code:404, message:"Item '#{key} not found (see https://github.com/HabitRPG/habitrpg-shared/blob/develop/script/content.coffee)"}) unless item - return cb?({code:200, message:'Not enough gold.'}) if user.stats.gp < item.value + return cb?({code:401, message:'Not enough gold.'}) if user.stats.gp < item.value if item.key is 'potion' user.stats.hp += 15 user.stats.hp = 50 if user.stats.hp > 50 @@ -425,15 +504,15 @@ api.wrap = (user) -> cb? message, req sell: (req, cb) -> - {key, type} = req.query - return cb("?type must by in [eggs, hatchingPotions, food]") unless type in ['eggs','hatchingPotions', 'food'] - return cb("?key not found for user.items.#{type}") unless user.items[type][key] + {key, type} = req.params + return cb({code:404,message:":type not found. Must bes in [eggs, hatchingPotions, food]"}) unless type in ['eggs','hatchingPotions', 'food'] + return cb({code:404,message:":key not found for user.items.#{type}"}) unless user.items[type][key] user.items[type][key]-- user.stats.gp += content[type][key].value cb? null, req equip: (req, cb) -> - [type, key] = [req.query.type || 'equipped', req.query.key] + [type, key] = [req.params.type || 'equipped', req.params.key] switch type when 'mount' user.items.currentMount = if user.items.currentMount is key then '' else key @@ -445,31 +524,10 @@ api.wrap = (user) -> message = user.fns.handleTwoHanded(item,type) cb? message, req - revive: (req, cb) -> - # Reset stats - _.merge user.stats, {hp:50, exp:0, gp:0} - user.stats.lvl-- if user.stats.lvl > 1 - - # Lose a stat point - lostStat = user.fns.randomVal _.reduce(['str','con','per','int'], ((m,k)->m[k]=k if user.stats[k];m), {}) - user.stats[lostStat]-- if lostStat - - # Lose a gear piece - # Note, they can actually lose item weapon_*_0 - it's 0 to buy back, no big deal - # Note the `""+` string-casting. Without this, when run on the server Mongoose returns funny objects - lostItem = user.fns.randomVal _.reduce(user.items.gear.owned, ((m,v,k)->m[""+k]=""+k if v;m), {}) - if item = content.gear.flat[lostItem] - user.items.gear.owned[lostItem] = false - user.items.gear.equipped[item.type] = "#{item.type}_base_0" if user.items.gear.equipped[item.type] is lostItem - user.items.gear.costume[item.type] = "#{item.type}_base_0" if user.items.gear.costume[item.type] is lostItem - user.markModified? 'items.gear' - - cb? (if item then {code:200,message:"Your #{item.text} broke."} else null), req - hatch: (req, cb) -> - {egg, hatchingPotion} = req.query - return cb("Please specify query.egg & query.hatchingPotion") unless egg and hatchingPotion - return cb("You're missing either that egg or that potion") unless user.items.eggs[egg] > 0 and user.items.hatchingPotions[hatchingPotion] > 0 + {egg, hatchingPotion} = req.params + return cb({code:404,message:"Please specify query.egg & query.hatchingPotion"}) unless egg and hatchingPotion + return cb({code:401,message:"You're missing either that egg or that potion"}) unless user.items.eggs[egg] > 0 and user.items.hatchingPotions[hatchingPotion] > 0 pet = "#{egg}-#{hatchingPotion}" return cb("You already have that pet. Try hatching a different combination!") if user.items.pets[pet] user.items.pets[pet] = 5 @@ -495,6 +553,10 @@ api.wrap = (user) -> user.markModified? 'purchased' cb? null, req + # ------ + # Classes + # ------ + changeClass: (req, cb) -> klass = req.query?.class if klass in ['warrior','rogue','wizard','healer'] @@ -533,6 +595,10 @@ api.wrap = (user) -> user.stats.mp++ if stat is 'int' #increase their MP along with their max MP cb? null, req + # ------ + # Score + # ------ + score: (req, cb) -> {id, direction} = req.params # up or down task = user.tasks[id]