From ecb354e2cc53bdf0bc827d67c81c72839de9e818 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 20 Dec 2013 12:42:36 -0700 Subject: [PATCH 01/10] Quests: initial Bosses WIP - invite, accept, reject, etc --- public/css/index.styl | 11 +- public/js/controllers/groupsCtrl.js | 6 +- public/js/controllers/inventoryCtrl.js | 18 ++- public/js/services/groupServices.js | 3 +- src/controllers/groups.js | 106 +++++++++++++++++- src/models/group.js | 14 ++- src/models/user.js | 10 +- src/routes/apiv2.js | 2 + views/options/inventory/inventory.jade | 17 ++- views/options/social/group.jade | 35 +++++- views/shared/modals/{pets.jade => drops.jade} | 0 views/shared/modals/index.jade | 5 +- views/shared/modals/quests.jade | 28 +++++ 13 files changed, 238 insertions(+), 17 deletions(-) rename views/shared/modals/{pets.jade => drops.jade} (100%) create mode 100644 views/shared/modals/quests.jade diff --git a/public/css/index.styl b/public/css/index.styl index 629567a2cd..796dc2e278 100644 --- a/public/css/index.styl +++ b/public/css/index.styl @@ -179,4 +179,13 @@ a .modal-indented-list margin-left: 10px; - padding-left: 10px; \ No newline at end of file + padding-left: 10px; + +.inline-modal + position: relative + top: auto; + left: auto + right: auto + margin: 0 auto 20px + z-index: 1 + max-width: 100% \ No newline at end of file diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index f31c385808..2b7c2c757e 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -267,11 +267,11 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'A } ]) - .controller("PartyCtrl", ['$scope', 'Groups', 'User', '$state', - function($scope, Groups, User, $state) { + .controller("PartyCtrl", ['$rootScope','$scope', 'Groups', 'User', '$state', + function($rootScope,$scope, Groups, User, $state) { $scope.type = 'party'; $scope.text = 'Party'; - $scope.group = Groups.party(); + $scope.group = $rootScope.party = Groups.party(); $scope.newGroup = new Groups.Group({type:'party', leader: User.user._id, members: [User.user._id]}); $scope.create = function(group){ group.$save(function(newGroup){ diff --git a/public/js/controllers/inventoryCtrl.js b/public/js/controllers/inventoryCtrl.js index 995f2fad83..8d6921fc90 100644 --- a/public/js/controllers/inventoryCtrl.js +++ b/public/js/controllers/inventoryCtrl.js @@ -1,5 +1,5 @@ -habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', 'API_URL', '$http', 'Notification', - function($rootScope, $scope, User, API_URL, $http, Notification) { +habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', + function($rootScope, $scope, User) { var user = User.user; var Content = $rootScope.Content; @@ -17,6 +17,7 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', 'API_URL', $scope.$watch('user.items.eggs', function(eggs){ $scope.eggCount = countStacks(eggs); }, true); $scope.$watch('user.items.hatchingPotions', function(pots){ $scope.potCount = countStacks(pots); }, true); $scope.$watch('user.items.food', function(food){ $scope.foodCount = countStacks(food); }, true); + $scope.$watch('user.items.quests', function(quest){ $scope.questCount = countStacks(quest); }, true); $scope.$watch('user.items.gear', function(gear){ $scope.gear = { @@ -114,5 +115,18 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', 'API_URL', $scope.chooseMount = function(egg, potion) { User.user.ops.equip({params:{type: 'mount', key: egg + '-' + potion}}); } + + $scope.showQuest = function(quest) { + $rootScope.selectedQuest = Content.quests[quest]; + $rootScope.modals.showQuest = true; + } + $scope.closeQuest = function(){ + $rootScope.selectedQuest = undefined; + $rootScope.modals.showQuest = false; + } + $scope.questInit = function(){ + $rootScope.party.$questAccept({key:$scope.selectedQuest.name}); + $scope.closeQuest(); + } } ]); \ No newline at end of file diff --git a/public/js/services/groupServices.js b/public/js/services/groupServices.js index 335e0b9d74..4dc7f7d4e2 100644 --- a/public/js/services/groupServices.js +++ b/public/js/services/groupServices.js @@ -16,7 +16,8 @@ angular.module('groupServices', ['ngResource']). join: {method: "POST", url: API_URL + '/api/v2/groups/:gid/join'}, leave: {method: "POST", url: API_URL + '/api/v2/groups/:gid/leave'}, invite: {method: "POST", url: API_URL + '/api/v2/groups/:gid/invite'}, - removeMember: {method: "POST", url: API_URL + '/api/v2/groups/:gid/removeMember'} + questAccept: {method: "POST", url: API_URL + '/api/v2/groups/:gid/questAccept'}, + questReject: {method: "POST", url: API_URL + '/api/v2/groups/:gid/questReject'} }); // Defer loading everything until they're requested diff --git a/src/controllers/groups.js b/src/controllers/groups.js index e62596a455..b67ca62634 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -200,6 +200,15 @@ api.attachGroup = function(req, res, next) { }) } +api.attachGroupPopulated = function(req, res, next) { + Group.findById(req.params.gid).populate('members').exec(function(err, group){ + if(err) return res.json(500, {err:err}); + if(!group) return res.json(404, {err: "Group not found"}); + res.locals.group = group; + next(); + }) +} + /** * TODO make this it's own ngResource so we don't have to send down group data with each chat post */ @@ -398,5 +407,100 @@ api.removeMember = function(req, res, next){ }else{ return res.json(400, {err: "User not found among group's members!"}); } - } + +// ------------------------------------ +// Quests +// ------------------------------------ + +questStart = function(req, res) { + var group = res.locals.group; + var user = res.locals.user; + var force = req.query.force; + + group.markModified('quest'); + + // Not ready yet, wait till everyone's accepted, rejected, or we force-start + if (!force && _.findIndex(group.quest.members, function(m){ + return m === undefined; + })) { + return group.save(function(err,saved){ + if (err) return res.json(500,{err:err}); + res.json(saved); + }) + } + + var parallel = []; + // TODO will this handle appropriately when people leave/join party between quest invite? + _.each(group.members, function(m){ + if (m._id == user._id) m.items.quests[m.party.quest]--; + if (group.quest.members[m._id] == true) { + m.party.quest = group.quest.key; + } else { + m.party.quest = undefined; + delete group.quest.members[m._id]; + } + parallel.push(function(cb2){m.save(cb2);}); + }) + + group.quest.active = true; + group.quest.hp = shared.content.quests[group.quest.key].hp; + parallel.push(function(cb2){group.save(cb2);}); + + async.parallel(parallel,function(err, results){ + if (err) return res.json(500,{err:err}); + return res.json(group); + }); +} + +api.questAccept = function(req, res) { + var group = res.locals.group; + var user = res.locals.user; + var key = req.query.key; + + if (!group) return res.json(400, {err: "Must be in a party to start quests (this will change in the future)."}); + + // If ?key=xxx is provided, we're starting a new quest and inviting the party. Otherwise, we're a party member accepting the invitation + if (key) { + if (!shared.content.quests[key]) return res.json(404,{err:'Quest ' + key + ' not found'}); + if (group.quest.key) return res.json(400, {err: 'Party already on a quest (and only have one quest at a time)'}); + group.quest.key = key; + group.quest.members = {}; + // Invite everyone. true means "accepted", false="rejected", undefined="pending". Once we click "start quest" + // or everyone has either accepted/rejected, then we store quest key in user object. + _.each(group.members, function(m){ + if (m._id == user._id) { + group.quest.members[m._id] = true; + } else { + group.quest.members[m._id] = undefined; + } + }); + + // Party member accepting the invitation + } else { + if (!group.quest.key) return res.json(400,{err:'No quest invitation has been sent out yet.'}); + group.quest.members[user._id] = true; + } + + questStart(req,res); +} + +api.questReject = function(req, res, next) { + var group = res.locals.group; + var user = res.locals.user; + + if (!group.quest.key) return res.json(400,{err:'No quest invitation has been sent out yet.'}); + group.quest.members[user._id] = false; + + group.save(function(err,saved){ + if (err) return res.json(500,{err:err}); + return res.json(200,saved); + }); + + questStart(req,res); +} + + +//TODO +function questEnd(){} +function questAbort(){} diff --git a/src/models/group.js b/src/models/group.js index b8a7a428b3..e13c5ea961 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -30,7 +30,19 @@ var GroupSchema = new Schema({ balance: Number, logo: String, leaderMessage: String, - challenges: [{type:'String', ref:'Challenge'}] // do we need this? could depend on back-ref instead (Challenge.find({group:GID})) + challenges: [{type:'String', ref:'Challenge'}], // do we need this? could depend on back-ref instead (Challenge.find({group:GID})) + quest: { + key: String, + hp: Number, + active: {type:Boolean, 'default':false}, + + /* + Shows boolean for each party-member who has accepted the quest. Eg {UUID: true, UUID: false}. Once all users click + 'Accept', the quest begins. If a false user waits too long, probably a good sign to prod them or boot them. + TODO when booting user, remove from .joined and check again if we can now start the quest + */ + members: Schema.Types.Mixed + } }, { strict: 'throw', minimize: false // So empty objects are returned diff --git a/src/models/user.js b/src/models/user.js index 6fdd789e55..3b0ad8b371 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -182,6 +182,12 @@ var UserSchema = new Schema({ ), currentMount: String, + // Quests: { + // 'boss_0': 0, // 0 indicates "doesn't own" + // 'collection_honey': 5 // Number indicates "stacking" + // } + quests: _.transform(shared.content.quests, function(m,v,k){ m[k] = Number; }), + lastDrop: { date: {type: Date, 'default': Date.now}, count: {type: Number, 'default': 0} @@ -193,14 +199,14 @@ var UserSchema = new Schema({ 'default': Date.now }, - // FIXME remove? party: { //party._id // FIXME make these populate docs? current: String, // party._id invitation: String, // party._id lastMessageSeen: String, leader: Boolean, - order: {type:String, 'default':'level'} + order: {type:String, 'default':'level'}, + quest: String }, preferences: { armorSet: String, diff --git a/src/routes/apiv2.js b/src/routes/apiv2.js index bfb4c8c5db..ff266fcbec 100644 --- a/src/routes/apiv2.js +++ b/src/routes/apiv2.js @@ -98,6 +98,8 @@ router.post('/groups/:gid/join', auth.auth, groups.attachGroup, groups.join); router.post('/groups/:gid/leave', auth.auth, groups.attachGroup, groups.leave); router.post('/groups/:gid/invite', auth.auth, groups.attachGroup, groups.invite); router.post('/groups/:gid/removeMember', auth.auth, groups.attachGroup, groups.removeMember); +router.post('/groups/:gid/questAccept', auth.auth, groups.attachGroupPopulated, groups.questAccept); // query={key} (optional. if provided, trigger new invite, if not, accept existing invite) +router.post('/groups/:gid/questReject', auth.auth, groups.attachGroupPopulated, groups.questReject); //GET /groups/:gid/chat router.post('/groups/:gid/chat', auth.auth, groups.attachGroup, groups.postChat); diff --git a/views/options/inventory/inventory.jade b/views/options/inventory/inventory.jade index 0e2e0143b2..fdb87014a6 100644 --- a/views/options/inventory/inventory.jade +++ b/views/options/inventory/inventory.jade @@ -41,7 +41,13 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html') div(ng-repeat='(pot,points) in ownedItems(user.items.hatchingPotions)') button.customize-option(popover='{{Content.hatchingPotions[pot].notes}}', popover-title='{{Content.hatchingPotions[pot].text}} Potion', popover-trigger='mouseenter', popover-placement='right', ng-click='choosePotion(pot)', class='Pet_HatchingPotion_{{pot}}', ng-class='{selectableInventory: selectedEgg && !user.items.pets[selectedEgg.name+"-"+pot]}') .badge.badge-info.stack-count {{points}} - //-p {{pot}} + + li.customize-menu + menu.pets-menu(label='Quest Scrolls ({{questCount}})') + p(ng-show='questCount < 1') You don't have any quest scrolls. + div(ng-repeat='(quest,points) in ownedItems(user.items.quests)') + button.customize-option(popover='{{Content.quests[quest].notes}}', popover-title='{{Content.quests[quest].text}}', popover-trigger='mouseenter', popover-placement='right', ng-click='showQuest(quest)', class='inventory_quest_scroll') + .badge.badge-info.stack-count {{points}} li.customize-menu menu.pets-menu(label='Food ({{foodCount}})') @@ -49,7 +55,6 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html') div(ng-repeat='(food,points) in ownedItems(user.items.food)') button.customize-option(popover='{{Content.food[food].notes}}', popover-title='{{Content.food[food].text}}', popover-trigger='mouseenter', popover-placement='right', ng-click='chooseFood(food)', class='Pet_Food_{{food}}') .badge.badge-info.stack-count {{points}} - //-p {{food}} li.customize-menu(ng-if='user.items.special.snowball') menu.pets-menu(label='Special') @@ -105,6 +110,14 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html') | {{food.value}} span.Pet_Currency_Gem1x.inline-gems + 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') + p + | {{quest.value}} + span.Pet_Currency_Gem1x.inline-gems + li.customize-menu menu.pets-menu(label='Special') div diff --git a/views/options/social/group.jade b/views/options/social/group.jade index 62f54a5a7e..64a924d957 100644 --- a/views/options/social/group.jade +++ b/views/options/social/group.jade @@ -6,8 +6,39 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' .row-fluid .span4 + + // ------ 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-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 + + .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.hp, Content.quests[group.quest.key].hp)}}%;') + span.meter-text + i.icon-heart + | {{group.quest.hp | number:0}} / {{Content.quests[group.quest.key].hp}} + p {{Content.quests[group.quest.key].notes}} + // ------ Information ------- - .modal(style='position: relative;top: auto;left: auto;right: auto;margin: 0 auto 20px;z-index: 1;max-width: 100%;') + .modal.inline-modal .modal-header(bindonce='group') span(ng-if='group.leader == user.id') button.btn.btn-primary.pull-right(ng-click='save(group)', ng-show='group._editing') Save @@ -35,7 +66,7 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' include ./challenge-box // ------ Members ------- - .modal(style='position: relative;top: auto;left: auto;right: auto;margin: 0 auto 20px;z-index: 1;max-width: 100%;') + .modal.inline-modal .modal-header h3 Members .modal-body diff --git a/views/shared/modals/pets.jade b/views/shared/modals/drops.jade similarity index 100% rename from views/shared/modals/pets.jade rename to views/shared/modals/drops.jade diff --git a/views/shared/modals/index.jade b/views/shared/modals/index.jade index 339424984e..bb7dd5ac8e 100644 --- a/views/shared/modals/index.jade +++ b/views/shared/modals/index.jade @@ -6,5 +6,6 @@ include ./new-stuff include ./buy-gems include ./members include ./settings -include ./pets -include ./classes \ No newline at end of file +include ./drops +include ./classes +include ./quests \ No newline at end of file diff --git a/views/shared/modals/quests.jade b/views/shared/modals/quests.jade new file mode 100644 index 0000000000..edd593abfe --- /dev/null +++ b/views/shared/modals/quests.jade @@ -0,0 +1,28 @@ +div(modal='modals.showQuest', ng-controller='InventoryCtrl') + .modal-header + h3 {{selectedQuest.text}} + .modal-body + table + tr + td + p {{selectedQuest.notes}} + td + div(class='quest_{{selectedQuest.name}}') + hr + div(style='clear:left;clear:right') + .npc_ian.pull-left + p Clicking "Invite" will send an invitation to your party members. When all members have accepted or denied, the quest begins. If they take to long, feel free to force-start the quest under Options > Social > Party + .modal-footer + button.btn.btn-default.btn-small.btn-cancel(ng-click='closeQuest()') Cancel + button.btn.btn-default.btn-primary(ng-click='questInit()') Invite Party + +div(modal='party.quest.key && !questHold && party.quest.members[user._id] == undefined') + .modal-header + h3 Quest Invitation + .modal-body + p You have been invited to {{Content.quests[party.quest.key].text}}! + p (TODO list rewards) + .modal-footer + button.btn.btn-default.btn-small.btn-cancel(ng-click='questHold = true') Ask Later + button.btn.btn-default.btn-small.btn-cancel(ng-click='party.$questReject()') Reject + button.btn.btn-default.btn-primary(ng-click='party.$questAccept()') Accept \ No newline at end of file From 8095397317f6cd6efe9c85aef4206f12fe208694 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 20 Dec 2013 15:43:10 -0700 Subject: [PATCH 02/10] Quests: hurt boss via score method, refactor chat function, change all content.*.name to content.*.key for consistency --- bower.json | 2 +- package.json | 2 +- public/js/controllers/inventoryCtrl.js | 30 +++---- public/js/controllers/notificationCtrl.js | 6 +- public/js/controllers/rootCtrl.js | 2 +- src/controllers/groups.js | 27 ++----- src/controllers/user.js | 58 ++++++++++---- src/models/group.js | 97 +++++++++++++++++++++-- src/models/user.js | 33 ++++---- views/options/inventory/inventory.jade | 20 ++--- views/options/inventory/stable.jade | 11 +-- views/options/social/chat-box.jade | 4 +- views/options/social/chat-message.jade | 4 +- views/options/social/group.jade | 4 +- views/shared/modals/drops.jade | 4 +- views/shared/modals/quests.jade | 29 ++++++- 16 files changed, 230 insertions(+), 103 deletions(-) diff --git a/bower.json b/bower.json index 7433383b0d..f20cab731c 100644 --- a/bower.json +++ b/bower.json @@ -29,7 +29,7 @@ "bootstrap": "v2.3.2", "bootstrap-datepicker": "~1.2.0", "bootstrap-growl": "~1.1.0", - "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared.git#develop", + "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared.git#bosses", "BrowserQuest": "https://github.com/mozilla/BrowserQuest.git", "github-buttons": "git://github.com/mdo/github-buttons.git", "marked": "~0.2.9", diff --git a/package.json b/package.json index 31ddab7388..81e9b6a1fe 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0-152", "main": "./src/server.js", "dependencies": { - "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#develop", + "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#bosses", "derby-auth": "git://github.com/lefnire/derby-auth#master", "connect-mongo": "*", "passport-facebook": "~1.0.0", diff --git a/public/js/controllers/inventoryCtrl.js b/public/js/controllers/inventoryCtrl.js index 8d6921fc90..c415d2a04f 100644 --- a/public/js/controllers/inventoryCtrl.js +++ b/public/js/controllers/inventoryCtrl.js @@ -32,10 +32,10 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', }, true); $scope.chooseEgg = function(egg){ - if ($scope.selectedEgg && $scope.selectedEgg.name == egg) { + if ($scope.selectedEgg && $scope.selectedEgg.key == egg) { return $scope.selectedEgg = null; // clicked same egg, unselect } - var eggData = _.findWhere(Content.eggs, {name:egg}); + var eggData = _.findWhere(Content.eggs, {key:egg}); if (!$scope.selectedPotion) { $scope.selectedEgg = eggData; } else { @@ -44,11 +44,11 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', } $scope.choosePotion = function(potion){ - if ($scope.selectedPotion && $scope.selectedPotion.name == potion) { + if ($scope.selectedPotion && $scope.selectedPotion.key == potion) { return $scope.selectedPotion = null; // clicked same egg, unselect } // we really didn't think through the way these things are stored and getting passed around... - var potionData = _.findWhere(Content.hatchingPotions, {name:potion}); + var potionData = _.findWhere(Content.hatchingPotions, {key:potion}); if (!$scope.selectedEgg) { $scope.selectedPotion = potionData; } else { @@ -57,7 +57,7 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', } $scope.chooseFood = function(food){ - if ($scope.selectedFood && $scope.selectedFood.name == food) return $scope.selectedFood = null; + if ($scope.selectedFood && $scope.selectedFood.key == food) return $scope.selectedFood = null; $scope.selectedFood = Content.food[food]; } @@ -65,8 +65,8 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', var selected = $scope.selectedEgg ? 'selectedEgg' : $scope.selectedPotion ? 'selectedPotion' : $scope.selectedFood ? 'selectedFood' : undefined; if (selected) { var type = $scope.selectedEgg ? 'eggs' : $scope.selectedPotion ? 'hatchingPotions' : $scope.selectedFood ? 'food' : undefined; - user.ops.sell({params:{type:type, key: $scope[selected].name}}); - if (user.items[type][$scope[selected].name] < 1) { + user.ops.sell({params:{type:type, key: $scope[selected].key}}); + if (user.items[type][$scope[selected].key] < 1) { $scope[selected] = null; } } @@ -77,8 +77,8 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', } $scope.hatch = function(egg, potion){ - if (!confirm('Hatch a ' + potion.name + ' ' + egg.name + '?')) return; - user.ops.hatch({params:{egg:egg.name, hatchingPotion:potion.name}}); + if (!confirm('Hatch a ' + potion.key + ' ' + egg.key + '?')) return; + user.ops.hatch({params:{egg:egg.key, hatchingPotion:potion.key}}); $scope.selectedEgg = null; $scope.selectedPotion = null; } @@ -89,7 +89,7 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', var string = (type == 'hatchingPotion') ? 'hatching potion' : type; // give hatchingPotion a space var message = "Buy this " + string + " with " + item.value + " of your " + gems + " Gems?" if(confirm(message)) - User.user.ops.purchase({params:{type:type,key:item.name}}); + User.user.ops.purchase({params:{type:type,key:item.key}}); } $scope.choosePet = function(egg, potion){ @@ -98,12 +98,12 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', // Feeding Pet if ($scope.selectedFood) { var food = $scope.selectedFood - if (food.name == 'Saddle') { + if (food.key == 'Saddle') { if (!confirm('Saddle ' + pet + '?')) return; - } else if (!confirm('Feed ' + pet + ' a ' + food.name + '?')) { + } else if (!confirm('Feed ' + pet + ' a ' + food.key + '?')) { return; } - User.user.ops.feed({params:{pet: pet, food: food.name}}); + User.user.ops.feed({params:{pet: pet, food: food.key}}); $scope.selectedFood = null; // Selecting Pet @@ -125,7 +125,9 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', $rootScope.modals.showQuest = false; } $scope.questInit = function(){ - $rootScope.party.$questAccept({key:$scope.selectedQuest.name}); + $rootScope.party.$questAccept({key:$scope.selectedQuest.key}, function(){ + $rootScope.party.$get(); + }); $scope.closeQuest(); } } diff --git a/public/js/controllers/notificationCtrl.js b/public/js/controllers/notificationCtrl.js index 03d418ffd0..6c3d7ede2a 100644 --- a/public/js/controllers/notificationCtrl.js +++ b/public/js/controllers/notificationCtrl.js @@ -37,10 +37,10 @@ habitrpg.controller('NotificationCtrl', var type = (after.type == 'Food') ? 'food' : (after.type == 'HatchingPotion') ? 'hatchingPotions' : // can we use camelcase and remove this line? (after.type.toLowerCase() + 's'); - if(!User.user.items[type][after.name]){ - User.user.items[type][after.name] = 0; + if(!User.user.items[type][after.key]){ + User.user.items[type][after.key] = 0; } - User.user.items[type][after.name]++; + User.user.items[type][after.key]++; $rootScope.modals.drop = true; }); diff --git a/public/js/controllers/rootCtrl.js b/public/js/controllers/rootCtrl.js index 6635f828c9..dd652eee74 100644 --- a/public/js/controllers/rootCtrl.js +++ b/public/js/controllers/rootCtrl.js @@ -167,7 +167,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ if ($scope.spell.target != type) return Notification.text("Invalid target"); $scope.spell.cast(User.user, target); User.save(); - $http.post('/api/v2/user/class/cast/' + $scope.spell.name, {target:target, type:type}).success(function(){ + $http.post('/api/v2/user/class/cast/' + $scope.spell.key, {target:target, type:type}).success(function(){ var msg = "You cast " + $scope.spell.text; switch (type) { case 'task': msg += ' on ' + target.text;break; diff --git a/src/controllers/groups.js b/src/controllers/groups.js index b67ca62634..70ce37279c 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -215,30 +215,18 @@ api.attachGroupPopulated = function(req, res, next) { api.postChat = function(req, res, next) { var user = res.locals.user var group = res.locals.group; - var message = { - id: shared.uuid(), - uuid: user._id, - contributor: user.contributor && user.contributor.toObject(), - backer: user.backer && user.backer.toObject(), - text: req.query.message, // FIXME this should be body, but ngResource is funky - user: user.profile.name, - timestamp: +(new Date) - }; - var lastClientMsg = req.query.previousMsg; var chatUpdated = (lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg) ? true : false; - group.chat.unshift(message); - group.chat.splice(200); + group.sendChat(req.query.message, user); // FIXME this should be body, but ngResource is funky if (group.type === 'party') { - user.party.lastMessageSeen = message.id; + user.party.lastMessageSeen = group.chat[0].id; user.save(); } group.save(function(err, saved){ if (err) return res.json(500, {err:err}); - return chatUpdated ? res.json({chat: group.chat}) : res.json({message: saved.chat[0]}); }); } @@ -431,20 +419,22 @@ questStart = function(req, res) { } var parallel = []; + var key = group.quest.key; // TODO will this handle appropriately when people leave/join party between quest invite? _.each(group.members, function(m){ - if (m._id == user._id) m.items.quests[m.party.quest]--; + if (m._id == user._id) m.items.quests[key]--; if (group.quest.members[m._id] == true) { - m.party.quest = group.quest.key; + m.party.quest.key = key; } else { - m.party.quest = undefined; + _.merge(m.party.quest, {key:undefined,collection:{}}); delete group.quest.members[m._id]; } + m._v++; parallel.push(function(cb2){m.save(cb2);}); }) group.quest.active = true; - group.quest.hp = shared.content.quests[group.quest.key].hp; + group.quest.progress.hp = shared.content.quests[group.quest.key].stats.hp; parallel.push(function(cb2){group.save(cb2);}); async.parallel(parallel,function(err, results){ @@ -502,5 +492,4 @@ api.questReject = function(req, res, next) { //TODO -function questEnd(){} function questAbort(){} diff --git a/src/controllers/user.js b/src/controllers/user.js index 705b87bd15..086d37fb63 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -82,11 +82,11 @@ api.score = function(req, res, next) { task = user.ops.addTask({body:task}); } var delta = user.ops.score({params:{id:task.id, direction:direction}}); - //user.markModified('flags'); - user.save(function(err, saved) { + + user.save(function(err,saved){ if (err) return res.json(500, {err: err}); res.json(200, _.extend({ - delta: delta + delta: delta, }, saved.toJSON().stats)); }); @@ -194,10 +194,43 @@ api.update = function(req, res, next) { api.cron = function(req, res, next) { var user = res.locals.user; - user.fns.cron(); - if (user.isModified()) - res.locals.wasModified = true; - user.save(next); + tally = user.fns.cron(); + if (user.isModified()) res.locals.wasModified = true; + + // If user is on a quest, roll for boss & player + if (user.party.quest.key && tally && (tally.up || tally.down)) { + async.waterfall([ + function(cb){ + Group.findOne({type: 'party', members: {'$in': [user._id]}}) + .populate({ + path: 'members', + match: {'party.quest.key':user.party.quest.key} + }) + .exec(cb); + }, + function(group, cb){ + var quest = shared.content.quests[group.quest.key]; + var down = tally.down * quest.stats.str; // multiply by boss strength + var parallel = []; + _.each(group.members, function(m){ + parallel.push(function(cb2){ + m.stats.hp += down; + m._v++; + m.save(cb2); + }) + }) + // Use http://js2coffee.org/ for building this string. Man I wish JS had interpolation... + group.sendChat("`<" + user.profile.name + "> attacks <" + quest.name + "> for " + (tally.up.toFixed(1)) + " damage, <" + quest.name + "> attacks party for " + (tally.down.toFixed(1)) + " damage.`"); + parallel.push(function(cb2){ + group.hurtBoss(tally.up,cb2); + }); + async.parallel(parallel,cb); + } + ], next); + + } else { + user.save(next); + } }; // api.reroll // Shared.ops @@ -347,15 +380,8 @@ api.cast = function(req, res) { if (group) { series.push(function(cb2){ - group.chat.unshift({ - id: shared.uuid(), - uuid: user._id, - contributor: user.contributor && user.contributor.toObject(), - backer: user.backer && user.backer.toObject(), - text: '`casts ' + spell.text + (type == 'user' ? ' on @'+found.profile.name : ' for the party') + '.`', - user: '<'+user.profile.name+'>', - timestamp: +new Date - }); + var message = '`<'+user.profile.name+'> casts '+spell.text + (type=='user' ? ' on @'+found.profile.name : ' for the party')+'.`'; + group.sendChat(message); group.save(cb2); }) } diff --git a/src/models/group.js b/src/models/group.js index e13c5ea961..e06939a3b3 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -2,6 +2,7 @@ var mongoose = require("mongoose"); var Schema = mongoose.Schema; var shared = require('habitrpg-shared'); var _ = require('lodash'); +var async = require('async'); var GroupSchema = new Schema({ _id: {type: String, 'default': shared.uuid}, @@ -33,14 +34,15 @@ var GroupSchema = new Schema({ challenges: [{type:'String', ref:'Challenge'}], // do we need this? could depend on back-ref instead (Challenge.find({group:GID})) quest: { key: String, - hp: Number, active: {type:Boolean, 'default':false}, + progress:{ + hp: Number, + collected: Schema.Types.Mixed, + }, - /* - Shows boolean for each party-member who has accepted the quest. Eg {UUID: true, UUID: false}. Once all users click - 'Accept', the quest begins. If a false user waits too long, probably a good sign to prod them or boot them. - TODO when booting user, remove from .joined and check again if we can now start the quest - */ + //Shows boolean for each party-member who has accepted the quest. Eg {UUID: true, UUID: false}. Once all users click + //'Accept', the quest begins. If a false user waits too long, probably a good sign to prod them or boot them. + //TODO when booting user, remove from .joined and check again if we can now start the quest members: Schema.Types.Mixed } }, { @@ -83,5 +85,88 @@ GroupSchema.methods.toJSON = function(){ return doc; } +GroupSchema.methods.sendChat = function(message, user){ + var group = this; + var message = { + id: shared.uuid(), + text: message, + timestamp: +(new Date) + }; + if (user) { + _.defaults(message, { + uuid: user._id, + contributor: user.contributor && user.contributor.toObject(), + backer: user.backer && user.backer.toObject(), + user: user.profile.name, + }); + } else { + message.uuid = 'system'; + } + group.chat.unshift(message); + group.chat.splice(200); +} + +GroupSchema.methods.hurtBoss = function(delta, cb) { + var group = this; + group.quest.progress.hp -= delta; + var hp = group.quest.progress.hp; + if (group.quest.progress.hp <= 0) { + var key = group.quest.key, + quest = shared.content.quests[key]; + + var parallel = _.reduce(group.members, function(m,v,k){ + + // Achievement + _.defaults(v.achievements, {quests:{}}) + if (!v.achievements.quests[key]) v.achievements.quests[key] = 0; + v.achievements.quests[key]++; + v.markModified('achievements'); + + // Drops + v.stats.gp += +quest.drop.gp; + v.stats.exp += +quest.drop.exp; + switch (quest.drop.type) { + case 'gear': + // TODO This means they can lose their new gear on death, is that what we want? + v.items.gear.owned[quest.drop.key] = true; + break; + case 'eggs': + case 'food': + case 'hatchingPotions': + if (!v.items.hatchingPotions[quest.drop.key]) v.items.hatchingPotions[quest.drop.key] = 0; + v.items.hatchingPotions[quest.drop.key]++; + break; + case 'pets': + if (!v.items.pets[quest.drop.key]) v.items.pets[quest.drop.key] = 5; + break; + case 'mounts': + v.items.mounts[quest.drop.key] = true; + break; + } + + v.party.quest.key = undefined; + v.party.quest.collection = {}; + v.markModified('party.quest'); + + v._v++; + m.push(function(cb3){ v.save(cb3); }) + return m; + }, []); + + // Finish the quest + group.quest = {};group.markModified('quest'); + group.sendChat('`' + quest.name + ' has been slain! Party has received their rewards`'); + + parallel.push(function(cb3){ + group.save(cb3); + }) + async.parallel(parallel,cb); + } + else { + group.save(cb); + } + return hp; +} + module.exports.schema = GroupSchema; module.exports.model = mongoose.model("Group", GroupSchema); diff --git a/src/models/user.js b/src/models/user.js index 3b0ad8b371..d0249cb433 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -16,7 +16,7 @@ var Challenge = require('./challenge').model; var eggPotionMapping = _.transform(shared.content.eggs, function(m, egg){ _.defaults(m, _.transform(shared.content.hatchingPotions, function(m2, pot){ - m2[egg.name + '-' + pot.name] = true; + m2[egg.key + '-' + pot.key] = true; })); }) @@ -45,7 +45,8 @@ var UserSchema = new Schema({ veteran: Boolean, snowball: Number, streak: Number, - challenges: Array + challenges: Array, + quests: Schema.Types.Mixed }, auth: { facebook: Schema.Types.Mixed, @@ -200,13 +201,17 @@ var UserSchema = new Schema({ }, party: { - //party._id // FIXME make these populate docs? - current: String, // party._id - invitation: String, // party._id + // id // FIXME can we use a populated doc instead of fetching party separate from user? lastMessageSeen: String, - leader: Boolean, order: {type:String, 'default':'level'}, - quest: String + quest: { + key: String, + tally: { + up: {type: Number, 'default': 0}, + down: {type: Number, 'default': 0}, + collection: {type: Schema.Types.Mixed, 'default': {}} // {feather:1, ingot:2} + } + } }, preferences: { armorSet: String, @@ -250,13 +255,13 @@ var UserSchema = new Schema({ int: {type: Number, 'default': 0}, per: {type: Number, 'default': 0}, buffs: { - str: Number, - def: Number, - per: Number, - con: Number, - stealth: Number, - streaks: Boolean, - snowball: Boolean + str: {type: Number, 'default': 0}, + def: {type: Number, 'default': 0}, + per: {type: Number, 'default': 0}, + con: {type: Number, 'default': 0}, + stealth: {type: Number, 'default': 0}, + streaks: {type: Boolean, 'default': false}, + snowball: {type: Boolean, 'default': false} } }, tags: [ diff --git a/views/options/inventory/inventory.jade b/views/options/inventory/inventory.jade index fdb87014a6..14d86bec34 100644 --- a/views/options/inventory/inventory.jade +++ b/views/options/inventory/inventory.jade @@ -31,7 +31,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html') p(ng-show='eggCount < 1') You don't have any eggs. div(ng-repeat='(egg,points) in ownedItems(user.items.eggs)') //TODO move positioning this styling to css - button.customize-option(popover='{{Content.eggs[egg].notes}}', popover-title='{{Content.eggs[egg].text}} Egg', popover-trigger='mouseenter', popover-placement='right', ng-click='chooseEgg(egg)', class='Pet_Egg_{{egg}}', ng-class='{selectableInventory: selectedPotion && !user.items.pets[egg+"-"+selectedPotion.name]}') + button.customize-option(popover='{{Content.eggs[egg].notes}}', popover-title='{{Content.eggs[egg].text}} Egg', popover-trigger='mouseenter', popover-placement='right', ng-click='chooseEgg(egg)', class='Pet_Egg_{{egg}}', ng-class='{selectableInventory: selectedPotion && !user.items.pets[egg+"-"+selectedPotion.key]}') .badge.badge-info.stack-count {{points}} //-p {{Content.eggs[egg].text}} @@ -39,7 +39,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html') menu.hatchingPotion-menu(label='Hatching Potions ({{potCount}})') p(ng-show='potCount < 1') You don't have any hatching potions. div(ng-repeat='(pot,points) in ownedItems(user.items.hatchingPotions)') - button.customize-option(popover='{{Content.hatchingPotions[pot].notes}}', popover-title='{{Content.hatchingPotions[pot].text}} Potion', popover-trigger='mouseenter', popover-placement='right', ng-click='choosePotion(pot)', class='Pet_HatchingPotion_{{pot}}', ng-class='{selectableInventory: selectedEgg && !user.items.pets[selectedEgg.name+"-"+pot]}') + button.customize-option(popover='{{Content.hatchingPotions[pot].notes}}', popover-title='{{Content.hatchingPotions[pot].text}} Potion', popover-trigger='mouseenter', popover-placement='right', ng-click='choosePotion(pot)', class='Pet_HatchingPotion_{{pot}}', ng-class='{selectableInventory: selectedEgg && !user.items.pets[selectedEgg.key+"-"+pot]}') .badge.badge-info.stack-count {{points}} li.customize-menu @@ -79,17 +79,17 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html') | Welcome to the Market! Buy hard-to-find eggs and potions! Sell your extras! Commission useful services! Come see what we have to offer. p button.btn.btn-primary(ng-show='selectedEgg', ng-click='sellInventory()') - | Sell {{selectedEgg.name}} for {{selectedEgg.value}} Gold + | Sell {{selectedEgg.key}} for {{selectedEgg.value}} Gold button.btn.btn-primary(ng-show='selectedPotion', ng-click='sellInventory()') - | Sell {{selectedPotion.name}} for {{selectedPotion.value}} Gold + | Sell {{selectedPotion.key}} for {{selectedPotion.value}} Gold button.btn.btn-primary(ng-show='selectedFood', ng-click='sellInventory()') - | Sell {{selectedFood.name}} for {{selectedFood.value}} Gold + | Sell {{selectedFood.key}} for {{selectedFood.value}} Gold menu.inventory-list(type='list') li.customize-menu menu.pets-menu(label='Eggs') div(ng-repeat='egg in Content.eggs') - button.customize-option(popover='{{egg.notes}}', popover-title='{{egg.text}} Egg', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("eggs", egg)', class='Pet_Egg_{{egg.name}}') + button.customize-option(popover='{{egg.notes}}', popover-title='{{egg.text}} Egg', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("eggs", egg)', class='Pet_Egg_{{egg.key}}') p | {{egg.value}} span.Pet_Currency_Gem1x.inline-gems @@ -97,15 +97,15 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html') li.customize-menu menu.pets-menu(label='Hatching Potions') div(ng-repeat='pot in Content.hatchingPotions') - button.customize-option(popover='{{pot.notes}}', popover-title='{{pot.text}} Potion', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("hatchingPotions", pot)', class='Pet_HatchingPotion_{{pot.name}}') + button.customize-option(popover='{{pot.notes}}', popover-title='{{pot.text}} Potion', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("hatchingPotions", pot)', class='Pet_HatchingPotion_{{pot.key}}') p | {{pot.value}} span.Pet_Currency_Gem1x.inline-gems li.customize-menu menu.pets-menu(label='Food') - div(ng-repeat='food in Content.food', ng-show='food.name !== "Saddle"') - button.customize-option(popover='{{food.notes}}', popover-title='{{food.text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("food", food)', class='Pet_Food_{{food.name}}') + div(ng-repeat='food in Content.food', ng-show='food.key !== "Saddle"') + button.customize-option(popover='{{food.notes}}', popover-title='{{food.text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("food", food)', class='Pet_Food_{{food.key}}') p | {{food.value}} span.Pet_Currency_Gem1x.inline-gems @@ -121,7 +121,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html') li.customize-menu menu.pets-menu(label='Special') div - button.customize-option(popover='{{Content.food.Saddle.notes}}', popover-title='{{Content.food.Saddle.text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("food", Content.food.Saddle)', class='Pet_Food_{{Content.food.Saddle.name}}') + button.customize-option(popover='{{Content.food.Saddle.notes}}', popover-title='{{Content.food.Saddle.text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("food", Content.food.Saddle)', class='Pet_Food_{{Content.food.Saddle.key}}') p | {{Content.food.Saddle.value}} span.Pet_Currency_Gem1x.inline-gems diff --git a/views/options/inventory/stable.jade b/views/options/inventory/stable.jade index e068bdd0ea..4da7d4830f 100644 --- a/views/options/inventory/stable.jade +++ b/views/options/inventory/stable.jade @@ -16,8 +16,8 @@ script(type='text/ng-template', id='partials/options.inventory.mounts.html') menu.pets(type='list') li.customize-menu(ng-repeat='egg in Content.eggs') menu - div(ng-repeat='potion in Content.hatchingPotions', popover-trigger='mouseenter', popover='{{potion.text}} {{egg.mountText}}', popover-placement='bottom', ng-init='mount = egg.name+"-"+potion.name') - button(class="pet-button Mount_Head_{{mount}}", ng-show='user.items.mounts[mount]', ng-class='{active: user.items.currentMount == mount}', ng-click='chooseMount(egg.name, potion.name)') + div(ng-repeat='potion in Content.hatchingPotions', popover-trigger='mouseenter', popover='{{potion.text}} {{egg.mountText}}', popover-placement='bottom', ng-init='mount = egg.key+"-"+potion.key') + button(class="pet-button Mount_Head_{{mount}}", ng-show='user.items.mounts[mount]', ng-class='{active: user.items.currentMount == mount}', ng-click='chooseMount(egg.key, potion.key)') //div(class='Mount_Head_{{mount}}') button(class="pet-button pet-not-owned", ng-hide='user.items.mounts[mount]') .PixelPaw @@ -49,8 +49,8 @@ script(type='text/ng-template', id='partials/options.inventory.pets.html') menu.pets(type='list') li.customize-menu(ng-repeat='egg in Content.eggs') menu - div(ng-repeat='potion in Content.hatchingPotions', popover-trigger='mouseenter', popover='{{potion.text}} {{egg.text}}', popover-placement='bottom', ng-init='pet = egg.name+"-"+potion.name') - button(class="pet-button Pet-{{pet}}", ng-if='user.items.pets[pet]>0', ng-class='{active: user.items.currentPet == pet, selectableInventory: selectedFood}', ng-click='choosePet(egg.name, potion.name)') + div(ng-repeat='potion in Content.hatchingPotions', popover-trigger='mouseenter', popover='{{potion.text}} {{egg.text}}', popover-placement='bottom', ng-init='pet = egg.key+"-"+potion.key') + button(class="pet-button Pet-{{pet}}", ng-if='user.items.pets[pet]>0', ng-class='{active: user.items.currentPet == pet, selectableInventory: selectedFood}', ng-click='choosePet(egg.key, potion.key)') .progress(ng-class='{"progress-success": user.items.pets[pet]<50}') .bar(style="width: {{user.items.pets[pet]/.5}}%;") button(class="pet-button pet-not-owned", ng-if='!user.items.pets[pet]') @@ -61,8 +61,9 @@ script(type='text/ng-template', id='partials/options.inventory.pets.html') div button(ng-if='user.items.pets["Wolf-Veteran"]', class="pet-button Pet-Wolf-Veteran", ng-class='{active: user.items.currentPet == "Wolf-Veteran"}', ng-click='choosePet("Wolf", "Veteran")', popover='Veteran Wolf', popover-trigger='mouseenter', popover-placement='bottom') button(ng-if='user.items.pets["Wolf-Cerberus"]', class="pet-button Pet-Wolf-Cerberus", ng-class='{active: user.items.currentPet == "Wolf-Cerberus"}', ng-click='choosePet("Wolf", "Cerberus")', popover='Cerberus Pup', popover-trigger='mouseenter', popover-placement='bottom') - button(ng-if='user.items.pets["Dragon-Hydra"]', class="pet-button Pet-Dragon-Hydra", ng-class='{active: user.items.currentPet == "Dragon-Hydra"}', ng-click='choosePet("Dragon", "Hydra")', popover='Hydra', popover-trigger='mouseenter', popover-placement='bottom') button(ng-if='user.items.pets["Turkey-Base"]', class="pet-button Pet-Turkey-Base", ng-class='{active: user.items.currentPet == "Turkey-Base"}', ng-click='choosePet("Turkey", "Base")', popover='Turkey', popover-trigger='mouseenter', popover-placement='bottom') + button(ng-if='user.items.pets["BearCub-Polar"]', class="pet-button Pet-BearCub-Polar", ng-class='{active: user.items.currentPet == "BearCub-Polar"}', ng-click='choosePet("BearCub", "Polar")', popover='Polar Bear Cub', popover-trigger='mouseenter', popover-placement='bottom') + button(ng-if='user.items.pets["Dragon-Hydra"]', class="pet-button Pet-Dragon-Hydra", ng-class='{active: user.items.currentPet == "Dragon-Hydra"}', ng-click='choosePet("Dragon", "Hydra")', popover='Hydra', popover-trigger='mouseenter', popover-placement='bottom') a(target='_blank', href='http://habitrpg.wikia.com/wiki/Contributing_to_HabitRPG') button(ng-if='!user.items.pets["Dragon-Hydra"]', class="pet-button pet-not-owned", popover-trigger='mouseenter', popover-placement='right', popover="Click the gold paw to learn more about how you can obtain this rare pet through contributing to HabitRPG!", popover-title='How to Get this Pet!') .PixelPaw-Gold diff --git a/views/options/social/chat-box.jade b/views/options/social/chat-box.jade index 3d55599b8b..ff4533c6ab 100644 --- a/views/options/social/chat-box.jade +++ b/views/options/social/chat-box.jade @@ -13,6 +13,4 @@ form.chat-form(ng-submit='postChat(group,message.content)') input.btn.chat-btn(type='submit', value='Send Chat', ng-class='{disabled: _sending == true}') td button.btn(type="button", ng-click='sync(group)', tooltip='Fetch Recent Messages') - i(class='pull-right icon-refresh') - - + i(class='pull-right icon-refresh') \ No newline at end of file diff --git a/views/options/social/chat-message.jade b/views/options/social/chat-message.jade index 0baa423a6e..3c83b7abf2 100644 --- a/views/options/social/chat-message.jade +++ b/views/options/social/chat-message.jade @@ -9,5 +9,5 @@ li(bindonce='group.chat', ng-repeat='message in group.chat', bo-class='{highligh a(bo-show='user.contributor.admin || message.uuid == user.id', ng-click='deleteChatMessage(group, message)') | i.icon-remove(tooltip='Delete') - a.label.chat-message(class='float-label', bo-class='{"label-npc": message.backer.npc, "label-contributor-{{message.contributor.level}}":message.contributor.level}', ng-click='clickMember(message.uuid, true)') - span(tooltip='{{contribText(message.contributor, message.backer)}}') {{message.user}}  + a.label.chat-message(bo-show='message.user', class='float-label', bo-class='{"label-npc": message.backer.npc, "label-contributor-{{message.contributor.level}}":message.contributor.level}', ng-click='clickMember(message.uuid, true)') + span(tooltip='{{contribText(message.contributor, message.backer)}}') {{message.user}}  \ No newline at end of file diff --git a/views/options/social/group.jade b/views/options/social/group.jade index 64a924d957..843306a8ba 100644 --- a/views/options/social/group.jade +++ b/views/options/social/group.jade @@ -31,10 +31,10 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' | {{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.hp, Content.quests[group.quest.key].hp)}}%;') + .bar(style='width: {{Shared.percent(group.quest.progress.hp, Content.quests[group.quest.key].stats.hp)}}%;') span.meter-text i.icon-heart - | {{group.quest.hp | number:0}} / {{Content.quests[group.quest.key].hp}} + | {{group.quest.progress.hp | number:0}} / {{Content.quests[group.quest.key].stats.hp}} p {{Content.quests[group.quest.key].notes}} // ------ Information ------- diff --git a/views/shared/modals/drops.jade b/views/shared/modals/drops.jade index ccac0bd490..d9c25b5859 100644 --- a/views/shared/modals/drops.jade +++ b/views/shared/modals/drops.jade @@ -3,7 +3,7 @@ div(modal='modals.dropsEnabled') h3 Drops Enabled! .modal-body //-p // TODO how to handle random first drop? - span.item-drop-icon(class='Pet_Egg_{{user.items.eggs.0.name}}', style='margin-left: 0px') + span.item-drop-icon(class='Pet_Egg_{{user.items.eggs.0.key}}', style='margin-left: 0px') | You've unlocked the Drop System! Now when you complete tasks, you have a small chance of finding an item. You just found a strong {{user.items.eggs.0.text}} Egg | ! {{user.items.eggs.0.notes}}. @@ -22,7 +22,7 @@ div(modal='modals.drop') h3 An item has dropped! .modal-body p - span.item-drop-icon(class='Pet_{{user._tmp.drop.type}}_{{user._tmp.drop.name}}') + span.item-drop-icon(class='Pet_{{user._tmp.drop.type}}_{{user._tmp.drop.key}}') | {{user._tmp.drop.dialog}} .modal-footer button.btn.btn-default.cancel(ng-click='modals.drop = false') Close \ No newline at end of file diff --git a/views/shared/modals/quests.jade b/views/shared/modals/quests.jade index edd593abfe..4d5fa33956 100644 --- a/views/shared/modals/quests.jade +++ b/views/shared/modals/quests.jade @@ -6,8 +6,18 @@ div(modal='modals.showQuest', ng-controller='InventoryCtrl') tr td p {{selectedQuest.notes}} + br + .well + h5 Rewards + table.table.table-striped + tr + td {{selectedQuest.drop.text}} + tr + td {{selectedQuest.drop.exp}} Experience + tr + td {{selectedQuest.drop.gp}} Gold td - div(class='quest_{{selectedQuest.name}}') + div(class='quest_{{selectedQuest.key}}') hr div(style='clear:left;clear:right') .npc_ian.pull-left @@ -18,10 +28,21 @@ div(modal='modals.showQuest', ng-controller='InventoryCtrl') div(modal='party.quest.key && !questHold && party.quest.members[user._id] == undefined') .modal-header - h3 Quest Invitation + h3 Quest Invitation: {{Content.quests[party.quest.key].text}} .modal-body - p You have been invited to {{Content.quests[party.quest.key].text}}! - p (TODO list rewards) + p You have been invited to "{{Content.quests[party.quest.key].text}}"! + br + p {{Content.quests[party.quest.key].notes}} + br + .well + h5 Rewards + table.table.table-striped + tr + td {{Content.quests[party.quest.key].drop.text}} + tr + td {{Content.quests[party.quest.key].drop.exp}} Experience + tr + td {{Content.quests[party.quest.key].drop.gp}} Gold .modal-footer button.btn.btn-default.btn-small.btn-cancel(ng-click='questHold = true') Ask Later button.btn.btn-default.btn-small.btn-cancel(ng-click='party.$questReject()') Reject From ae403a55a53df232d6db7746afec2ec324b97ef6 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 23 Dec 2013 02:56:55 -0700 Subject: [PATCH 03/10] quests: add abort feature --- public/js/controllers/groupsCtrl.js | 6 ++++++ public/js/services/groupServices.js | 3 ++- src/controllers/groups.js | 20 ++++++++++++++++++-- src/routes/apiv2.js | 1 + views/options/social/group.jade | 1 + 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index 2b7c2c757e..39d809df51 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -300,6 +300,12 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'A //User.user.invitations.party = undefined; User.set({'invitations.party':{}}); } + + $scope.questAbort = function(){ + if (!confirm("Are you sure you want to abort this mission? It will abort it for everyone in your party.")) return; + if (!confirm("Are you double sure? Make sure they won't hate you forever!")) return; + $rootScope.party.$questAbort(); + } } ]) diff --git a/public/js/services/groupServices.js b/public/js/services/groupServices.js index 4dc7f7d4e2..c4c5fac1ba 100644 --- a/public/js/services/groupServices.js +++ b/public/js/services/groupServices.js @@ -17,7 +17,8 @@ angular.module('groupServices', ['ngResource']). leave: {method: "POST", url: API_URL + '/api/v2/groups/:gid/leave'}, invite: {method: "POST", url: API_URL + '/api/v2/groups/:gid/invite'}, questAccept: {method: "POST", url: API_URL + '/api/v2/groups/:gid/questAccept'}, - questReject: {method: "POST", url: API_URL + '/api/v2/groups/:gid/questReject'} + questReject: {method: "POST", url: API_URL + '/api/v2/groups/:gid/questReject'}, + questAbort: {method: "POST", url: API_URL + '/api/v2/groups/:gid/questAbort'} }); // Defer loading everything until they're requested diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 70ce37279c..6eaead5240 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -491,5 +491,21 @@ api.questReject = function(req, res, next) { } -//TODO -function questAbort(){} +api.questAbort = function(req, res, next){ + var group = res.locals.group; + var parallel = _.transform(group.members, function(m,v){ + m.push(function(cb){ + v.party.quest = {}; + v.party.quest.tally.collection = {};v.markModified('party.quest'); + v._v++; + v.save(cb); + }) + }); + group.quest = {}; + parallel.push(function(cb){group.save(cb);}); + async.parallel(parallel,function(err){ + if (err) return res.json(500,{err:err}); + res.json(group); + }) + +} diff --git a/src/routes/apiv2.js b/src/routes/apiv2.js index ff266fcbec..0cb32a30a9 100644 --- a/src/routes/apiv2.js +++ b/src/routes/apiv2.js @@ -100,6 +100,7 @@ router.post('/groups/:gid/invite', auth.auth, groups.attachGroup, groups.invite) router.post('/groups/:gid/removeMember', auth.auth, groups.attachGroup, groups.removeMember); router.post('/groups/:gid/questAccept', auth.auth, groups.attachGroupPopulated, groups.questAccept); // query={key} (optional. if provided, trigger new invite, if not, accept existing invite) router.post('/groups/:gid/questReject', auth.auth, groups.attachGroupPopulated, groups.questReject); +router.post('/groups/:gid/questAbort', auth.auth, groups.attachGroupPopulated, groups.questAbort); //GET /groups/:gid/chat router.post('/groups/:gid/chat', auth.auth, groups.attachGroup, groups.postChat); diff --git a/views/options/social/group.jade b/views/options/social/group.jade index 843306a8ba..07fe18feaf 100644 --- a/views/options/social/group.jade +++ b/views/options/social/group.jade @@ -36,6 +36,7 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' 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 // ------ Information ------- .modal.inline-modal From f538a915386c9154a323eabf5050b96565289a5c Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 23 Dec 2013 12:43:53 -0700 Subject: [PATCH 04/10] bosses: no models, performance improvements --- src/controllers/groups.js | 67 ++++++++--------- src/controllers/user.js | 36 +++------ src/models/group.js | 117 +++++++++++++++-------------- src/models/user.js | 5 +- src/routes/apiv2.js | 6 +- views/options/inventory/index.jade | 2 +- 6 files changed, 110 insertions(+), 123 deletions(-) diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 6eaead5240..74d0885d69 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -200,15 +200,6 @@ api.attachGroup = function(req, res, next) { }) } -api.attachGroupPopulated = function(req, res, next) { - Group.findById(req.params.gid).populate('members').exec(function(err, group){ - if(err) return res.json(500, {err:err}); - if(!group) return res.json(404, {err: "Group not found"}); - res.locals.group = group; - next(); - }) -} - /** * TODO make this it's own ngResource so we don't have to send down group data with each chat post */ @@ -418,24 +409,29 @@ questStart = function(req, res) { }) } - var parallel = []; + var parallel = [], questMembers = {}; var key = group.quest.key; // TODO will this handle appropriately when people leave/join party between quest invite? _.each(group.members, function(m){ - if (m._id == user._id) m.items.quests[key]--; - if (group.quest.members[m._id] == true) { - m.party.quest.key = key; + var updates = {$set:{},$inc:{'_v':1}}; + if (m == user._id) + updates['$inc']['items.quests.'+key] = -1; + if (group.quest.members[m] == true) { + updates['$set']['party.quest.key'] = key; + questMembers[m] = true; } else { - _.merge(m.party.quest, {key:undefined,collection:{}}); - delete group.quest.members[m._id]; + updates['$unset'] = {'party.quest.key':undefined}; + updates['$set']['party.quest.collection'] = {}; } - m._v++; - parallel.push(function(cb2){m.save(cb2);}); + parallel.push(function(cb2){ + User.update({_id:m},updates,cb2); + }); }) group.quest.active = true; group.quest.progress.hp = shared.content.quests[group.quest.key].stats.hp; - parallel.push(function(cb2){group.save(cb2);}); + group.quest.members = questMembers; + parallel.push(function(cb2){group.save(cb2)}); async.parallel(parallel,function(err, results){ if (err) return res.json(500,{err:err}); @@ -459,11 +455,10 @@ api.questAccept = function(req, res) { // Invite everyone. true means "accepted", false="rejected", undefined="pending". Once we click "start quest" // or everyone has either accepted/rejected, then we store quest key in user object. _.each(group.members, function(m){ - if (m._id == user._id) { - group.quest.members[m._id] = true; - } else { - group.quest.members[m._id] = undefined; - } + if (m == user._id) + group.quest.members[m] = true; + else + group.quest.members[m] = undefined; }); // Party member accepting the invitation @@ -484,7 +479,7 @@ api.questReject = function(req, res, next) { group.save(function(err,saved){ if (err) return res.json(500,{err:err}); - return res.json(200,saved); + res.json(200,saved); }); questStart(req,res); @@ -493,19 +488,19 @@ api.questReject = function(req, res, next) { api.questAbort = function(req, res, next){ var group = res.locals.group; - var parallel = _.transform(group.members, function(m,v){ - m.push(function(cb){ - v.party.quest = {}; - v.party.quest.tally.collection = {};v.markModified('party.quest'); - v._v++; - v.save(cb); - }) - }); - group.quest = {}; - parallel.push(function(cb){group.save(cb);}); - async.parallel(parallel,function(err){ + async.parallel([ + function(cb){ + User.update({_id:{$in: _.keys(group.quest.members)}},{ + $set:{'party.quest.key':undefined,'party.quest.tally.collection':{}}, + $inc:{_v:1} + },cb); + }, + function(cb) { + group.quest = {}; + group.save(cb); + } + ], function(err){ if (err) return res.json(500,{err:err}); res.json(group); }) - } diff --git a/src/controllers/user.js b/src/controllers/user.js index 086d37fb63..f6ec6e20a1 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -200,33 +200,21 @@ api.cron = function(req, res, next) { // If user is on a quest, roll for boss & player if (user.party.quest.key && tally && (tally.up || tally.down)) { async.waterfall([ - function(cb){ - Group.findOne({type: 'party', members: {'$in': [user._id]}}) - .populate({ - path: 'members', - match: {'party.quest.key':user.party.quest.key} - }) - .exec(cb); + 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 quest = shared.content.quests[group.quest.key]; - var down = tally.down * quest.stats.str; // multiply by boss strength - var parallel = []; - _.each(group.members, function(m){ - parallel.push(function(cb2){ - m.stats.hp += down; - m._v++; - m.save(cb2); - }) - }) - // Use http://js2coffee.org/ for building this string. Man I wish JS had interpolation... - group.sendChat("`<" + user.profile.name + "> attacks <" + quest.name + "> for " + (tally.up.toFixed(1)) + " damage, <" + quest.name + "> attacks party for " + (tally.down.toFixed(1)) + " damage.`"); - parallel.push(function(cb2){ - group.hurtBoss(tally.up,cb2); - }); - async.parallel(parallel,cb); + group.bossAttack(user,tally,cb); + }, + function(updated,cb){ + // User has been updated in boss-grapple, reload + User.findById(user._id,cb); } - ], next); + ], function(err, saved) { + user = res.locals.user = saved; + next(err,saved); + }); } else { user.save(next); diff --git a/src/models/group.js b/src/models/group.js index e06939a3b3..f70e3f327c 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -3,6 +3,7 @@ var Schema = mongoose.Schema; var shared = require('habitrpg-shared'); var _ = require('lodash'); var async = require('async'); +var User = require('./user').model; var GroupSchema = new Schema({ _id: {type: String, 'default': shared.uuid}, @@ -106,66 +107,72 @@ GroupSchema.methods.sendChat = function(message, user){ group.chat.splice(200); } -GroupSchema.methods.hurtBoss = function(delta, cb) { +GroupSchema.methods.bossAttack = function(user, tally, cb) { var group = this; - group.quest.progress.hp -= delta; - var hp = group.quest.progress.hp; + var questK = group.quest.key; + var quest = shared.content.quests[questK]; + var dropK = quest.drop.key; + var down = tally.down * quest.stats.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.`"); + //var hp = group.quest.progress.hp; + + // Everyone takes damage + var series = [ + function(cb2){ + mongoose.models.User.update({_id:{$in: _.keys(group.quest.members)}}, {$inc:{'stats.hp':down, _v:1}}, {multi:true}, cb2); + } + ] + + // Boss slain, finish quest if (group.quest.progress.hp <= 0) { - var key = group.quest.key, - quest = shared.content.quests[key]; + 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'] = {}; - var parallel = _.reduce(group.members, function(m,v,k){ - - // Achievement - _.defaults(v.achievements, {quests:{}}) - if (!v.achievements.quests[key]) v.achievements.quests[key] = 0; - v.achievements.quests[key]++; - v.markModified('achievements'); - - // Drops - v.stats.gp += +quest.drop.gp; - v.stats.exp += +quest.drop.exp; - switch (quest.drop.type) { - case 'gear': - // TODO This means they can lose their new gear on death, is that what we want? - v.items.gear.owned[quest.drop.key] = true; - break; - case 'eggs': - case 'food': - case 'hatchingPotions': - if (!v.items.hatchingPotions[quest.drop.key]) v.items.hatchingPotions[quest.drop.key] = 0; - v.items.hatchingPotions[quest.drop.key]++; - break; - case 'pets': - if (!v.items.pets[quest.drop.key]) v.items.pets[quest.drop.key] = 5; - break; - case 'mounts': - v.items.mounts[quest.drop.key] = true; - break; - } - - v.party.quest.key = undefined; - v.party.quest.collection = {}; - v.markModified('party.quest'); - - v._v++; - m.push(function(cb3){ v.save(cb3); }) - return m; - }, []); - - // Finish the quest - group.quest = {};group.markModified('quest'); - group.sendChat('`' + quest.name + ' has been slain! Party has received their rewards`'); - - parallel.push(function(cb3){ - group.save(cb3); + 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); }) - async.parallel(parallel,cb); } - else { - group.save(cb); - } - return hp; + + series.push(function(cb2){group.save(cb2)}); + async.series(series,cb); + //return hp; } module.exports.schema = GroupSchema; diff --git a/src/models/user.js b/src/models/user.js index d0249cb433..7ad4062508 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -195,10 +195,7 @@ var UserSchema = new Schema({ } }, - lastCron: { - type: Date, - 'default': Date.now - }, + lastCron: {type: Date, 'default': Date.now}, party: { // id // FIXME can we use a populated doc instead of fetching party separate from user? diff --git a/src/routes/apiv2.js b/src/routes/apiv2.js index 0cb32a30a9..a44d5e8b1c 100644 --- a/src/routes/apiv2.js +++ b/src/routes/apiv2.js @@ -98,9 +98,9 @@ router.post('/groups/:gid/join', auth.auth, groups.attachGroup, groups.join); router.post('/groups/:gid/leave', auth.auth, groups.attachGroup, groups.leave); router.post('/groups/:gid/invite', auth.auth, groups.attachGroup, groups.invite); router.post('/groups/:gid/removeMember', auth.auth, groups.attachGroup, groups.removeMember); -router.post('/groups/:gid/questAccept', auth.auth, groups.attachGroupPopulated, groups.questAccept); // query={key} (optional. if provided, trigger new invite, if not, accept existing invite) -router.post('/groups/:gid/questReject', auth.auth, groups.attachGroupPopulated, groups.questReject); -router.post('/groups/:gid/questAbort', auth.auth, groups.attachGroupPopulated, groups.questAbort); +router.post('/groups/:gid/questAccept', auth.auth, groups.attachGroup, groups.questAccept); // query={key} (optional. if provided, trigger new invite, if not, accept existing invite) +router.post('/groups/:gid/questReject', auth.auth, groups.attachGroup, groups.questReject); +router.post('/groups/:gid/questAbort', auth.auth, groups.attachGroup, groups.questAbort); //GET /groups/:gid/chat router.post('/groups/:gid/chat', auth.auth, groups.attachGroup, groups.postChat); diff --git a/views/options/inventory/index.jade b/views/options/inventory/index.jade index dcd317cddd..f57e406c85 100644 --- a/views/options/inventory/index.jade +++ b/views/options/inventory/index.jade @@ -5,7 +5,7 @@ script(type='text/ng-template', id='partials/options.inventory.html') ul.nav.nav-tabs li(ng-class="{ active: $state.includes('options.inventory.drops') }") a(ui-sref='options.inventory.drops') - | Drops + | Market li(ng-class="{ active: $state.includes('options.inventory.pets') }") a(ui-sref='options.inventory.pets') | Pets From 094989db130326d29c4bce2405ecf98f0bcc0059 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 24 Dec 2013 10:25:09 -0700 Subject: [PATCH 05/10] quests: add collection quests, incl. polar bear pt 2, and quest arks, and achievements --- public/js/controllers/inventoryCtrl.js | 2 + src/controllers/auth.js | 16 +-- src/controllers/groups.js | 25 +++-- src/controllers/user.js | 54 +++++----- src/models/group.js | 125 +++++++++++++++--------- src/models/user.js | 2 +- views/options/inventory/inventory.jade | 2 +- views/options/social/group.jade | 64 +++++++----- views/shared/profiles/achievements.jade | 15 ++- 9 files changed, 185 insertions(+), 120 deletions(-) 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 From 5c5f4abcd42e5d31f3ea831163877ab825e4eaad Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 24 Dec 2013 20:30:30 -0700 Subject: [PATCH 06/10] add missing-gems migration --- migrations/missing_gems.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 migrations/missing_gems.js diff --git a/migrations/missing_gems.js b/migrations/missing_gems.js new file mode 100644 index 0000000000..67b44e7b2c --- /dev/null +++ b/migrations/missing_gems.js @@ -0,0 +1 @@ +db.users.update({_id:''},{$set:{'purchased.ads':true},$inc:{balance:10}}); \ No newline at end of file From f2f1d6621edcb3517e296efc3e9a5416a2d6295c Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 24 Dec 2013 21:57:56 -0700 Subject: [PATCH 07/10] rename user.party.tally => user.party.progress --- src/controllers/groups.js | 6 +++--- src/controllers/user.js | 4 ++-- src/models/group.js | 14 +++++++------- src/models/user.js | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 7b52dbc7e1..bfb0691ada 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -422,11 +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}; + updates['$set']['party.quest.progress'] = {up:0,down:0,collect:collectTally}; questMembers[m] = true; } else { updates['$unset'] = {'party.quest.key':1}; - updates['$set']['party.quest.tally'] = {}; + updates['$set']['party.quest.progress'] = {}; } parallel.push(function(cb2){ User.update({_id:m},updates,cb2); @@ -501,7 +501,7 @@ api.questAbort = function(req, res, next){ function(cb){ User.update({_id:{$in: _.keys(group.quest.members)}},{ $unset: {'party.quest.key':1}, - $set: {'party.quest.tally.collect':{}}, + $set: {'party.quest.progress.collect':{}}, $inc: {_v:1} },cb); }, diff --git a/src/controllers/user.js b/src/controllers/user.js index a92428cf2f..bb519cb5e0 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -194,7 +194,7 @@ api.update = function(req, res, next) { api.cron = function(req, res, next) { var user = res.locals.user, - tally = user.fns.cron(), + progress = user.fns.cron(), ranCron = user.isModified(), quest = shared.content.quests[user.party.quest.key]; @@ -213,7 +213,7 @@ api.cron = function(req, res, next) { }, function(group, cb){ var type = quest.boss ? 'boss' : 'collect'; - group[type+'Quest'](user,tally,cb); + group[type+'Quest'](user,progress,cb); }, function(){ var cb = arguments[arguments.length-1]; diff --git a/src/models/group.js b/src/models/group.js index 949afc5557..aecafa0968 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -146,15 +146,15 @@ GroupSchema.methods.finishQuest = function(quest, cb) { mongoose.models.User.update({_id:{$in:members}}, updates, {multi:true}, cb); } -GroupSchema.methods.collectQuest = function(user, tally, cb) { +GroupSchema.methods.collectQuest = function(user, progress, cb) { var group = this, quest = shared.content.quests[group.quest.key]; - _.each(tally.collect,function(v,k){ + _.each(progress.collect,function(v,k){ group.quest.progress.collect[k] += v; }); - var foundText = _.reduce(tally.collect, function(m,v,k){ + var foundText = _.reduce(progress.collect, function(m,v,k){ m.push(v + ' ' + quest.collect[k].text); return m; }, []); @@ -178,13 +178,13 @@ GroupSchema.methods.collectQuest = function(user, tally, cb) { ],cb); } -GroupSchema.methods.bossQuest = function(user, tally, cb) { +GroupSchema.methods.bossQuest = function(user, progress, cb) { var group = this; var quest = shared.content.quests[group.quest.key]; - var down = tally.down * quest.boss.str; // multiply by boss strength + var down = progress.down * quest.boss.str; // multiply by boss strength - group.quest.progress.hp -= tally.up; - 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.`"); + group.quest.progress.hp -= progress.up; + group.sendChat("`<" + user.profile.name + "> attacks <" + quest.boss.name + "> for " + (progress.up.toFixed(1)) + " damage, <" + quest.boss.name + "> attacks party for " + (down.toFixed(1)) + " damage.`"); //var hp = group.quest.progress.hp; // Everyone takes damage diff --git a/src/models/user.js b/src/models/user.js index e089a9215a..b7f4cd6167 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -203,7 +203,7 @@ var UserSchema = new Schema({ order: {type:String, 'default':'level'}, quest: { key: String, - tally: { + progress: { up: {type: Number, 'default': 0}, down: {type: Number, 'default': 0}, collect: {type: Schema.Types.Mixed, 'default': {}} // {feather:1, ingot:2} From f8559b17629a8abd3bba4c807f6c24307b68b587 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 24 Dec 2013 23:30:48 -0700 Subject: [PATCH 08/10] bosses: pretty things up amongst the modals, etc. Move rewards to a directive so we can share it. Add dialog on quest-completion using user.party.quest.completed flag --- public/js/directives/directives.js | 11 ++++++ src/controllers/groups.js | 7 ++-- src/models/group.js | 3 +- src/models/user.js | 3 +- views/options/inventory/inventory.jade | 2 +- views/options/social/group.jade | 26 ++++++++++++- views/shared/modals/quests.jade | 54 ++++++++++++-------------- 7 files changed, 70 insertions(+), 36 deletions(-) diff --git a/public/js/directives/directives.js b/public/js/directives/directives.js index 467395752f..e550cff864 100644 --- a/public/js/directives/directives.js +++ b/public/js/directives/directives.js @@ -218,3 +218,14 @@ habitrpg.directive('fromNow', ['$interval', function($interval){ }); } }]); + +habitrpg.directive('questRewards', ['$rootScope', function($rootScope){ + return { + restrict: 'AE', + templateUrl: 'partials/options.social.party.quest-rewards.html', + link: function(scope, element, attrs){ + scope.header = attrs.header || 'Rewards'; + scope.quest = $rootScope.Content.quests[attrs.key]; + } + } +}]) diff --git a/src/controllers/groups.js b/src/controllers/groups.js index bfb0691ada..941a66d859 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -413,7 +413,7 @@ questStart = function(req, res) { questMembers = {}, key = group.quest.key, quest = shared.content.quests[key], - collectTally = quest.collect ? _.transform(quest.collect, function(m,v,k){m[k]=0}) : {}; + collected = 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){ @@ -422,7 +422,8 @@ 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.progress'] = {up:0,down:0,collect:collectTally}; + updates['$set']['party.quest.progress'] = {up:0,down:0,collect:collected}; + updates['$unset'] = {'party.quest.completed':1}; questMembers[m] = true; } else { updates['$unset'] = {'party.quest.key':1}; @@ -437,7 +438,7 @@ questStart = function(req, res) { if (quest.boss) group.quest.progress.hp = quest.boss.hp; else - group.quest.progress.collect = collectTally; + group.quest.progress.collect = collected; group.quest.members = questMembers; group.markModified('quest'); // members & progress.collect are both Mixed types parallel.push(function(cb2){group.save(cb2)}); diff --git a/src/models/group.js b/src/models/group.js index aecafa0968..71823fb41e 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -115,12 +115,13 @@ GroupSchema.methods.finishQuest = function(quest, cb) { var dropK = quest.drop.key; var updates = {$inc:{},$set:{}}; - updates['$inc']['achievements.quests.'+questK] = 1; + 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'] = {}; + updates['$set']['party.quest.completed'] = questK; switch (quest.drop.type) { case 'gear': diff --git a/src/models/user.js b/src/models/user.js index b7f4cd6167..1c32342c1c 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -207,7 +207,8 @@ var UserSchema = new Schema({ up: {type: Number, 'default': 0}, down: {type: Number, 'default': 0}, collect: {type: Schema.Types.Mixed, 'default': {}} // {feather:1, ingot:2} - } + }, + completed: String // When quest is done, we move it from key => completed, and it's a one-time flag (for modal) that they unset by clicking "ok" in browser } }, preferences: { diff --git a/views/options/inventory/inventory.jade b/views/options/inventory/inventory.jade index f509f0063b..560b104dbd 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', ng-class='{locked: quest.previous && !user.achievements.quests[quest.previous]}') + button.customize-option(popover='{{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 d48d9e62a7..c9f9b16bae 100644 --- a/views/options/social/group.jade +++ b/views/options/social/group.jade @@ -10,7 +10,7 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' // ------ Bosses ------- .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==false') Quest Invitation: {{Content.quests[group.quest.key].text}} h3(ng-if='group.quest.active==true') {{Content.quests[group.quest.key].text}} .modal-body div(ng-if='group.quest.active==false') @@ -19,6 +19,8 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' 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 + //-TODO Cancel button + //-TODO Both force-start & cancel should only be available to quest-initiator div(ng-if='group.quest.active==true') div(ng-if='Content.quests[group.quest.key].boss') @@ -46,6 +48,28 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' {{v}} / {{Content.quests[group.quest.key].collect[k].count}} p {{Content.quests[group.quest.key].notes}} + hr + h5 Participants + table.table.table-striped + tr(ng-repeat='(k,v) in group.members', ng-show='group.quest.members[v._id]') + td {{v.profile.name}} + quest-rewards(key='{{group.quest.key}}') + + div(ng-if='Content.quests[group.quest.key].boss') + .npc_ian.pull-left + p To hurt a boss, complete your Dailies and To-Dos. Higher task-damage (completing reds, Wizard spells, Warrior attacks...) translates to high Boss damage. + br + p At the end of each day, every daily you missed will be tallied up, and the boss will deal that much damage (multiplied by the bosses Strength) to the party. So help your friends by completing your dailies! + br + p Only participants will fight the boss and share in the quest loot. If you die during a boss-battle, you get booted from the battle. If everyone dies once, the quest fails. + + div(ng-if='Content.quests[group.quest.key].collect') + .npc_ian.pull-left + p To collect items, do your positive tasks. Quest items drop just like normal items; however, you won't see the drops until the next day, then everything you've found will be tallied up and contributed to the pile. + br + p Only participants can collect items and share in the quest loot. If you die during a quest, you get booted from the quest. If everyone dies once, the quest fails. + + button.btn.btn-mini.btn-danger(ng-click='questAbort()') Abort // ------ Information ------- diff --git a/views/shared/modals/quests.jade b/views/shared/modals/quests.jade index 4d5fa33956..3f931c757a 100644 --- a/views/shared/modals/quests.jade +++ b/views/shared/modals/quests.jade @@ -1,27 +1,34 @@ +script(id='partials/options.social.party.quest-rewards.html', type='text/ng-template') + hr + h5 {{header}} + table.table.table-striped + tr + td {{quest.drop.text}} + tr + td {{quest.drop.exp}} Experience + tr + td {{quest.drop.gp}} Gold + +div(modal='user.party.quest.completed', ng-controller='InventoryCtrl') + .modal-header + h3 "{{Content.quests[user.party.quest.completed].text}}" Completed! + .modal-body + p {{Content.quests[user.party.quest.completed].completion}} + quest-rewards(key='{{user.party.quest.completed}}', header="You've Received") + .modal-footer + button.btn.btn-default.btn-primary(ng-click='set({"party.quest.completed":""})') Accept + div(modal='modals.showQuest', ng-controller='InventoryCtrl') .modal-header h3 {{selectedQuest.text}} .modal-body - table - tr - td - p {{selectedQuest.notes}} - br - .well - h5 Rewards - table.table.table-striped - tr - td {{selectedQuest.drop.text}} - tr - td {{selectedQuest.drop.exp}} Experience - tr - td {{selectedQuest.drop.gp}} Gold - td - div(class='quest_{{selectedQuest.key}}') + .pull-right(class='quest_{{selectedQuest.key}}') + p {{selectedQuest.notes}} + quest-rewards(key='{{selectedQuest.key}}') hr div(style='clear:left;clear:right') .npc_ian.pull-left - p Clicking "Invite" will send an invitation to your party members. When all members have accepted or denied, the quest begins. If they take to long, feel free to force-start the quest under Options > Social > Party + p Clicking "Invite" will send an invitation to your party members. When all members have accepted or denied, the quest begins. If they take to long, feel free to force-start the quest under Options > Social > Party. .modal-footer button.btn.btn-default.btn-small.btn-cancel(ng-click='closeQuest()') Cancel button.btn.btn-default.btn-primary(ng-click='questInit()') Invite Party @@ -30,19 +37,8 @@ div(modal='party.quest.key && !questHold && party.quest.members[user._id] == und .modal-header h3 Quest Invitation: {{Content.quests[party.quest.key].text}} .modal-body - p You have been invited to "{{Content.quests[party.quest.key].text}}"! - br p {{Content.quests[party.quest.key].notes}} - br - .well - h5 Rewards - table.table.table-striped - tr - td {{Content.quests[party.quest.key].drop.text}} - tr - td {{Content.quests[party.quest.key].drop.exp}} Experience - tr - td {{Content.quests[party.quest.key].drop.gp}} Gold + quest-rewards(key='{{party.quest.key}}') .modal-footer button.btn.btn-default.btn-small.btn-cancel(ng-click='questHold = true') Ask Later button.btn.btn-default.btn-small.btn-cancel(ng-click='party.$questReject()') Reject From 44e89d1be758bcb79eed4cd992e7e3c956b49cef Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 24 Dec 2013 23:56:11 -0700 Subject: [PATCH 09/10] bosses: misc small bugs --- public/js/controllers/inventoryCtrl.js | 3 ++- src/controllers/groups.js | 11 ++--------- views/options/social/group.jade | 9 +++++++-- views/shared/modals/quests.jade | 4 ++-- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/public/js/controllers/inventoryCtrl.js b/public/js/controllers/inventoryCtrl.js index 3c7dae52c8..dfb6e3715d 100644 --- a/public/js/controllers/inventoryCtrl.js +++ b/public/js/controllers/inventoryCtrl.js @@ -84,7 +84,8 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', } $scope.purchase = function(type, item){ - if (item.previous && !User.user.achievements.quests[item.previous]) + var completedPrevious = !item.previous || (User.user.achievements.quests && User.user.achievements.quests[item.previous]); + if (!completedPrevious) 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; diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 941a66d859..96521d3729 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -400,9 +400,8 @@ questStart = function(req, res) { group.markModified('quest'); // Not ready yet, wait till everyone's accepted, rejected, or we force-start - if (!force && _.findIndex(group.quest.members, function(m){ - return m === undefined; - })) { + var statuses = _.values(group.quest.members); + if (!force && (~statuses.indexOf(undefined) || ~statuses.indexOf(null))) { return group.save(function(err,saved){ if (err) return res.json(500,{err:err}); res.json(saved); @@ -486,12 +485,6 @@ api.questReject = function(req, res, next) { if (!group.quest.key) return res.json(400,{err:'No quest invitation has been sent out yet.'}); group.quest.members[user._id] = false; - - group.save(function(err,saved){ - if (err) return res.json(500,{err:err}); - res.json(200,saved); - }); - questStart(req,res); } diff --git a/views/options/social/group.jade b/views/options/social/group.jade index c9f9b16bae..ddc25beeec 100644 --- a/views/options/social/group.jade +++ b/views/options/social/group.jade @@ -17,8 +17,11 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' 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 + td {{group.quest.members[member._id] === true ? 'Accepted' : group.quest.members[member._id] === false ? 'Rejected' : 'Pending'}} + hr + .npc_ian.pull-left + p Once all members have either accepted or rejected, the quest begins. Only those that clicked "accept" will be able to participate in the quest and recieve the drops. If members are pending too long (inactive?), you can start without them by clicking "Begin". + button.btn.btn-small.btn-warning(ng-click='party.$questAccept({"force":true})') Begin //-TODO Cancel button //-TODO Both force-start & cancel should only be available to quest-initiator @@ -97,6 +100,8 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' div(ng-show='!group._editing') img.pull-right(ng-show='group.logo', style='max-width:150px', ng-src='{{group.logo}}') markdown(ng-model='group.description') + hr + small.muted Group ID: {{group._id}} include ./challenge-box diff --git a/views/shared/modals/quests.jade b/views/shared/modals/quests.jade index 3f931c757a..bc32ff28c5 100644 --- a/views/shared/modals/quests.jade +++ b/views/shared/modals/quests.jade @@ -28,12 +28,12 @@ div(modal='modals.showQuest', ng-controller='InventoryCtrl') hr div(style='clear:left;clear:right') .npc_ian.pull-left - p Clicking "Invite" will send an invitation to your party members. When all members have accepted or denied, the quest begins. If they take to long, feel free to force-start the quest under Options > Social > Party. + p Clicking "Invite" will send an invitation to your party members. When all members have accepted or denied, the quest begins. See status under Options > Social > Party. .modal-footer button.btn.btn-default.btn-small.btn-cancel(ng-click='closeQuest()') Cancel button.btn.btn-default.btn-primary(ng-click='questInit()') Invite Party -div(modal='party.quest.key && !questHold && party.quest.members[user._id] == undefined') +div(modal='party.quest.key && !party.quest.active && !questHold && party.quest.members[user._id] == undefined') .modal-header h3 Quest Invitation: {{Content.quests[party.quest.key].text}} .modal-body From fc4a141d2e213e73d339d26ed1c07486dc083ca0 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 25 Dec 2013 08:24:39 -0700 Subject: [PATCH 10/10] bosses: bailey --- views/shared/modals/new-stuff.jade | 60 ++++++++++++++++++------------ 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/views/shared/modals/new-stuff.jade b/views/shared/modals/new-stuff.jade index 4a85bf188a..1587717ddc 100644 --- a/views/shared/modals/new-stuff.jade +++ b/views/shared/modals/new-stuff.jade @@ -26,35 +26,47 @@ div(modal='modals.newStuff') a(target='_blank', href='https://twitter.com/Mihakuu') Bailey .popover-content - h4 Winter Wonderland Event! - p It's time for HabitRPG's biggest event yet - Winter Wonderland! The fun starts today, on the first day of winter, and ends on January 31st - HabitRPG's birthday. - p Get prepared to build new habits, earn fun drops, hold your party members accountable for their tasks, and decorate your avatar. Various features will be rolling out over the course of the event, so expect many updates! For starters... + h3 Winter Wonderland Event! + h4 Part 2: Rescue the Bears table.table.table-striped tr td - h4 NPC Decorations - p Looks like everyone is really getting into the winter spirit! Check out the new NPC sprites. (And I heard a rumor that the final NPC might show up, just in time for the new year...) - tr - td - .customize-option.hair_bangs_1_winternight.pull-right - h4 Limited-Edition Holiday Hair-Colors - p Now your avatar can dye their hair Candy Cane, Frost, Winter Sky, or Holly! You'll only be able to purchase these hair colors until January 31st, when they will be retired. - tr - td - .shop_snowball.pull-right - h4 The Great HabitRPG Snowball Fight - p Yes, you can now buy snowballs and hurl them at all your friends... to, uh, help them improve their habits. How? Weeeeellll, let's just say that after getting walloped, they might find themselves needing some extra gold to escape their predicament... - //-span.shop_head_special_candycane.item-img.shop-sprite - tr - td - h4 More to Come - p A beast is roaring in the distant mountains, mysterious tracks have appeared in the snow, and Lemoness is furiously crocheting something sparkly. - p It's going to be a wild winter. + h5 Quests & Bosses! + p A beast is roaring in the distant mountains, mysterious tracks have appeared in the snow. A new feature has been unlocked, Quests & Bosses. As a holiday present, HabitRPG gives you your first quest: "Evil Santa". Check your inventory, you have until Jan 31 to complete it! - hr - p By @lemoness - small.muted 12/20/2013 + p By @lefnire, @pandoro, @Shaners + small.muted 12/25/2013 + + div.muted + hr + p It's time for HabitRPG's biggest event yet - Winter Wonderland! The fun starts today, on the first day of winter, and ends on January 31st - HabitRPG's birthday. + p Get prepared to build new habits, earn fun drops, hold your party members accountable for their tasks, and decorate your avatar. Various features will be rolling out over the course of the event, so expect many updates! For starters... + h4 Part 1: The Great Snowball Fight + table.table.table-striped + tr + td + h5 NPC Decorations + p Looks like everyone is really getting into the winter spirit! Check out the new NPC sprites. (And I heard a rumor that the final NPC might show up, just in time for the new year...) + tr + td + .customize-option.hair_bangs_1_winternight.pull-right + h5 Limited-Edition Holiday Hair-Colors + p Now your avatar can dye their hair Candy Cane, Frost, Winter Sky, or Holly! You'll only be able to purchase these hair colors until January 31st, when they will be retired. + tr + td + .shop_snowball.pull-right + h5 The Great HabitRPG Snowball Fight + p Yes, you can now buy snowballs and hurl them at all your friends... to, uh, help them improve their habits. How? Weeeeellll, let's just say that after getting walloped, they might find themselves needing some extra gold to escape their predicament... + //-span.shop_head_special_candycane.item-img.shop-sprite + tr + td + h5 More to Come + p A beast is roaring in the distant mountains, mysterious tracks have appeared in the snow, and Lemoness is furiously crocheting something sparkly. + p It's going to be a wild winter. + + p By @lemoness + small.muted 12/20/2013 hr