diff --git a/public/js/controllers/inventoryCtrl.js b/public/js/controllers/inventoryCtrl.js index c415d2a04f..3c7dae52c8 100644 --- a/public/js/controllers/inventoryCtrl.js +++ b/public/js/controllers/inventoryCtrl.js @@ -84,6 +84,8 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', } $scope.purchase = function(type, item){ + if (item.previous && !User.user.achievements.quests[item.previous]) + return alert("You must first complete " + $rootScope.Content.quests[item.previous].text + '.'); var gems = User.user.balance * 4; if(gems < item.value) return $rootScope.modals.buyGems = true; var string = (type == 'hatchingPotion') ? 'hatching potion' : type; // give hatchingPotion a space diff --git a/src/controllers/auth.js b/src/controllers/auth.js index 0d3a8304d5..462cf4ac59 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -28,12 +28,6 @@ api.auth = function(req, res, next) { if (err) return res.json(500, {err: err}); if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND); - // Remove this after a few days. Users aren't refreshing after the pets roll out, which is required - if (_.find(req.body, function(v){return v && v.data && _.isArray(v.data['items.pets'])})) { - // simply discard the update. Unfortunately, sending an error will keep their set ops in the sync queue. - return res.json(200, {_v: user._v-1}); - } - res.locals.wasModified = req.query._v ? +user._v !== +req.query._v : true; res.locals.user = user; req.session.userId = user._id; @@ -42,16 +36,14 @@ api.auth = function(req, res, next) { }; api.authWithSession = function(req, res, next) { //[todo] there is probably a more elegant way of doing this... - var uid; - uid = req.session.userId; - if (!(req.session && req.session.userId)) { + var uid = req.session.userId; + if (!(req.session && req.session.userId)) return res.json(401, NO_SESSION_FOUND); - } - return User.findOne({_id: uid,}, function(err, user) { + User.findOne({_id: uid}, function(err, user) { if (err) return res.json(500, {err: err}); if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND); res.locals.user = user; - return next(); + next(); }); }; diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 74d0885d69..7b52dbc7e1 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -409,8 +409,12 @@ questStart = function(req, res) { }) } - var parallel = [], questMembers = {}; - var key = group.quest.key; + var parallel = [], + questMembers = {}, + key = group.quest.key, + quest = shared.content.quests[key], + collectTally = quest.collect ? _.transform(quest.collect, function(m,v,k){m[k]=0}) : {}; + // TODO will this handle appropriately when people leave/join party between quest invite? _.each(group.members, function(m){ var updates = {$set:{},$inc:{'_v':1}}; @@ -418,10 +422,11 @@ questStart = function(req, res) { updates['$inc']['items.quests.'+key] = -1; if (group.quest.members[m] == true) { updates['$set']['party.quest.key'] = key; + updates['$set']['party.quest.tally'] = {up:0,down:0,collect:collectTally}; questMembers[m] = true; } else { - updates['$unset'] = {'party.quest.key':undefined}; - updates['$set']['party.quest.collection'] = {}; + updates['$unset'] = {'party.quest.key':1}; + updates['$set']['party.quest.tally'] = {}; } parallel.push(function(cb2){ User.update({_id:m},updates,cb2); @@ -429,8 +434,12 @@ questStart = function(req, res) { }) group.quest.active = true; - group.quest.progress.hp = shared.content.quests[group.quest.key].stats.hp; + if (quest.boss) + group.quest.progress.hp = quest.boss.hp; + else + group.quest.progress.collect = collectTally; group.quest.members = questMembers; + group.markModified('quest'); // members & progress.collect are both Mixed types parallel.push(function(cb2){group.save(cb2)}); async.parallel(parallel,function(err, results){ @@ -491,12 +500,14 @@ api.questAbort = function(req, res, next){ async.parallel([ function(cb){ User.update({_id:{$in: _.keys(group.quest.members)}},{ - $set:{'party.quest.key':undefined,'party.quest.tally.collection':{}}, - $inc:{_v:1} + $unset: {'party.quest.key':1}, + $set: {'party.quest.tally.collect':{}}, + $inc: {_v:1} },cb); }, function(cb) { group.quest = {}; + group.markModified('quest'); group.save(cb); } ], function(err){ diff --git a/src/controllers/user.js b/src/controllers/user.js index f6ec6e20a1..a92428cf2f 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -193,32 +193,38 @@ api.update = function(req, res, next) { }; api.cron = function(req, res, next) { - var user = res.locals.user; - tally = user.fns.cron(); - if (user.isModified()) res.locals.wasModified = true; + var user = res.locals.user, + tally = user.fns.cron(), + ranCron = user.isModified(), + quest = shared.content.quests[user.party.quest.key]; - // If user is on a quest, roll for boss & player - if (user.party.quest.key && tally && (tally.up || tally.down)) { - async.waterfall([ - function(cb){user.save(cb)}, // make sure to save the cron effects - function(saved, count, cb) { - Group.findOne({type: 'party', members: {'$in': [user._id]}}, cb); - }, - function(group, cb){ - group.bossAttack(user,tally,cb); - }, - function(updated,cb){ - // User has been updated in boss-grapple, reload - User.findById(user._id,cb); - } - ], function(err, saved) { - user = res.locals.user = saved; - next(err,saved); - }); + if (ranCron) res.locals.wasModified = true; + if (!ranCron) return next(null,user); + if (!quest) return user.save(next); + + // If user is on a quest, roll for boss & player, or handle collections + // FIXME this saves user, runs db updates, loads user. Is there a better way to handle this? + async.waterfall([ + function(cb){ + user.save(cb); // make sure to save the cron effects + }, + function(saved, count, cb) { + Group.findOne({type: 'party', members: {'$in': [user._id]}}, cb); + }, + function(group, cb){ + var type = quest.boss ? 'boss' : 'collect'; + group[type+'Quest'](user,tally,cb); + }, + function(){ + var cb = arguments[arguments.length-1]; + // User has been updated in boss-grapple, reload + User.findById(user._id, cb); + } + ], function(err, saved) { + user = res.locals.user = saved; + next(err,saved); + }); - } else { - user.save(next); - } }; // api.reroll // Shared.ops diff --git a/src/models/group.js b/src/models/group.js index f70e3f327c..949afc5557 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -38,7 +38,7 @@ var GroupSchema = new Schema({ active: {type:Boolean, 'default':false}, progress:{ hp: Number, - collected: Schema.Types.Mixed, + collect: {type:Schema.Types.Mixed, 'default':{}} // {feather: 5, ingot: 3} }, //Shows boolean for each party-member who has accepted the quest. Eg {UUID: true, UUID: false}. Once all users click @@ -107,15 +107,84 @@ GroupSchema.methods.sendChat = function(message, user){ group.chat.splice(200); } -GroupSchema.methods.bossAttack = function(user, tally, cb) { +// Participants: Grant rewards & achievements, finish quest +GroupSchema.methods.finishQuest = function(quest, cb) { var group = this; - var questK = group.quest.key; - var quest = shared.content.quests[questK]; + + var questK = quest.key; var dropK = quest.drop.key; - var down = tally.down * quest.stats.str; // multiply by boss strength + var updates = {$inc:{},$set:{}}; + + updates['$inc']['achievements.quests.'+questK] = 1; + updates['$inc']['stats.gp'] = +quest.drop.gp; + updates['$inc']['stats.exp'] = +quest.drop.exp; + updates['$inc']['_v'] = 1; + updates['$unset'] = {'party.quest.key':undefined}; + updates['$set']['party.quest.collect'] = {}; + + switch (quest.drop.type) { + case 'gear': + // TODO This means they can lose their new gear on death, is that what we want? + updates['$set']['items.gear.owned.'+dropK] = true; + break; + case 'eggs': + case 'food': + case 'hatchingPotions': + updates['$inc']['items.'+quest.drop.type+'.'+dropK] = 1; + break; + case 'pets': + updates['$set']['items.pets.'+dropK] = 5; + break; + case 'mounts': + updates['$set']['items.mounts.'+dropK] = true; + break; + } + // FIXME this is TERRIBLE practice. Looks like there are circular dependencies in the models, such that `var User` at + // this point is undefined. So we get around that by loading from mongoose only once we get to this point + var members = _.keys(group.quest.members); + group.quest = {};group.markModified('quest'); + mongoose.models.User.update({_id:{$in:members}}, updates, {multi:true}, cb); +} + +GroupSchema.methods.collectQuest = function(user, tally, cb) { + var group = this, + quest = shared.content.quests[group.quest.key]; + + _.each(tally.collect,function(v,k){ + group.quest.progress.collect[k] += v; + }); + + var foundText = _.reduce(tally.collect, function(m,v,k){ + m.push(v + ' ' + quest.collect[k].text); + return m; + }, []); + foundText = foundText ? foundText.join(', ') : 'nothing'; + group.sendChat("`<" + user.profile.name + "> found "+foundText+".`"); + group.markModified('quest.progress.collect'); + + // Still needs completing + if (_.find(shared.content.quests[group.quest.key].collect, function(v,k){ + return group.quest.progress.collect[k] < v.count; + })) return group.save(cb); + + async.series([ + function(cb2){ + group.finishQuest(quest,cb2); + }, + function(cb2){ + group.sendChat('`All items found! Party has received their rewards.`'); + group.save(cb2); + } + ],cb); +} + +GroupSchema.methods.bossQuest = function(user, tally, cb) { + var group = this; + var quest = shared.content.quests[group.quest.key]; + var down = tally.down * quest.boss.str; // multiply by boss strength group.quest.progress.hp -= tally.up; - group.sendChat("`<" + user.profile.name + "> attacks <" + quest.name + "> for " + (tally.up.toFixed(1)) + " damage, <" + quest.name + "> attacks party for " + (down.toFixed(1)) + " damage.`"); + group.sendChat("`<" + user.profile.name + "> attacks <" + quest.boss.name + "> for " + (tally.up.toFixed(1)) + " damage, <" + quest.boss.name + "> attacks party for " + (down.toFixed(1)) + " damage.`"); //var hp = group.quest.progress.hp; // Everyone takes damage @@ -127,47 +196,11 @@ GroupSchema.methods.bossAttack = function(user, tally, cb) { // Boss slain, finish quest if (group.quest.progress.hp <= 0) { + group.sendChat('`' + quest.boss.name + ' has been slain! Party has received their rewards.`'); + // Participants: Grant rewards & achievements, finish quest series.push(function(cb2){ - async.parallel([ - // Participants: Grant rewards & achievements, finish quest - function(cb3){ - var updates = {$inc:{},$set:{}}; - updates['$inc']['achievements.quests.'+questK] = 1; - updates['$inc']['stats.gp'] = +quest.drop.gp; - updates['$inc']['stats.exp'] = +quest.drop.exp; - updates['$inc']['_v'] = 1; - updates['$unset'] = {'party.quest.key':undefined}; - updates['$set']['party.quest.collection'] = {}; - - switch (quest.drop.type) { - case 'gear': - // TODO This means they can lose their new gear on death, is that what we want? - updates['$set']['items.gear.owned.'+dropK] = true; - break; - case 'eggs': - case 'food': - case 'hatchingPotions': - updates['$inc']['items.'+quest.drop.type+'.'+dropK] = 1; - break; - case 'pets': - updates['$set']['items.pets.'+dropK] = 5; - break; - case 'mounts': - updates['$set']['items.mounts.'+dropK] = true; - break; - } - // FIXME this is TERRIBLE practice. Looks like there are circular dependencies in the models, such that `var User` at - // this point is undefined. So we get around that by loading from mongoose only once we get to this point - mongoose.models.User.update({_id:{$in: _.keys(group.quest.members)}},updates,{multi:true},cb3); - }, - // Group: finish quest - function(cb3){ - group.quest = {};group.markModified('quest'); - group.sendChat('`' + quest.name + ' has been slain! Party has received their rewards`'); - group.save(cb3); - } - ],cb2); - }) + group.finishQuest(quest,cb2); + }); } series.push(function(cb2){group.save(cb2)}); diff --git a/src/models/user.js b/src/models/user.js index 7ad4062508..e089a9215a 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -206,7 +206,7 @@ var UserSchema = new Schema({ tally: { up: {type: Number, 'default': 0}, down: {type: Number, 'default': 0}, - collection: {type: Schema.Types.Mixed, 'default': {}} // {feather:1, ingot:2} + collect: {type: Schema.Types.Mixed, 'default': {}} // {feather:1, ingot:2} } } }, diff --git a/views/options/inventory/inventory.jade b/views/options/inventory/inventory.jade index 14d86bec34..f509f0063b 100644 --- a/views/options/inventory/inventory.jade +++ b/views/options/inventory/inventory.jade @@ -113,7 +113,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html') li.customize-menu menu.pets-menu(label='Quests') div(ng-repeat='quest in Content.quests') - button.customize-option(popover='{{quest.notes}}', popover-title='{{quest.text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("quests", quest)', class='inventory_quest_scroll') + button.customize-option(popover='{{quest.notes}}', popover-title='{{quest.text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("quests", quest)', class='inventory_quest_scroll', ng-class='{locked: quest.previous && !user.achievements.quests[quest.previous]}') p | {{quest.value}} span.Pet_Currency_Gem1x.inline-gems diff --git a/views/options/social/group.jade b/views/options/social/group.jade index 07fe18feaf..d48d9e62a7 100644 --- a/views/options/social/group.jade +++ b/views/options/social/group.jade @@ -8,35 +8,45 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' // ------ Bosses ------- - .modal.inline-modal(ng-if='group.type==="party" && group.quest.key && group.quest.active==false') - .modal-header(bindonce='group') - h3 Quest: {{Content.quests[group.quest.key].text}} + .modal.inline-modal(bindonce='group', ng-if='group.type==="party" && group.quest.key') + .modal-header + h3(ng-if='group.quest.active==false') Quest Invite: {{Content.quests[group.quest.key].text}} + h3(ng-if='group.quest.active==true') {{Content.quests[group.quest.key].text}} .modal-body - table.table.table-striped - tr(ng-repeat='member in group.members') - td {{member.profile.name}} - td {{group.quest.members[member._id] == undefined ? 'Pending' : k ? 'Rejected' : 'Accepted'}} - button.btn.btn-warning(ng-click='party.$questAccept({"force":true})') Force Start + div(ng-if='group.quest.active==false') + table.table.table-striped + tr(ng-repeat='member in group.members') + td {{member.profile.name}} + td {{group.quest.members[member._id] == undefined ? 'Pending' : k ? 'Rejected' : 'Accepted'}} + button.btn.btn-warning(ng-click='party.$questAccept({"force":true})') Force Start - .modal.inline-modal(ng-if='group.type=="party" && group.quest.key && group.quest.active==true') - .modal-header(bindonce='group') - h3 {{Content.quests[group.quest.key].text}} - .modal-body - div(class="quest_{{group.quest.key}}") - //- - .progress(style="height:10px") - .bar(style='width: {{Shared.percent(group.quest.hp, Content.quests[group.quest.key].hp)}}%;') - span.meter-text - i.icon-heart - | {{group.quest.hp | number:0}} / {{Content.quests[group.quest.key].hp}} - .hero-stats - .meter.health(title='Boss Health') - .bar(style='width: {{Shared.percent(group.quest.progress.hp, Content.quests[group.quest.key].stats.hp)}}%;') - span.meter-text - i.icon-heart - | {{group.quest.progress.hp | number:0}} / {{Content.quests[group.quest.key].stats.hp}} - p {{Content.quests[group.quest.key].notes}} - button.btn.btn-mini.btn-danger(ng-click='questAbort()') Abort + div(ng-if='group.quest.active==true') + div(ng-if='Content.quests[group.quest.key].boss') + div(class="quest_{{group.quest.key}}") + //- + .progress(style="height:10px") + .bar(style='width: {{Shared.percent(group.quest.hp, Content.quests[group.quest.key].hp)}}%;') + span.meter-text + i.icon-heart + | {{group.quest.hp | number:0}} / {{Content.quests[group.quest.key].hp}} + .hero-stats + .meter.health(title='Boss Health') + .bar(style='width: {{Shared.percent(group.quest.progress.hp, Content.quests[group.quest.key].boss.hp)}}%;') + span.meter-text + i.icon-heart + | {{group.quest.progress.hp | number:0}} / {{Content.quests[group.quest.key].boss.hp}} + + div(ng-if='Content.quests[group.quest.key].collect') + h4 Collected: + table.table.table-striped + tr(ng-repeat='(k,v) in group.quest.progress.collect') + td + div(class='{{group.quest.key}}_k') {{Content.quests[group.quest.key].collect[k].text}} + td + {{v}} / {{Content.quests[group.quest.key].collect[k].count}} + + p {{Content.quests[group.quest.key].notes}} + button.btn.btn-mini.btn-danger(ng-click='questAbort()') Abort // ------ Information ------- .modal.inline-modal diff --git a/views/shared/profiles/achievements.jade b/views/shared/profiles/achievements.jade index fefd37bf82..a830bccbbc 100644 --- a/views/shared/profiles/achievements.jade +++ b/views/shared/profiles/achievements.jade @@ -81,7 +81,18 @@ .achievement.achievement-karaoke(ng-show='profile.achievements.challenges') div(ng-class='{muted: !profile.achievements.challenges}') h5 Was the winner in the following challenges - ul - li(ng-repeat='chal in profile.achievements.challenges') {{chal}} + table.table.table-striped + tr(ng-repeat='chal in profile.achievements.challenges') + td {{chal}} + hr + + div(ng-if='profile.achievements.quests || user._id == profile._id') + .achievement.achievement-alien(ng-show='profile.achievements.quests') + div(ng-class='{muted: !profile.achievements.quests}') + h5 Completed the following quests + table.table.table-striped + tr(ng-repeat='(k,v) in profile.achievements.quests') + td {{Content.quests[k].text}} + td x{{v}} hr