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