mirror of
https://github.com/sudoxnym/habitica-self-host.git
synced 2026-07-22 05:44:16 +00:00
Merge branch 'bosses' into develop
This commit is contained in:
commit
67ed5c3102
27 changed files with 616 additions and 145 deletions
|
|
@ -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",
|
||||
|
|
|
|||
1
migrations/missing_gems.js
Normal file
1
migrations/missing_gems.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
db.users.update({_id:''},{$set:{'purchased.ads':true},$inc:{balance:10}});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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%
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -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}}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -6,5 +6,6 @@ include ./new-stuff
|
|||
include ./buy-gems
|
||||
include ./members
|
||||
include ./settings
|
||||
include ./pets
|
||||
include ./classes
|
||||
include ./drops
|
||||
include ./classes
|
||||
include ./quests
|
||||
|
|
@ -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
|
||||
|
|
|
|||
45
views/shared/modals/quests.jade
Normal file
45
views/shared/modals/quests.jade
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue