Merge branch 'bosses' into develop

This commit is contained in:
Tyler Renelle 2013-12-25 08:27:35 -07:00
commit 67ed5c3102
27 changed files with 616 additions and 145 deletions

View file

@ -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",

View file

@ -0,0 +1 @@
db.users.update({_id:''},{$set:{'purchased.ads':true},$inc:{balance:10}});

View file

@ -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",

View file

@ -179,4 +179,13 @@ a
.modal-indented-list
margin-left: 10px;
padding-left: 10px;
padding-left: 10px;
.inline-modal
position: relative
top: auto;
left: auto
right: auto
margin: 0 auto 20px
z-index: 1
max-width: 100%

View file

@ -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();
}
}
])

View file

@ -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();
}
}
]);

View file

@ -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;
});

View file

@ -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;

View file

@ -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];
}
}
}])

View file

@ -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

View file

@ -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();
});
};

View file

@ -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);
})
}

View file

@ -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);
})
}

View file

@ -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);

View file

@ -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: [

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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')

View file

@ -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}}&nbsp;
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}}&nbsp;

View file

@ -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

View file

@ -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

View file

@ -6,5 +6,6 @@ include ./new-stuff
include ./buy-gems
include ./members
include ./settings
include ./pets
include ./classes
include ./drops
include ./classes
include ./quests

View file

@ -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, <a href='https://trello.com/c/VPPjVRlF/212-quests-bosses' target='_blank'>Quests & Bosses</a>. 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

View file

@ -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

View file

@ -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