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/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 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/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..39d809df51 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){ @@ -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/controllers/inventoryCtrl.js b/public/js/controllers/inventoryCtrl.js index 995f2fad83..dfb6e3715d 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 = { @@ -31,10 +32,10 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', 'API_URL', }, 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 { @@ -43,11 +44,11 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', 'API_URL', } $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 { @@ -56,7 +57,7 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', 'API_URL', } $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]; } @@ -64,8 +65,8 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', 'API_URL', 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; } } @@ -76,19 +77,22 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', 'API_URL', } $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; } $scope.purchase = function(type, item){ + 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; 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){ @@ -97,12 +101,12 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', 'API_URL', // 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 @@ -114,5 +118,20 @@ 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.key}, function(){ + $rootScope.party.$get(); + }); + $scope.closeQuest(); + } } ]); \ No newline at end of file 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/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/public/js/services/groupServices.js b/public/js/services/groupServices.js index 335e0b9d74..c4c5fac1ba 100644 --- a/public/js/services/groupServices.js +++ b/public/js/services/groupServices.js @@ -16,7 +16,9 @@ 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'}, + questAbort: {method: "POST", url: API_URL + '/api/v2/groups/:gid/questAbort'} }); // Defer loading everything until they're requested 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 e62596a455..96521d3729 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -206,30 +206,18 @@ api.attachGroup = 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]}); }); } @@ -398,5 +386,126 @@ 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 + 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); + }) + } + + var parallel = [], + questMembers = {}, + key = group.quest.key, + quest = shared.content.quests[key], + 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){ + 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; + 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}; + updates['$set']['party.quest.progress'] = {}; + } + parallel.push(function(cb2){ + User.update({_id:m},updates,cb2); + }); + }) + + group.quest.active = true; + if (quest.boss) + group.quest.progress.hp = quest.boss.hp; + else + 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)}); + + 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 == user._id) + group.quest.members[m] = true; + else + group.quest.members[m] = 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; + questStart(req,res); +} + + +api.questAbort = function(req, res, next){ + var group = res.locals.group; + async.parallel([ + function(cb){ + User.update({_id:{$in: _.keys(group.quest.members)}},{ + $unset: {'party.quest.key':1}, + $set: {'party.quest.progress.collect':{}}, + $inc: {_v:1} + },cb); + }, + function(cb) { + group.quest = {}; + group.markModified('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 705b87bd15..bb519cb5e0 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)); }); @@ -193,11 +193,38 @@ 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); + var user = res.locals.user, + progress = user.fns.cron(), + ranCron = user.isModified(), + quest = shared.content.quests[user.party.quest.key]; + + 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,progress,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); + }); + }; // api.reroll // Shared.ops @@ -347,15 +374,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 b8a7a428b3..71823fb41e 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -2,6 +2,8 @@ var mongoose = require("mongoose"); 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}, @@ -30,7 +32,20 @@ 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, + active: {type:Boolean, 'default':false}, + progress:{ + hp: Number, + 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 + //'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 @@ -71,5 +86,128 @@ 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); +} + +// Participants: Grant rewards & achievements, finish quest +GroupSchema.methods.finishQuest = function(quest, cb) { + var group = this; + + var questK = quest.key; + var dropK = quest.drop.key; + 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'] = {}; + updates['$set']['party.quest.completed'] = questK; + + 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, progress, cb) { + var group = this, + quest = shared.content.quests[group.quest.key]; + + _.each(progress.collect,function(v,k){ + group.quest.progress.collect[k] += v; + }); + + var foundText = _.reduce(progress.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, progress, cb) { + var group = this; + var quest = shared.content.quests[group.quest.key]; + var down = progress.down * quest.boss.str; // multiply by boss strength + + 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 + 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) { + group.sendChat('`' + quest.boss.name + ' has been slain! Party has received their rewards.`'); + // Participants: Grant rewards & achievements, finish quest + series.push(function(cb2){ + group.finishQuest(quest,cb2); + }); + } + + series.push(function(cb2){group.save(cb2)}); + async.series(series,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 6fdd789e55..1c32342c1c 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, @@ -182,25 +183,33 @@ 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} } }, - lastCron: { - type: Date, - 'default': Date.now - }, + lastCron: {type: Date, 'default': Date.now}, - // FIXME remove? 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'} + order: {type:String, 'default':'level'}, + quest: { + key: String, + progress: { + 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: { armorSet: String, @@ -244,13 +253,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/src/routes/apiv2.js b/src/routes/apiv2.js index bfb4c8c5db..a44d5e8b1c 100644 --- a/src/routes/apiv2.js +++ b/src/routes/apiv2.js @@ -98,6 +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.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 diff --git a/views/options/inventory/inventory.jade b/views/options/inventory/inventory.jade index 0e2e0143b2..560b104dbd 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,9 +39,15 @@ 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 + 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}} - //-p {{pot}} 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') @@ -74,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 @@ -92,23 +97,31 @@ 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 + li.customize-menu + menu.pets-menu(label='Quests') + div(ng-repeat='quest in Content.quests') + 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 + 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 62f54a5a7e..ddc25beeec 100644 --- a/views/options/social/group.jade +++ b/views/options/social/group.jade @@ -6,8 +6,77 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' .row-fluid .span4 + + // ------ Bosses ------- + .modal.inline-modal(bindonce='group', ng-if='group.type==="party" && group.quest.key') + .modal-header + 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') + table.table.table-striped + tr(ng-repeat='member in group.members') + td {{member.profile.name}} + 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 + + 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}} + 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 ------- - .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 @@ -31,11 +100,13 @@ 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 // ------ 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 91% rename from views/shared/modals/pets.jade rename to views/shared/modals/drops.jade index ccac0bd490..d9c25b5859 100644 --- a/views/shared/modals/pets.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/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/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 diff --git a/views/shared/modals/quests.jade b/views/shared/modals/quests.jade new file mode 100644 index 0000000000..bc32ff28c5 --- /dev/null +++ b/views/shared/modals/quests.jade @@ -0,0 +1,45 @@ +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 + .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. 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 && !party.quest.active && !questHold && party.quest.members[user._id] == undefined') + .modal-header + h3 Quest Invitation: {{Content.quests[party.quest.key].text}} + .modal-body + p {{Content.quests[party.quest.key].notes}} + 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 + button.btn.btn-default.btn-primary(ng-click='party.$questAccept()') Accept \ No newline at end of file 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