Merge branch 'develop' into sabrecat/pet-201509

Conflicts:
	common/dist/sprites/habitrpg-shared.css
	common/dist/sprites/spritesmith2.css
	common/dist/sprites/spritesmith2.png
	common/dist/sprites/spritesmith3.css
	common/dist/sprites/spritesmith3.png
	common/dist/sprites/spritesmith4.css
	common/dist/sprites/spritesmith4.png
This commit is contained in:
Sabe Jones
2015-09-08 13:25:10 -05:00
219 changed files with 6888 additions and 3042 deletions
+4 -2
View File
@@ -110,8 +110,10 @@ html, body
//move
.new-stuff-modal // a .modal-body
h5
h2, h3
font-weight: 700
font-size: 14px
margin-top: 10px
.modal-fixed-height
overflow-y: auto
@@ -200,5 +202,5 @@ a.label
*
image-rendering: pixelated
.img-rendering-auto
.img-rendering-auto, .emoji
image-rendering: auto
+6
View File
@@ -45,6 +45,9 @@ a.hint:hover
ul
list-style-position: inside;
li.spaced
margin: auto auto .5em 3em
.group-leave-join
.glyphicon-ban-circle
color: white;
@@ -64,3 +67,6 @@ ul
.flex-column
display: flex
flex-direction: column
.row-margin
margin: auto auto 1em auto
+35 -4
View File
@@ -25,16 +25,47 @@ body
.marketing
text-align: center
.static-old-news
h2
font-weight: 700
font-size: 16px
margin-top: 30px
text-transform: uppercase
h3
font-weight: 700
font-size: 14px
margin-top: 20px
.pull-right
margin-left: 5px
#frontpage-play-button
//box-shadow: 0 0 40px #494141;
display:inline
display: inline
font-size: 20px
min-width:100px
height:50px
min-width: 100px
height: 50px
#about-page
img
box-shadow 0px 0px 10px 5px #888
margin 10px
max-width 500px
// Accordion tweaks for FAQ
a.h2.accordion
display: block
text-decoration: none
a.h2.accordion:before
font-family: 'Glyphicons Halflings'
color: grey
content: "\e114"
margin-right: 0.5em
a.h2.accordion.collapsed:before
font-family: 'Glyphicons Halflings'
color: grey
content: "\e080"
margin-right: 0.5em
@@ -0,0 +1,73 @@
'use strict';
habitrpg.controller('AutocompleteCtrl', ['$scope', '$timeout', 'Groups', 'User', 'InputCaret', function ($scope,$timeout,Groups,User,InputCaret) {
$scope.clearUserlist = function() {
$scope.response = [];
$scope.usernames = [];
}
$scope.filterUser = function(msg) {
if (!$scope.query || !msg.user) {
return false;
}
// Ignore casing when checking for username
var user = msg.user.toLowerCase();
var text = $scope.query.text.toLowerCase();
return user.indexOf(text) == 0;
}
$scope.performCompletion = function(msg) {
$scope.autoComplete(msg);
$scope.query = null;
}
$scope.addNewUser = function(user) {
if($.inArray(user.user,$scope.usernames) == -1) {
user.username = user.user;
$scope.usernames.push(user.user);
$scope.response.push(user);
}
}
$scope.clearUserlist();
$scope.chatChanged = function(newvalue,oldvalue){
if($scope.group && $scope.group.chat && $scope.group.chat.length > 0){
for(var i = 0; i < $scope.group.chat.length; i++) {
$scope.addNewUser($scope.group.chat[i]);
}
}
}
$scope.$watch('group.chat',$scope.chatChanged);
$scope.caretChanged = function(newCaretPos) {
var relativeelement = $('.chat-form div:first');
var textarea = $('.chat-form textarea');
var userlist = $('.list-at-user');
var offset = {
x: textarea.offset().left - relativeelement.offset().left,
y: textarea.offset().top - relativeelement.offset().top,
};
if(relativeelement) {
var caretOffset = InputCaret.getPosition(textarea);
userlist.css({
left: caretOffset.left + offset.x,
top: caretOffset.top + offset.y + 16
});
}
}
$scope.updateTimer = false;
$scope.$watch(function () { return $scope.caretPos; },function(newCaretPos) {
if($scope.updateTimer){
$timeout.cancel($scope.updateTimer)
}
$scope.updateTimer = $timeout(function(){
$scope.caretChanged(newCaretPos);
},$scope.watchDelay)
});
}]);
+139
View File
@@ -0,0 +1,139 @@
'use strict';
habitrpg.controller('ChatCtrl', ['$scope', 'Groups', 'Chat', 'User', '$http', 'ApiUrl', 'Notification', 'Members', '$rootScope', 'Analytics',
function($scope, Groups, Chat, User, $http, ApiUrl, Notification, Members, $rootScope, Analytics){
$scope.message = {content:''};
$scope._sending = false;
$scope.isUserMentioned = function(user, message) {
if(message.hasOwnProperty("highlight"))
return message.highlight;
message.highlight = false;
var messagetext = message.text.toLowerCase();
var username = user.profile.name;
var mentioned = messagetext.indexOf(username.toLowerCase());
var pattern = username+"([^\w]|$){1}";
if(mentioned > -1) {
var preceedingchar = messagetext.substring(mentioned-1,mentioned);
if(mentioned == 0 || preceedingchar.trim() == '' || preceedingchar == '@'){
var regex = new RegExp(pattern,'i');
message.highlight = regex.test(messagetext);
}
}
return message.highlight;
}
$scope.postChat = function(group, message){
if (_.isEmpty(message) || $scope._sending) return;
$scope._sending = true;
var previousMsg = (group.chat && group.chat[0]) ? group.chat[0].id : false;
Chat.utils.postChat({gid: group._id, message:message, previousMsg: previousMsg}, undefined, function(data){
if(data.chat){
group.chat = data.chat;
}else if(data.message){
group.chat.unshift(data.message);
}
$scope.message.content = '';
$scope._sending = false;
if (group.type == 'party') {
Analytics.updateUser({'partyID':group.id,'partySize':group.memberCount});
}
if (group.privacy == 'public'){
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'group chat','groupType':group.type,'privacy':group.privacy,'groupName':group.name,'message':message});
} else {
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'group chat','groupType':group.type,'privacy':group.privacy});
}
}, function(err){
$scope._sending = false;
});
}
$scope.deleteChatMessage = function(group, message){
if(message.uuid === User.user.id || (User.user.backer && User.user.contributor.admin)){
var previousMsg = (group.chat && group.chat[0]) ? group.chat[0].id : false;
if(confirm('Are you sure you want to delete this message?')){
Chat.utils.deleteChatMessage({gid:group._id, messageId:message.id, previousMsg:previousMsg}, undefined, function(data){
if(data.chat) group.chat = data.chat;
var i = _.findIndex(group.chat, {id: message.id});
if(i !== -1) group.chat.splice(i, 1);
});
}
}
}
$scope.likeChatMessage = function(group,message) {
if (message.uuid == User.user._id)
return Notification.text(window.env.t('foreverAlone'));
if (!message.likes) message.likes = {};
if (message.likes[User.user._id]) {
delete message.likes[User.user._id];
} else {
message.likes[User.user._id] = true;
}
Chat.utils.like({ gid:group._id, messageId:message.id }, undefined);
}
$scope.flagChatMessage = function(groupId,message) {
if(!message.flags) message.flags = {};
if(message.flags[User.user._id])
Notification.text(window.env.t('abuseAlreadyReported'));
else {
$scope.abuseObject = message;
$scope.groupId = groupId;
Members.selectMember(message.uuid, function(){
$rootScope.openModal('abuse-flag',{
controller:'MemberModalCtrl',
scope: $scope
});
});
}
};
$scope.copyToDo = function(message) {
var taskNotes = env.t("messageWroteIn", {
user: message.uuid == 'system'
? 'system'
: '[' + message.user + '](' + env.BASE_URL + '/static/front/#?memberId=' + message.uuid + ')',
group: '[' + $scope.group.name + '](' + window.location.href + ')'
});
var newScope = $scope.$new();
newScope.text = message.text;
newScope.notes = taskNotes;
$rootScope.openModal('copyChatToDo',{
controller:'CopyMessageModalCtrl',
scope: newScope
});
};
$scope.sync = function(group){
if(group.type == 'party') {
group.$syncParty(); // Syncs the whole party, not just 15 members
} else {
group.$get();
}
// When the user clicks fetch recent messages we need to update
// that the user has seen the new messages
Chat.seenMessage(group._id);
}
// List of Ordering options for the party members list
$scope.partyOrderChoices = {
'level': window.env.t('sortLevel'),
'random': window.env.t('sortRandom'),
'pets': window.env.t('sortPets'),
'habitrpg_date_joined' : window.env.t('sortHabitrpgJoined'),
'party_date_joined': window.env.t('sortJoined'),
'habitrpg_last_logged_in': window.env.t('sortHabitrpgLastLoggedIn'),
'name': window.env.t('sortName'),
'backgrounds': window.env.t('sortBackgrounds'),
};
$scope.partyOrderAscendingChoices = {
'ascending': window.env.t('ascendingSort'),
'descending': window.env.t('descendingSort')
}
}]);
@@ -0,0 +1,17 @@
'use strict';
habitrpg.controller("CopyMessageModalCtrl", ['$scope', 'User', 'Notification',
function($scope, User, Notification){
$scope.saveTodo = function() {
var newTask = {
text: $scope.text,
type: 'todo',
notes: $scope.notes
};
User.user.ops.addTask({body:newTask});
Notification.text(window.env.t('messageAddedAsToDo'));
$scope.$close();
}
}]);
+22 -485
View File
@@ -7,13 +7,13 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
if (!group.quest || !group.quest.members) return false;
if (group.quest.active) return false; // quest is started, not pending
return userid in group.quest.members && group.quest.members[userid] != false;
}
};
$scope.isMemberOfRunningQuest = function(userid, group) {
if (!group.quest || !group.quest.members) return false;
if (!group.quest.active) return false; // quest is pending, not started
return group.quest.members[userid];
}
};
$scope.isMemberOfGroup = function(userid, group){
@@ -32,11 +32,11 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
if (!group.members) return false;
var memberIds = _.map(group.members, function(x){return x._id});
return ~(memberIds.indexOf(userid));
}
};
$scope.isMember = function(user, group){
return ~(group.members.indexOf(user._id));
}
};
$scope.Members = Members;
$scope._editing = {group:false};
@@ -45,13 +45,13 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
if(group._newLeader && group._newLeader._id) group.leader = group._newLeader._id;
group.$save();
group._editing = false;
}
};
$scope.deleteAllMessages = function() {
if (confirm(window.env.t('confirmDeleteAllMessages'))) {
User.user.ops.clearPMs({});
}
}
};
// ------ Modals ------
@@ -69,7 +69,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$rootScope.openModal('member', {controller:'MemberModalCtrl', windowClass:'profile-modal', size:'lg'});
});
}
}
};
$scope.removeMember = function(group, member, isMember){
@@ -80,7 +80,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
isMember: isMember
};
$rootScope.openModal('remove-member', {scope: $scope});
}
};
$scope.confirmRemoveMember = function(confirm){
if(confirm){
@@ -100,31 +100,21 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
}else{
$scope.removeMemberData = undefined;
}
}
$scope.openInviteModal = function(group){
$rootScope.openModal('invite-friends', {controller:'InviteToGroupCtrl', resolve:
{injectedGroup: function(){
return group;
}}});
};
//var serializeQs = function(obj, prefix){
// var str = [];
// for(var p in obj) {
// if (obj.hasOwnProperty(p)) {
// var k = prefix ? prefix + "[" + p + "]" : p, v = obj[p];
// str.push(typeof v == "object" ?
// serializeQs(v, k) :
// encodeURIComponent(k) + "=" + encodeURIComponent(v));
// }
// }
// return str.join("&");
//}
//
//$scope.inviteLink = function(obj){
// return window.env.BASE_URL + '?' + serializeQs({partyInvite: obj});
//}
$scope.openInviteModal = function(group){
if (group.type !== 'party' && group.type !== 'guild') {
return console.log('Invalid group type.')
}
$rootScope.openModal('invite-' + group.type, {
controller:'InviteToGroupCtrl',
resolve: {
injectedGroup: function(){
return group;
}
}
});
};
$scope.quickReply = function(uid) {
Members.selectMember(uid, function(){
@@ -132,457 +122,4 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
});
}
}])
.controller('InviteToGroupCtrl', ['$scope', 'User', 'Groups', 'injectedGroup', '$http', 'Notification', function($scope, User, Groups, injectedGroup, $http, Notification){
$scope.group = injectedGroup;
$scope.inviter = User.user.profile.name;
$scope.emails = [{name:"",email:""},{name:"",email:""}];
$scope.inviteEmails = function(){
Groups.Group.invite({gid: $scope.group._id}, {inviter: $scope.inviter, emails: $scope.emails}, function(){
Notification.text("Invitation(s) sent!");
$scope.emails = [{name:'',email:''},{name:'',email:''}];
}, function(){
$scope.emails = [{name:'',email:''},{name:'',email:''}];
});
};
$scope.invite = function(){
Groups.Group.invite({gid: $scope.group._id}, {uuids: [$scope.invitee]}, function(){
Notification.text("Invitation(s) sent!");
$scope.invitee = '';
}, function(){
$scope.invitee = '';
});
};
}])
.controller('AutocompleteCtrl', ['$scope', '$timeout', 'Groups', 'User', 'InputCaret', function ($scope,$timeout,Groups,User,InputCaret) {
$scope.clearUserlist = function() {
$scope.response = [];
$scope.usernames = [];
}
$scope.filterUser = function(msg) {
if (!$scope.query || !msg.user) {
return false;
}
// Ignore casing when checking for username
var user = msg.user.toLowerCase();
var text = $scope.query.text.toLowerCase();
return user.indexOf(text) == 0;
}
$scope.performCompletion = function(msg) {
$scope.autoComplete(msg);
$scope.query = null;
}
$scope.addNewUser = function(user) {
if($.inArray(user.user,$scope.usernames) == -1) {
user.username = user.user;
$scope.usernames.push(user.user);
$scope.response.push(user);
}
}
$scope.clearUserlist();
$scope.chatChanged = function(newvalue,oldvalue){
if($scope.group && $scope.group.chat && $scope.group.chat.length > 0){
for(var i = 0; i < $scope.group.chat.length; i++) {
$scope.addNewUser($scope.group.chat[i]);
}
}
}
$scope.$watch('group.chat',$scope.chatChanged);
$scope.caretChanged = function(newCaretPos) {
var relativeelement = $('.chat-form div:first');
var textarea = $('.chat-form textarea');
var userlist = $('.list-at-user');
var offset = {
x: textarea.offset().left - relativeelement.offset().left,
y: textarea.offset().top - relativeelement.offset().top,
};
if(relativeelement) {
var caretOffset = InputCaret.getPosition(textarea);
userlist.css({
left: caretOffset.left + offset.x,
top: caretOffset.top + offset.y + 16
});
}
}
$scope.updateTimer = false;
$scope.$watch(function () { return $scope.caretPos; },function(newCaretPos) {
if($scope.updateTimer){
$timeout.cancel($scope.updateTimer)
}
$scope.updateTimer = $timeout(function(){
$scope.caretChanged(newCaretPos);
},$scope.watchDelay)
});
}])
.controller('ChatCtrl', ['$scope', 'Groups', 'Chat', 'User', '$http', 'ApiUrl', 'Notification', 'Members', '$rootScope', 'Analytics',
function($scope, Groups, Chat, User, $http, ApiUrl, Notification, Members, $rootScope, Analytics){
$scope.message = {content:''};
$scope._sending = false;
$scope.isUserMentioned = function(user, message) {
if(message.hasOwnProperty("highlight"))
return message.highlight;
message.highlight = false;
var messagetext = message.text.toLowerCase();
var username = user.profile.name;
var mentioned = messagetext.indexOf(username.toLowerCase());
var pattern = username+"([^\w]|$){1}";
if(mentioned > -1) {
var preceedingchar = messagetext.substring(mentioned-1,mentioned);
if(mentioned == 0 || preceedingchar.trim() == '' || preceedingchar == '@'){
var regex = new RegExp(pattern,'i');
message.highlight = regex.test(messagetext);
}
}
return message.highlight;
}
$scope.postChat = function(group, message){
if (_.isEmpty(message) || $scope._sending) return;
$scope._sending = true;
var previousMsg = (group.chat && group.chat[0]) ? group.chat[0].id : false;
Chat.utils.postChat({gid: group._id, message:message, previousMsg: previousMsg}, undefined, function(data){
if(data.chat){
group.chat = data.chat;
}else if(data.message){
group.chat.unshift(data.message);
}
$scope.message.content = '';
$scope._sending = false;
if (group.type == 'party') {
Analytics.updateUser({'partyID':group.id,'partySize':group.memberCount});
}
if (group.privacy == 'public'){
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'group chat','groupType':group.type,'privacy':group.privacy,'groupName':group.name,'message':message});
} else {
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'group chat','groupType':group.type,'privacy':group.privacy});
}
}, function(err){
$scope._sending = false;
});
}
$scope.deleteChatMessage = function(group, message){
if(message.uuid === User.user.id || (User.user.backer && User.user.contributor.admin)){
var previousMsg = (group.chat && group.chat[0]) ? group.chat[0].id : false;
if(confirm('Are you sure you want to delete this message?')){
Chat.utils.deleteChatMessage({gid:group._id, messageId:message.id, previousMsg:previousMsg}, undefined, function(data){
if(data.chat) group.chat = data.chat;
var i = _.findIndex(group.chat, {id: message.id});
if(i !== -1) group.chat.splice(i, 1);
});
}
}
}
$scope.likeChatMessage = function(group,message) {
if (message.uuid == User.user._id)
return Notification.text(window.env.t('foreverAlone'));
if (!message.likes) message.likes = {};
if (message.likes[User.user._id]) {
delete message.likes[User.user._id];
} else {
message.likes[User.user._id] = true;
}
Chat.utils.like({ gid:group._id, messageId:message.id }, undefined);
}
$scope.flagChatMessage = function(groupId,message) {
if(!message.flags) message.flags = {};
if(message.flags[User.user._id])
Notification.text(window.env.t('abuseAlreadyReported'));
else {
$scope.abuseObject = message;
$scope.groupId = groupId;
Members.selectMember(message.uuid, function(){
$rootScope.openModal('abuse-flag',{
controller:'MemberModalCtrl',
scope: $scope
});
});
}
};
$scope.copyToDo = function(message) {
var taskNotes = env.t("messageWroteIn", {
user: message.uuid == 'system'
? 'system'
: '[' + message.user + '](' + env.BASE_URL + '/static/front/#?memberId=' + message.uuid + ')',
group: '[' + $scope.group.name + '](' + window.location.href + ')'
});
var newScope = $scope.$new();
newScope.text = message.text;
newScope.notes = taskNotes;
$rootScope.openModal('copyChatToDo',{
controller:'CopyMessageModalCtrl',
scope: newScope
});
};
$scope.sync = function(group){
if(group.type == 'party') {
group.$syncParty(); // Syncs the whole party, not just 15 members
} else {
group.$get();
}
// When the user clicks fetch recent messages we need to update
// that the user has seen the new messages
Chat.seenMessage(group._id);
}
// List of Ordering options for the party members list
$scope.partyOrderChoices = {
'level': window.env.t('sortLevel'),
'random': window.env.t('sortRandom'),
'pets': window.env.t('sortPets'),
'habitrpg_date_joined' : window.env.t('sortHabitrpgJoined'),
'party_date_joined': window.env.t('sortJoined'),
'habitrpg_last_logged_in': window.env.t('sortHabitrpgLastLoggedIn'),
'name': window.env.t('sortName'),
'backgrounds': window.env.t('sortBackgrounds'),
};
$scope.partyOrderAscendingChoices = {
'ascending': window.env.t('ascendingSort'),
'descending': window.env.t('descendingSort')
}
}])
.controller("GuildsCtrl", ['$scope', 'Groups', 'User', 'Challenges', '$rootScope', '$state', '$location', '$compile', 'Analytics',
function($scope, Groups, User, Challenges, $rootScope, $state, $location, $compile, Analytics) {
$scope.groups = {
guilds: Groups.myGuilds(),
"public": Groups.publicGuilds()
}
$scope.type = 'guild';
$scope.text = window.env.t('guild');
var newGroup = function(){
return new Groups.Group({type:'guild', privacy:'private'});
}
$scope.newGroup = newGroup()
$scope.create = function(group){
if (User.user.balance < 1)
return $rootScope.openModal('buyGems', {track:"Gems > Create Group"});
if (confirm(window.env.t('confirmGuild'))) {
group.$save(function(saved){
if (saved.privacy == 'public') {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':true,'groupType':'guild','privacy':saved.privacy,'groupName':saved.name})}
else {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':true,'groupType':'guild','privacy':saved.privacy})}
$rootScope.hardRedirect('/#/options/groups/guilds/' + saved._id);
});
}
}
$scope.join = function(group){
// If we're accepting an invitation, we don't have the actual group object, but a faux group object (for performance
// purposes) {id, name}. Let's trick ngResource into thinking we have a group, so we can call the same $join
// function (server calls .attachGroup(), which finds group by _id and handles this properly)
if (group.id && !group._id) {
group = new Groups.Group({_id:group.id});
}
group.$join(function(joined){
if (joined.privacy == 'public') {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':false,'groupType':'guild','privacy':joined.privacy,'groupName':joined.name})}
else {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':false,'groupType':'guild','privacy':joined.privacy})}
$rootScope.hardRedirect('/#/options/groups/guilds/' + joined._id);
})
}
$scope.leave = function(keep) {
if (keep == 'cancel') {
$scope.selectedGroup = undefined;
$scope.popoverEl.popover('destroy');
} else {
Groups.Group.leave({gid: $scope.selectedGroup._id, keep:keep}, undefined, function(){
$rootScope.hardRedirect('/#/options/groups/guilds');
});
}
}
$scope.clickLeave = function(group, $event){
$scope.selectedGroup = group;
$scope.popoverEl = $($event.target);
var html, title;
Challenges.Challenge.query(function(challenges) {
challenges = _.pluck(_.filter(challenges, function(c) {
return c.group._id == group._id;
}), '_id');
if (_.intersection(challenges, User.user.challenges).length > 0) {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'remove-all\')">' + window.env.t('removeTasks') + '</a><br/>\n<a ng-click="leave(\'keep-all\')">' + window.env.t('keepTasks') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leaveGroupCha');
} else {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'keep-all\')">' + window.env.t('confirm') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leaveGroup')
}
$scope.popoverEl.popover('destroy').popover({
html: true,
placement: 'top',
trigger: 'manual',
title: title,
content: html
}).popover('show');
});
}
$scope.reject = function(guild){
var i = _.findIndex(User.user.invitations.guilds, {id:guild.id});
if (~i){
User.user.invitations.guilds.splice(i, 1);
User.set({'invitations.guilds':User.user.invitations.guilds});
}
}
}
])
.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','Challenges','$state','$compile','Analytics','Quests',
function($rootScope,$scope,Groups,Chat,User,Challenges,$state,$compile,Analytics,Quests) {
$scope.type = 'party';
$scope.text = window.env.t('party');
$scope.group = $rootScope.party = Groups.party();
$scope.newGroup = new Groups.Group({type:'party'});
$scope.inviteOrStartParty = Groups.inviteOrStartParty;
$scope.questInit = Quests.questInit;
if ($state.is('options.social.party')) {
$scope.group.$syncParty(); // Sync party automatically when navigating to party page
}
Chat.seenMessage($scope.group._id);
$scope.create = function(group){
group.$save(function(){
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':true,'groupType':'party','privacy':'private'});
Analytics.updateUser({'partyID':group.id,'partySize':1});
$rootScope.hardRedirect('/#/options/groups/party');
});
};
$scope.join = function(party){
var group = new Groups.Group({_id: party.id, name: party.name});
group.$join(function(){
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':false,'groupType':'party','privacy':'private'});
Analytics.updateUser({'partyID':party.id});
$rootScope.hardRedirect('/#/options/groups/party');
});
};
// TODO: refactor guild and party leave into one function
$scope.leave = function(keep) {
if (keep == 'cancel') {
$scope.selectedGroup = undefined;
$scope.popoverEl.popover('destroy');
} else {
Groups.Group.leave({gid: $scope.selectedGroup._id, keep:keep}, undefined, function(){
Analytics.updateUser({'partySize':null,'partyID':null});
$rootScope.hardRedirect('/#/options/groups/party');
});
}
};
// TODO: refactor guild and party clickLeave into one function
$scope.clickLeave = function(group, $event){
$scope.selectedGroup = group;
$scope.popoverEl = $($event.target);
var html, title;
Challenges.Challenge.query(function(challenges) {
challenges = _.pluck(_.filter(challenges, function(c) {
return c.group._id == group._id;
}), '_id');
if (_.intersection(challenges, User.user.challenges).length > 0) {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'remove-all\')">' + window.env.t('removeTasks') + '</a><br/>\n<a ng-click="leave(\'keep-all\')">' + window.env.t('keepTasks') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leavePartyCha');
} else {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'keep-all\')">' + window.env.t('confirm') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leaveParty');
}
$scope.popoverEl.popover('destroy').popover({
html: true,
placement: 'top',
trigger: 'manual',
title: title,
content: html
}).popover('show');
});
}
$scope.reject = function(){
//User.user.invitations.party = undefined;
User.set({'invitations.party':{}});
}
$scope.questCancel = function(party){
if (!confirm(window.env.t('sureCancel'))) return;
Groups.questCancel(party);
}
$scope.questAbort = function(party){
if (!confirm(window.env.t('sureAbort'))) return;
if (!confirm(window.env.t('doubleSureAbort'))) return;
Groups.questAbort(party);
}
$scope.questAccept = function(party){
Groups.questAccept(party);
};
$scope.questReject = function(party){
Groups.questReject(party);
}
}
])
.controller("TavernCtrl", ['$scope', 'Groups', 'User',
function($scope, Groups, User) {
$scope.group = Groups.tavern();
$scope.toggleUserTier = function($event) {
$($event.target).next().toggle();
}
}
])
.controller("CopyMessageModalCtrl", ['$scope', 'User', 'Notification',
function($scope, User, Notification){
$scope.saveTodo = function() {
var newTask = {
text: $scope.text,
type: 'todo',
notes: $scope.notes
};
User.user.ops.addTask({body:newTask});
Notification.text(window.env.t('messageAddedAsToDo'));
$scope.$close();
}
}
]);
}]);
@@ -0,0 +1,92 @@
'use strict';
habitrpg.controller("GuildsCtrl", ['$scope', 'Groups', 'User', 'Challenges', '$rootScope', '$state', '$location', '$compile', 'Analytics',
function($scope, Groups, User, Challenges, $rootScope, $state, $location, $compile, Analytics) {
$scope.groups = {
guilds: Groups.myGuilds(),
"public": Groups.publicGuilds()
}
$scope.type = 'guild';
$scope.text = window.env.t('guild');
var newGroup = function(){
return new Groups.Group({type:'guild', privacy:'private'});
}
$scope.newGroup = newGroup()
$scope.create = function(group){
if (User.user.balance < 1)
return $rootScope.openModal('buyGems', {track:"Gems > Create Group"});
if (confirm(window.env.t('confirmGuild'))) {
group.$save(function(saved){
if (saved.privacy == 'public') {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':true,'groupType':'guild','privacy':saved.privacy,'groupName':saved.name})}
else {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':true,'groupType':'guild','privacy':saved.privacy})}
$rootScope.hardRedirect('/#/options/groups/guilds/' + saved._id);
});
}
}
$scope.join = function(group){
// If we're accepting an invitation, we don't have the actual group object, but a faux group object (for performance
// purposes) {id, name}. Let's trick ngResource into thinking we have a group, so we can call the same $join
// function (server calls .attachGroup(), which finds group by _id and handles this properly)
if (group.id && !group._id) {
group = new Groups.Group({_id:group.id});
}
group.$join(function(joined){
if (joined.privacy == 'public') {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':false,'groupType':'guild','privacy':joined.privacy,'groupName':joined.name})}
else {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':false,'groupType':'guild','privacy':joined.privacy})}
$rootScope.hardRedirect('/#/options/groups/guilds/' + joined._id);
})
}
$scope.leave = function(keep) {
if (keep == 'cancel') {
$scope.selectedGroup = undefined;
$scope.popoverEl.popover('destroy');
} else {
Groups.Group.leave({gid: $scope.selectedGroup._id, keep:keep}, undefined, function(){
$rootScope.hardRedirect('/#/options/groups/guilds');
});
}
}
$scope.clickLeave = function(group, $event){
$scope.selectedGroup = group;
$scope.popoverEl = $($event.target);
var html, title;
Challenges.Challenge.query(function(challenges) {
challenges = _.pluck(_.filter(challenges, function(c) {
return c.group._id == group._id;
}), '_id');
if (_.intersection(challenges, User.user.challenges).length > 0) {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'remove-all\')">' + window.env.t('removeTasks') + '</a><br/>\n<a ng-click="leave(\'keep-all\')">' + window.env.t('keepTasks') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leaveGroupCha');
} else {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'keep-all\')">' + window.env.t('confirm') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leaveGroup')
}
$scope.popoverEl.popover('destroy').popover({
html: true,
placement: 'top',
trigger: 'manual',
title: title,
content: html
}).popover('show');
});
}
$scope.reject = function(guild){
var i = _.findIndex(User.user.invitations.guilds, {id:guild.id});
if (~i){
User.user.invitations.guilds.splice(i, 1);
User.set({'invitations.guilds':User.user.invitations.guilds});
}
}
}
]);
@@ -0,0 +1,72 @@
'use strict';
habitrpg.controller('InviteToGroupCtrl', ['$scope', 'User', 'Groups', 'injectedGroup', '$http', 'Notification', function($scope, User, Groups, injectedGroup, $http, Notification) {
$scope.group = injectedGroup;
$scope.inviter = User.user.profile.name;
_resetInvitees();
$scope.addUuid = function() {
$scope.invitees.push({uuid: ''});
};
$scope.addEmail = function() {
$scope.emails.push({name: '', email: ''});
};
$scope.inviteNewUsers = function(inviteMethod) {
if (!$scope.group._id) {
$scope.group.name = $scope.group.name || env.t('possessiveParty', {name: User.user.profile.name});
return $scope.group.$save()
.then(function(res) {
_inviteByMethod(inviteMethod);
});
}
_inviteByMethod(inviteMethod);
};
function _inviteByMethod(inviteMethod) {
var invitationDetails;
if (inviteMethod === 'email') {
var emails = _getEmails();
invitationDetails = {inviter: $scope.inviter, emails: emails};
} else if (inviteMethod === 'uuid') {
var uuids = _getOnlyUuids();
invitationDetails = {uuids: uuids};
} else {
return console.log('Invalid invite method.')
}
Groups.Group.invite({gid: $scope.group._id}, invitationDetails, function(){
Notification.text(window.env.t('invitationsSent'));
_resetInvitees();
}, function(){
_resetInvitees();
});
}
function _getOnlyUuids() {
var uuids = _.pluck($scope.invitees, 'uuid');
var filteredUuids = _.filter(uuids, function(id) {
return id != '';
});
return filteredUuids;
}
function _getEmails() {
var emails = _.filter($scope.emails, function(obj) {
return obj.email != '';
});
return emails;
}
function _resetInvitees() {
var emptyEmails = [{name:"",email:""},{name:"",email:""}];
var emptyInvitees = [{uuid: ''}];
$scope.emails = emptyEmails;
$scope.invitees = emptyInvitees;
}
}]);
@@ -8,6 +8,8 @@ habitrpg.controller('NotificationCtrl',
if (after <= 0){
$rootScope.playSound('Death');
$rootScope.openModal('death', {keyboard:false, backdrop:'static'});
} else if (after <= 30 && !User.user.flags.warnedLowHealth) {
$rootScope.openModal('lowHealth', {keyboard:false, backdrop:'static', controller:'UserCtrl', track:'Health Warning'});
}
if (after == before) return;
if (User.user.stats.lvl == 0) return;
+134
View File
@@ -0,0 +1,134 @@
'use strict';
habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','Challenges','$state','$compile','Analytics','Quests',
function($rootScope,$scope,Groups,Chat,User,Challenges,$state,$compile,Analytics,Quests) {
$scope.type = 'party';
$scope.text = window.env.t('party');
$scope.group = $rootScope.party = Groups.party();
$scope.newGroup = new Groups.Group({type:'party'});
$scope.inviteOrStartParty = Groups.inviteOrStartParty;
$scope.questInit = Quests.questInit;
if ($state.is('options.social.party')) {
$scope.group.$syncParty(); // Sync party automatically when navigating to party page
}
Chat.seenMessage($scope.group._id);
$scope.create = function(group){
if (!group.name) group.name = env.t('possessiveParty', {name: User.user.profile.name});
group.$save(function(){
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':true,'groupType':'party','privacy':'private'});
Analytics.updateUser({'partyID':group.id,'partySize':1});
$rootScope.hardRedirect('/#/options/groups/party');
});
};
$scope.join = function(party){
var group = new Groups.Group({_id: party.id, name: party.name});
group.$join(function(){
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':false,'groupType':'party','privacy':'private'});
Analytics.updateUser({'partyID':party.id});
$rootScope.hardRedirect('/#/options/groups/party');
});
};
// TODO: refactor guild and party leave into one function
$scope.leave = function(keep) {
if (keep == 'cancel') {
$scope.selectedGroup = undefined;
$scope.popoverEl.popover('destroy');
} else {
Groups.Group.leave({gid: $scope.selectedGroup._id, keep:keep}, undefined, function(){
Analytics.updateUser({'partySize':null,'partyID':null});
$rootScope.hardRedirect('/#/options/groups/party');
});
}
};
// TODO: refactor guild and party clickLeave into one function
$scope.clickLeave = function(group, $event){
Analytics.track({'hitType':'event','eventCategory':'button','eventAction':'click','eventLabel':'Leave Party'});
$scope.selectedGroup = group;
$scope.popoverEl = $($event.target);
var html, title;
Challenges.Challenge.query(function(challenges) {
challenges = _.pluck(_.filter(challenges, function(c) {
return c.group._id == group._id;
}), '_id');
if (_.intersection(challenges, User.user.challenges).length > 0) {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'remove-all\')">' + window.env.t('removeTasks') + '</a><br/>\n<a ng-click="leave(\'keep-all\')">' + window.env.t('keepTasks') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leavePartyCha');
} else {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'keep-all\')">' + window.env.t('confirm') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leaveParty');
}
$scope.popoverEl.popover('destroy').popover({
html: true,
placement: 'top',
trigger: 'manual',
title: title,
content: html
}).popover('show');
});
};
$scope.clickStartQuest = function(){
Analytics.track({'hitType':'event','eventCategory':'button','eventAction':'click','eventLabel':'Start a Quest'});
var hasQuests = _.find(User.user.items.quests, function(quest) {
return quest > 0;
});
if (hasQuests){
$rootScope.openModal("ownedQuests", { controller:"InventoryCtrl" });
} else {
$rootScope.$state.go('options.inventory.quests');
}
};
$scope.leaveOldPartyAndJoinNewParty = function(newPartyId, newPartyName) {
if (confirm('Are you sure you want to delete your party and join ' + newPartyName + '?')) {
Groups.Group.leave({gid: Groups.party()._id, keep:false}, undefined, function() {
$scope.group = {
loadingNewParty: true
};
$scope.join({ id: newPartyId, name: newPartyName });
});
}
}
$scope.reject = function(){
User.set({'invitations.party':{}});
}
$scope.questCancel = function(party){
if (!confirm(window.env.t('sureCancel'))) return;
Groups.questCancel(party);
}
$scope.questAbort = function(party){
if (!confirm(window.env.t('sureAbort'))) return;
if (!confirm(window.env.t('doubleSureAbort'))) return;
Groups.questAbort(party);
}
$scope.questLeave = function(party){
if (!confirm(window.env.t('sureLeave'))) return;
delete $scope.group.quest.members[User.user._id];
Groups.questLeave(party);
}
$scope.questAccept = function(party){
Groups.questAccept(party);
};
$scope.questReject = function(party){
Groups.questReject(party);
}
}
]);
+1 -1
View File
@@ -124,7 +124,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
// Otherwise use the proper $modal.open
$rootScope.openModal = function(template, options){//controller, scope, keyboard, backdrop){
if (!options) options = {};
if (options.track) Analytics.track(_.merge(options.track,{'hitType':'event','eventCategory':'button','eventAction':'click'}));
if (options.track) Analytics.track({'hitType':'event','eventCategory':'button','eventAction':'click','eventLabel':options.track});
if(template === 'newStuff') return forceLoadBailey(template, options);
return $modal.open({
templateUrl: 'modals/' + template + '.html',
+28 -12
View File
@@ -58,22 +58,25 @@ habitrpg.controller('SettingsCtrl',
Guide.goto('intro', 0, true);
}
$scope.showClassesTour = function(){
Guide.goto('classes', 0, true);
}
$scope.showBailey = function(){
User.set({'flags.newStuff':true});
}
$scope.saveDayStart = function(){
var dayStart = +User.user.preferences.dayStart;
if (_.isNaN(dayStart) || dayStart < 0 || dayStart > 24) {
dayStart = 0;
return alert(window.env.t('enterNumber'));
}
User.set({'preferences.dayStart': dayStart});
}
$scope.dayStart = User.user.preferences.dayStart;
$scope.openDayStartModal = function(dayStart) {
$scope.dayStart = +dayStart;
$scope.nextCron = _calculateNextCron();
$rootScope.openModal('change-day-start', { scope: $scope });
};
$scope.saveDayStart = function() {
User.set({
'preferences.dayStart': Math.floor($scope.dayStart),
'lastCron': +new Date
});
};
$scope.language = window.env.language;
$scope.avalaibleLanguages = window.env.avalaibleLanguages;
@@ -196,5 +199,18 @@ habitrpg.controller('SettingsCtrl',
subs["google_6mo"].discount = false;
});
}
function _calculateNextCron() {
$scope.dayStart;
var nextCron = moment().hours($scope.dayStart).minutes(0).seconds(0).milliseconds(0);
var currentHour = moment().format('H');
if (currentHour >= $scope.dayStart) {
nextCron = nextCron.add(1, 'day');;
}
return +nextCron.format('x');
}
}
]);
@@ -0,0 +1,10 @@
'use strict';
habitrpg.controller("TavernCtrl", ['$scope', 'Groups', 'User',
function($scope, Groups, User) {
$scope.group = Groups.tavern();
$scope.toggleUserTier = function($event) {
$($event.target).next().toggle();
}
}
]);
@@ -43,6 +43,10 @@ habitrpg.controller("UserCtrl", ['$rootScope', '$scope', '$location', 'User', '$
$scope._editing.profile = false;
}
$scope.acknowledgeHealthWarning = function(){
User.user.ops.update && User.set({'flags.warnedLowHealth':true});
}
/**
* For gem-unlockable preferences, (a) if owned, select preference (b) else, purchase
* @param path: User.preferences <-> User.purchased maps like User.preferences.skin=abc <-> User.purchased.skin.abc.
+103 -74
View File
@@ -1,110 +1,139 @@
'use strict';
/**
* Services that persists and retrieves user from localStorage.
*/
(function() {
angular
.module('habitrpg')
.factory('Groups', groupsFactory);
angular.module('habitrpg').factory('Groups',
['$rootScope','ApiUrl', '$resource', '$q', '$http', 'User', 'Challenges', 'Analytics', '$location',
function($rootScope, ApiUrl, $resource, $q, $http, User, Challenges, Analytics, $location) {
var Group = $resource(ApiUrl.get() + '/api/v2/groups/:gid',
{gid:'@_id', messageId: '@_messageId'},
{
get: {
method: "GET",
isArray:false,
// Wrap challenges as ngResource so they have functions like $leave or $join
transformResponse: function(data, headers) {
data = angular.fromJson(data);
_.each(data && data.challenges, function(c) {
angular.extend(c, Challenges.Challenge.prototype);
});
return data;
}
},
groupsFactory.$inject = [
'$location',
'$resource',
'$rootScope',
'Analytics',
'ApiUrl',
'Challenges',
'User'
];
syncParty: {method: "GET", url: '/api/v2/groups/party'},
join: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/join'},
leave: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/leave'},
invite: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/invite'},
removeMember: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/removeMember'},
questAccept: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questAccept'},
questReject: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questReject'},
questCancel: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questCancel'},
questAbort: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questAbort'}
});
function groupsFactory($location, $resource, $rootScope, Analytics, ApiUrl, Challenges, User) {
// Defer loading everything until they're requested
var data = {party: undefined, myGuilds: undefined, publicGuilds: undefined, tavern: undefined};
var data = {party: undefined, myGuilds: undefined, publicGuilds: undefined, tavern: undefined};
var Group = $resource(ApiUrl.get() + '/api/v2/groups/:gid',
{gid:'@_id', messageId: '@_messageId'},
{
get: {
method: "GET",
isArray:false,
// Wrap challenges as ngResource so they have functions like $leave or $join
transformResponse: function(data) {
data = angular.fromJson(data);
_.each(data && data.challenges, function(c) {
angular.extend(c, Challenges.Challenge.prototype);
});
return data;
}
},
var syncUser = function(res) {
User.sync();
}
var logError = function(err) {
console.log(err);
}
syncParty: {method: "GET", url: '/api/v2/groups/party'},
join: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/join'},
leave: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/leave'},
invite: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/invite'},
removeMember: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/removeMember'},
questAccept: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questAccept'},
questReject: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questReject'},
questCancel: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questCancel'},
questAbort: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questAbort'},
questLeave: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questLeave'}
});
return {
party: function(cb){
function _syncUser() {
User.sync();
}
function _logError(err) {
console.log(err);
}
function party(cb) {
if (!data.party) return (data.party = Group.get({gid: 'party'}, cb));
return (cb) ? cb(party) : data.party;
},
publicGuilds: function(){
}
function publicGuilds() {
//TODO combine these as {type:'guilds,public'} and create a $filter() to separate them
if (!data.publicGuilds) data.publicGuilds = Group.query({type:'public'});
return data.publicGuilds;
},
myGuilds: function(){
}
function myGuilds() {
if (!data.myGuilds) data.myGuilds = Group.query({type:'guilds'});
return data.myGuilds;
},
tavern: function(){
}
function tavern() {
if (!data.tavern) data.tavern = Group.get({gid:'habitrpg'});
return data.tavern;
},
}
questAccept: function(party){
function questAccept(party) {
Analytics.updateUser({'partyID':party.id,'partySize':party.memberCount});
party.$questAccept()
.then(syncUser, logError);
},
return party.$questAccept()
.then(_syncUser, _logError);
}
questReject: function(party){
function questReject(party) {
Analytics.updateUser({'partyID':party.id,'partySize':party.memberCount});
party.$questReject()
.then(syncUser, logError);
},
return party.$questReject()
.then(_syncUser, _logError);
}
questCancel: function(party){
function questCancel(party) {
Analytics.updateUser({'partyID':party.id,'partySize':party.memberCount});
party.$questCancel()
.then(syncUser, logError);
},
return party.$questCancel()
.then(_syncUser, _logError);
}
questAbort: function(party){
function questAbort(party) {
Analytics.updateUser({'partyID':party.id,'partySize':party.memberCount});
party.$questAbort()
.then(syncUser, logError);
},
return party.$questAbort()
.then(_syncUser, _logError);
}
inviteOrStartParty: function(group) {
if (group.type === "party") {
$rootScope.openModal('invite-friends', {
function questLeave(party) {
Analytics.updateUser({'partyID':party.id,'partySize':party.memberCount});
return party.$questLeave()
.then(_syncUser, _logError);
}
function inviteOrStartParty(group) {
Analytics.track({'hitType':'event','eventCategory':'button','eventAction':'click','eventLabel':'Invite Friends'});
if (group.type === "party" || $location.$$path === "/options/groups/party") {
group.type = 'party';
$rootScope.openModal('invite-party', {
controller:'InviteToGroupCtrl',
resolve: {
injectedGroup: function(){ return group; }
}
});
} else {
Analytics.track({'hitType':'event','eventCategory':'button','eventAction':'click','eventLabel':'Invite Friends'});
$location.path("/options/groups/party");
}
},
}
// Pass reference to party, myGuilds, publicGuilds, tavern; inside data in order to
// be able to modify them directly (otherwise will be stick with cached version)
data: data,
return {
party: party,
publicGuilds: publicGuilds,
myGuilds: myGuilds,
tavern: tavern,
questAccept: questAccept,
questReject: questReject,
questAbort: questAbort,
questLeave: questLeave,
questCancel: questCancel,
inviteOrStartParty: inviteOrStartParty,
Group: Group
data: data,
Group: Group
}
}
}])
})();
+17
View File
@@ -40,3 +40,20 @@ window.habitrpg = angular.module('habitrpg', ['chieffancypants.loadingBar', 'ui.
});
});
}])
.controller('AccordionCtrl', function() {
function openHashAccordion() {
if (window.location.hash) {
var $target = $(window.location.hash.replace('/',''));
if ($target.hasClass('collapse')) {
$target.collapse('show');
$('html, body').animate({
scrollTop: $($target).offset().top - 100
});
}
}
}
$(document).ready(function(){
openHashAccordion();
});
})
+16 -9
View File
@@ -71,20 +71,27 @@
"js/directives/when-scrolled.directive.js",
"js/controllers/authCtrl.js",
"js/controllers/menuCtrl.js",
"js/controllers/autoCompleteCtrl.js",
"js/controllers/challengesCtrl.js",
"js/controllers/chatCtrl.js",
"js/controllers/copyMessageModalCtrl.js",
"js/controllers/filtersCtrl.js",
"js/controllers/footerCtrl.js",
"js/controllers/groupsCtrl.js",
"js/controllers/guildsCtrl.js",
"js/controllers/hallCtrl.js",
"js/controllers/headerCtrl.js",
"js/controllers/inventoryCtrl.js",
"js/controllers/inviteToGroupCtrl.js",
"js/controllers/memberModalCtrl.js",
"js/controllers/menuCtrl.js",
"js/controllers/notificationCtrl.js",
"js/controllers/partyCtrl.js",
"js/controllers/rootCtrl.js",
"js/controllers/settingsCtrl.js",
"js/controllers/headerCtrl.js",
"js/controllers/tavernCtrl.js",
"js/controllers/tasksCtrl.js",
"js/controllers/filtersCtrl.js",
"js/controllers/userCtrl.js",
"js/controllers/groupsCtrl.js",
"js/controllers/inventoryCtrl.js",
"js/controllers/footerCtrl.js",
"js/controllers/challengesCtrl.js",
"js/controllers/hallCtrl.js"
"js/controllers/userCtrl.js"
],
"css": [
"bower_components/bootstrap/dist/css/bootstrap.css",
+25 -3
View File
@@ -6,6 +6,7 @@ var async = require('async');
var utils = require('../utils');
var nconf = require('nconf');
var request = require('request');
var FirebaseTokenGenerator = require('firebase-token-generator');
var User = require('../models/user').model;
var EmailUnsubscription = require('../models/emailUnsubscription').model;
var analytics = utils.analytics;
@@ -33,7 +34,7 @@ api.auth = function(req, res, next) {
var uid = req.headers['x-api-user'];
var token = req.headers['x-api-key'];
if (!(uid && token)) return res.json(401, NO_TOKEN_OR_UID);
User.findOne({_id: uid,apiToken: token}, function(err, user) {
User.findOne({_id: uid, apiToken: token}, function(err, user) {
if (err) return next(err);
if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND);
if (user.auth.blocked) return res.json(401, accountSuspended(user._id));
@@ -319,11 +320,32 @@ api.changePassword = function(req, res, next) {
if (err) next(err);
res.send(200);
})
}
};
var firebaseTokenGeneratorInstance = new FirebaseTokenGenerator(nconf.get('FIREBASE:SECRET'));
api.getFirebaseToken = function(req, res, next) {
var user = res.locals.user;
// Expires 24 hours after now (60*60*24*1000) (in milliseconds)
var expires = new Date();
expires.setTime(expires.getTime() + 86400000);
var token = firebaseTokenGeneratorInstance
.createToken({
uid: user._id,
isHabiticaUser: true
}, {
expires: expires
});
res.json(200, {
token: token,
expires: expires
});
};
/*
Registers a new user. Only accepting username/password registrations, no Facebook
*/
*/
api.setupPassport = function(router) {
+94 -92
View File
@@ -8,6 +8,7 @@ function clone(a) {
var _ = require('lodash');
var nconf = require('nconf');
var async = require('async');
var Q = require('q');
var utils = require('./../utils');
var shared = require('../../../common');
var User = require('./../models/user').model;
@@ -18,6 +19,7 @@ var isProd = nconf.get('NODE_ENV') === 'production';
var api = module.exports;
var pushNotify = require('./pushNotifications');
var analytics = utils.analytics;
var firebase = require('../libs/firebase');
/*
------------------------------------------------------------------------
@@ -190,7 +192,11 @@ api.create = function(req, res, next) {
async.waterfall([
function(cb){user.save(cb)},
function(saved,ct,cb){group.save(cb)},
function(saved,ct,cb){saved.populate('members',nameFields,cb)}
function(saved,ct,cb){
firebase.updateGroupData(saved);
firebase.addUserToGroup(saved._id, user._id);
saved.populate('members', nameFields, cb);
}
],function(err,saved){
if (err) return next(err);
res.json(saved);
@@ -207,6 +213,8 @@ api.create = function(req, res, next) {
group.save(cb);
},
function(saved, count, cb){
firebase.updateGroupData(saved);
firebase.addUserToGroup(saved._id, user._id);
saved.populate('members', nameFields, cb);
}
], function(err, populated){
@@ -231,10 +239,13 @@ api.update = function(req, res, next) {
group.save(function(err, saved){
if (err) return next(err);
firebase.updateGroupData(saved);
res.send(204);
});
}
// TODO remove from api object?
api.attachGroup = function(req, res, next) {
var gid = req.params.gid;
var q = (gid == 'party') ? Group.findOne({type: 'party', members: {'$in': [res.locals.user._id]}}) : Group.findById(gid);
@@ -243,7 +254,7 @@ api.attachGroup = function(req, res, next) {
if(!group) return res.json(404, {err: "Group not found"});
res.locals.group = group;
next();
})
});
}
api.getChat = function(req, res, next) {
@@ -462,7 +473,12 @@ api.join = function(req, res, next) {
if(!isUserInvited) return res.json(401, {err: "Can't join a group you're not invited to."});
if (!_.contains(group.members, user._id)){
if (group.members.length === 0) {
group.leader = user._id;
}
group.members.push(user._id);
if (group.invites.length > 0) {
group.invites.splice(_.indexOf(group.invites, user._id), 1);
}
@@ -473,6 +489,8 @@ api.join = function(req, res, next) {
group.save(cb);
},
function(cb){
firebase.addUserToGroup(group._id, user._id);
// TODO why query group once again?
populateQuery(group.type, Group.findById(group._id)).exec(cb);
}
], function(err, results){
@@ -484,81 +502,22 @@ api.join = function(req, res, next) {
}
api.leave = function(req, res, next) {
var user = res.locals.user,
group = res.locals.group;
var user = res.locals.user;
var group = res.locals.group;
if (group.type === 'party' && user.party.quest && user.party.quest.key) {
return res.json(403, 'You cannot leave party during an active quest. Please leave the quest first');
}
// When removing the user from challenges, should we keep the tasks?
var keep = (/^remove-all/i).test(req.query.keep) ? 'remove-all' : 'keep-all';
async.parallel([
// Remove active quest from user if they're leaving the party
function(cb){
if (group.type != 'party') return cb(null,{},1);
user.party.quest = Group.cleanQuestProgress();
user.save(cb);
},
// Remove user from group challenges
function(cb){
async.waterfall([
// Find relevant challenges
function(cb2) {
Challenge.find({
_id: {$in: user.challenges}, // Challenges I am in
group: group._id // that belong to the group I am leaving
}, cb2);
},
// Update each challenge
function(challenges, cb2) {
Challenge.update(
{_id:{$in: _.pluck(challenges, '_id')}},
{$pull:{members:user._id}},
{multi: true},
function(err) {
cb2(err, challenges); // pass `challenges` above to cb
}
);
},
// Unlink the challenge tasks from user
function(challenges, cb2) {
async.waterfall(challenges.map(function(chal) {
return function(cb3) {
var i = user.challenges.indexOf(chal._id)
if (~i) user.challenges.splice(i,1);
user.unlink({cid:chal._id, keep:keep}, cb3);
}
}), cb2);
}
], cb);
},
// Update the group
function(cb){
var update = {$pull:{members:user._id}};
if (group.type == 'party' && group.quest.key){
update['$unset'] = {};
update['$unset']['quest.members.' + user._id] = 1;
}
// FIXME do we want to remove the group `if group.members.length == 0` ? (well, 1 since the update hasn't gone through yet)
if (group.members.length > 1) {
var seniorMember = _.find(group.members, function (m) {return m != user._id});
// If the leader is leaving (or if the leader previously left, and this wasn't accounted for)
var leader = group.leader;
if (leader == user._id || !~group.members.indexOf(leader)) {
update['$set'] = update['$set'] || {};
update['$set'].leader = seniorMember;
}
leader = group.quest && group.quest.leader;
if (leader && (leader == user._id || !~group.members.indexOf(leader))) {
update['$set'] = update['$set'] || {};
update['$set']['quest.leader'] = seniorMember;
}
}
update['$inc'] = {memberCount: -1};
Group.update({_id:group._id},update,cb);
}
],function(err){
group.leave(user, keep, function(err){
if (err) return next(err);
user = group = keep = null;
return res.send(204);
})
}
});
};
var inviteByUUIDs = function(uuids, group, req, res, next){
async.each(uuids, function(uuid, cb){
@@ -568,17 +527,18 @@ var inviteByUUIDs = function(uuids, group, req, res, next){
return cb({code:400,err:'User with id "' + uuid + '" not found'});
if (group.type == 'guild') {
if (_.contains(group.members,uuid))
return cb({code:400,err: "User already in that group"});
return cb({code:400, err: "User already in that group"});
if (invite.invitations && invite.invitations.guilds && _.find(invite.invitations.guilds, {id:group._id}))
return cb({code:400,err:"User already invited to that group"});
return cb({code:400, err:"User already invited to that group"});
sendInvite();
} else if (group.type == 'party') {
if (invite.invitations && !_.isEmpty(invite.invitations.party))
return cb({code:400,err:"User already pending invitation."});
Group.find({type:'party', members:{$in:[uuid]}}, function(err, groups){
return cb({code: 400,err:"User already pending invitation."});
Group.find({type: 'party', members: {$in: [uuid]}}, function(err, groups){
if (err) return cb(err);
if (!_.isEmpty(groups))
return cb({code:400,err:"User already in a party."})
if (!_.isEmpty(groups) && groups[0].members.length > 1) {
return cb({code: 400, err: "User already in a party."})
}
sendInvite();
});
}
@@ -600,9 +560,6 @@ var inviteByUUIDs = function(uuids, group, req, res, next){
async.series([
function(cb){
invite.save(cb);
},
function(cb){
group.save(cb);
}
], function(err, results){
if (err) return cb(err);
@@ -636,14 +593,20 @@ var inviteByUUIDs = function(uuids, group, req, res, next){
}, function(err){
if(err) return err.code ? res.json(err.code, {err: err.err}) : next(err);
// TODO pass group from save above don't find it again, or you have to find it again in order to run populate?
populateQuery(group.type, Group.findById(group._id)).exec(function(err, populatedGroup){
if(err) return next(err);
async.series([
function(cb) {
group.save(cb);
},
function(cb) {
// TODO pass group from save above don't find it again, or you have to find it again in order to run populate?
populateQuery(group.type, Group.findById(group._id)).exec(function(err, populatedGroup){
if(err) return next(err);
res.json(populatedGroup);
});
res.json(populatedGroup);
});
}
]);
});
};
var inviteByEmails = function(invites, group, req, res, next){
@@ -664,6 +627,7 @@ var inviteByEmails = function(invites, group, req, res, next){
}
// yeah, it supports guild too but for backward compatibility we'll use partyInvite as query
var link = '?partyInvite='+ utils.encrypt(JSON.stringify({id:group._id, inviter:res.locals.user._id, name:group.name}));
var inviterVars = utils.getUserInfo(res.locals.user, ['name', 'email']);
@@ -694,9 +658,9 @@ var inviteByEmails = function(invites, group, req, res, next){
}, function(err){
if(err) return err.code ? res.json(err.code, {err: err.err}) : next(err);
if(usersAlreadyRegistered.length > 0){
if (usersAlreadyRegistered.length > 0){
inviteByUUIDs(usersAlreadyRegistered, group, req, res, next);
}else{
} else{
// Send only status code down the line because it doesn't need
// info on invited users since they are not yet registered
@@ -708,11 +672,11 @@ var inviteByEmails = function(invites, group, req, res, next){
api.invite = function(req, res, next){
var group = res.locals.group;
if(req.body.uuids){
if (req.body.uuids) {
inviteByUUIDs(req.body.uuids, group, req, res, next);
}else if(req.body.emails){
} else if (req.body.emails) {
inviteByEmails(req.body.emails, group, req, res, next)
}else{
} else {
return res.json(400,{err: "Can invite only by email or uuid"});
}
}
@@ -739,6 +703,10 @@ api.removeMember = function(req, res, next){
return res.json(401, {err: "Only group leader can remove a member!"});
}
if(user._id === uuid){
return res.json(401, {err: "You cannot remove yourself!"});
}
if(_.contains(group.members, uuid)){
var update = {$pull:{members:uuid}};
if(group.quest && group.quest.members){
@@ -1068,3 +1036,37 @@ api.questAbort = function(req, res, next){
group = null;
})
}
api.questLeave = function(req, res, next) {
// Non-member leave quest while still in progress
var group = res.locals.group;
var user = res.locals.user;
if (!(group.quest && group.quest.active)) {
return res.json(404, { err: 'No active quest to leave' });
}
if (!(group.quest.members && group.quest.members[user._id])) {
return res.json(403, { err: 'You are not part of the quest' });
}
if (group.quest.leader === user._id) {
return res.json(403, { err: 'Quest leader cannot leave quest' });
}
delete group.quest.members[user._id];
group.markModified('quest.members');
user.party.quest = Group.cleanQuestProgress();
user.markModified('party.quest');
var groupSavePromise = Q.nbind(group.save, group);
var userSavePromise = Q.nbind(user.save, user);
Q.all([groupSavePromise(), userSavePromise()])
.done(function(values) {
return res.send(204);
}, function(error) {
return next(error);
});
}
+27 -6
View File
@@ -16,6 +16,7 @@ var logging = require('./../logging');
var acceptablePUTPaths;
var api = module.exports;
var qs = require('qs');
var firebase = require('../libs/firebase');
var webhook = require('../webhook');
// api.purchase // Shared.ops
@@ -365,13 +366,33 @@ api.cron = function(req, res, next) {
// api.reset // Shared.ops
api['delete'] = function(req, res, next) {
var plan = res.locals.user.purchased.plan;
if (plan && plan.customerId && !plan.dateTerminated)
var user = res.locals.user;
var plan = user.purchased.plan;
if (plan && plan.customerId && !plan.dateTerminated){
return res.json(400,{err:"You have an active subscription, cancel your plan before deleting your account."});
res.locals.user.remove(function(err){
if (err) return next(err);
res.send(200);
})
}
Group.find({
members: {
'$in': [user._id]
}
}, function(err, groups){
if(err) return next(err);
async.each(groups, function(group, cb){
group.leave(user, 'remove-all', cb);
}, function(err){
if(err) return next(err);
user.remove(function(err){
if(err) return next(err);
firebase.deleteUser(user._id);
res.send(200);
});
});
});
}
/*
+81
View File
@@ -0,0 +1,81 @@
var Firebase = require('firebase');
var nconf = require('nconf');
var isProd = nconf.get('NODE_ENV') === 'production';
var firebaseConfig = nconf.get('FIREBASE');
var firebaseRef;
var isFirebaseEnabled = (nconf.get('NODE_ENV') === 'production') && (firebaseConfig.ENABLED === 'true');
// Setup
if(isFirebaseEnabled){
firebaseRef = new Firebase('https://' + firebaseConfig.APP + '.firebaseio.com');
// TODO what happens if an op is sent before client is authenticated?
firebaseRef.authWithCustomToken(firebaseConfig.SECRET, function(err, authData){
// TODO it's ok to kill the server here? what if FB is offline?
if(err) throw new Error('Impossible to authenticate Firebase');
});
}
var api = module.exports = {};
api.updateGroupData = function(group){
if(!isFirebaseEnabled) return;
// TODO is throw ok? we don't have callbacks
if(!group) throw new Error('group is required.');
// Return in case of tavern (comparison working because we use string for _id)
if(group._id === 'habitrpg') return;
firebaseRef.child('rooms/' + group._id)
.set({
name: group.name
});
};
api.addUserToGroup = function(groupId, userId){
if(!isFirebaseEnabled) return;
if(!userId || !groupId) throw new Error('groupId, userId are required.');
if(groupId === 'habitrpg') return;
firebaseRef.child('members/' + groupId + '/' + userId)
.set(true);
firebaseRef.child('users/' + userId + '/rooms/' + groupId)
.set(true);
};
api.removeUserFromGroup = function(groupId, userId){
if(!isFirebaseEnabled) return;
if(!userId || !groupId) throw new Error('groupId, userId are required.');
if(groupId === 'habitrpg') return;
firebaseRef.child('members/' + groupId + '/' + userId)
.remove();
firebaseRef.child('users/' + userId + '/rooms/' + groupId)
.remove();
};
api.deleteGroup = function(groupId){
if(!isFirebaseEnabled) return;
if(!groupId) throw new Error('groupId is required.');
if(groupId === 'habitrpg') return;
firebaseRef.child('rooms/' + groupId)
.remove();
// FIXME not really necessary as long as we only store room data,
// as empty objects are automatically deleted (/members/... in future...)
firebaseRef.child('members/' + groupId)
.remove();
};
// FIXME not really necessary as long as we only store room data,
// as empty objects are automatically deleted
api.deleteUser = function(userId){
if(!isFirebaseEnabled) return;
if(!userId) throw new Error('userId is required.');
firebaseRef.child('users/' + userId)
.remove();
};
+104 -4
View File
@@ -4,7 +4,11 @@ var shared = require('../../../common');
var _ = require('lodash');
var async = require('async');
var logging = require('../logging');
var Challenge = require('./../models/challenge').model;
var firebase = require('../libs/firebase');
// NOTE any change to groups' members in MongoDB will have to be run through the API
// changes made directly to the db will cause Firebase to get out of sync
var GroupSchema = new Schema({
_id: {type: String, 'default': shared.uuid},
name: String,
@@ -239,7 +243,8 @@ process.nextTick(function(){
// Using _assign so we don't lose the reference to the exported tavern
_.assign(module.exports.tavern, tavern);
});
})
});
GroupSchema.statics.tavernBoss = function(user,progress) {
if (!progress) return;
@@ -342,6 +347,100 @@ GroupSchema.statics.bossQuest = function(user, progress, cb) {
})
}
// Remove user from this group
GroupSchema.methods.leave = function(user, keep, mainCb){
if(!user) return mainCb(new Error('Missing user.'));
if(keep && typeof keep === 'function'){
mainCb = keep;
keep = null;
}
if(typeof keep !== 'string') keep = 'keep-all'; // can be also 'remove-all'
var group = this;
var groupWasRemoved = false;
async.parallel([
// Remove user from group challenges
function(cb){
async.waterfall([
// Find relevant challenges
function(cb2) {
Challenge.find({
_id: {$in: user.challenges}, // Challenges I am in
group: group._id // that belong to the group I am leaving
}, cb2);
},
// Update each challenge
function(challenges, cb2) {
Challenge.update(
{_id: {$in: _.pluck(challenges, '_id')}},
{$pull: {members: user._id}},
{multi: true},
function(err) {
cb2(err, challenges); // pass `challenges` above to cb
}
);
},
// Unlink the challenge tasks from user
function(challenges, cb2) {
async.waterfall(challenges.map(function(chal) {
return function(cb3) {
var i = user.challenges.indexOf(chal._id)
if (~i) user.challenges.splice(i,1);
user.unlink({cid: chal._id, keep: keep}, cb3);
}
}), cb2);
}
], cb);
},
// Update the group
function(cb){
// If user is the last one in group and group is private, delete it
if(group.members.length === 1 && (
group.type === 'party' ||
(group.type === 'guild' && group.privacy === 'private')
)){
// TODO remove invitations to this group
groupWasRemoved = true;
Group.remove({
_id: group._id
}, cb);
}else{ // otherwise just remove a member
var update = {$pull: {members: user._id}};
// If the leader is leaving (or if the leader previously left, and this wasn't accounted for)
var leader = group.leader;
if(leader == user._id || !~group.members.indexOf(leader)){
var seniorMember = _.find(group.members, function (m) {return m != user._id});
// could not exist in case of public guild with 1 member who is leaving
if(seniorMember){
if (leader == user._id || !~group.members.indexOf(leader)) {
update['$set'] = update['$set'] || {};
update['$set'].leader = seniorMember;
}
}
}
update['$inc'] = {memberCount: -1};
Group.update({_id: group._id}, update, cb);
}
}
], function(err){
if(err) return mainCb(err);
firebase.removeUserFromGroup(group._id, user._id);
if(groupWasRemoved) firebase.deleteGroup(group._id);
return mainCb();
});
};
GroupSchema.methods.toJSON = function() {
var doc = this.toObject();
if(doc.chat){
@@ -358,14 +457,15 @@ module.exports.schema = GroupSchema;
var Group = module.exports.model = mongoose.model("Group", GroupSchema);
// initialize tavern if !exists (fresh installs)
Group.count({_id:'habitrpg'},function(err,ct){
Group.count({_id: 'habitrpg'}, function(err, ct){
if (ct > 0) return;
new Group({
_id: 'habitrpg',
chat: [],
leader: '9',
name: 'HabitRPG',
type: 'guild',
privacy:'public'
privacy: 'public'
}).save();
})
});
+2 -1
View File
@@ -163,7 +163,8 @@ var UserSchema = new Schema({
armoireEnabled: {type: Boolean, 'default': false},
armoireOpened: {type: Boolean, 'default': false},
armoireEmpty: {type: Boolean, 'default': false},
cardReceived: {type: Boolean, 'default': false}
cardReceived: {type: Boolean, 'default': false},
warnedLowHealth: {type: Boolean, 'default': false}
},
history: {
exp: Array, // [{date: Date, value: Number}], // big peformance issues if these are defined
+9 -1
View File
@@ -302,7 +302,7 @@ module.exports = (swagger, v2) ->
method: 'POST'
description: "Casts a spell on a target."
parameters: [
path 'spell',"The key of the spell to cast (see ../../common#content.coffee)",'string'
path 'spell',"The key of the spell to cast (see ../../common#content/index.coffee)",'string'
query 'targetType',"The type of object you're targeting",'string',['party','self','user','task']
query 'targetId',"The ID of the object you're targeting",'string'
@@ -527,6 +527,14 @@ module.exports = (swagger, v2) ->
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.questAbort
"/groups/{gid}/questLeave":
spec:
method: 'POST'
description: 'Leave an active quest (Quest leaders cannot leave active quests. They must abort the quest to leave)'
parameters: [path('gid','Group to leave quest in','string')]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.questLeave
#TODO PUT /groups/:gid/chat/:messageId
"/groups/{gid}/chat:GET":
+1
View File
@@ -13,6 +13,7 @@ router.post('/api/v2/user/reset-password', i18n.getUserLanguage, auth.resetPassw
router.post('/api/v2/user/change-password', i18n.getUserLanguage, auth.auth, auth.changePassword);
router.post('/api/v2/user/change-username', i18n.getUserLanguage, auth.auth, auth.changeUsername);
router.post('/api/v2/user/change-email', i18n.getUserLanguage, auth.auth, auth.changeEmail);
router.post('/api/v2/user/auth/firebase', i18n.getUserLanguage, auth.auth, auth.getFirebaseToken);
router.post('/api/v1/register', i18n.getUserLanguage, auth.registerUser);
router.post('/api/v1/user/auth/local', i18n.getUserLanguage, auth.loginLocal);
+5 -2
View File
@@ -20,11 +20,14 @@ router.get('/', i18n.getUserLanguage, locals, function(req, res) {
// -------- Marketing --------
var pages = ['front', 'privacy', 'terms', 'api', 'features', 'videos', 'contact', 'plans', 'new-stuff', 'community-guidelines', 'old-news', 'press-kit'];
var pages = ['front', 'privacy', 'terms', 'api', 'features', 'videos', 'contact', 'plans', 'new-stuff', 'community-guidelines', 'old-news', 'press-kit', 'faq'];
_.each(pages, function(name){
router.get('/static/' + name, i18n.getUserLanguage, locals, function(req, res) {
res.render('static/' + name, {env: res.locals.habitrpg});
res.render( 'static/' + name, {
env: res.locals.habitrpg,
marked: require('marked')
});
});
})
+2
View File
@@ -46,6 +46,8 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
});
autoinc.init(db);
require('./libs/firebase');
// load schemas & models
require('./models/challenge');
require('./models/group');
+13 -25
View File
@@ -1,37 +1,25 @@
//- If this variable gets any bigger, please move it out of this jade template and into common/script/content
- var questCategories = {'unlockable':'unlockableQuests','gold':'goldQuests','pet':'petQuests'}
include ../../shared/mixins
.container-fluid
.row: .col-xs-12
.npc_ian.pull-left-sm.col-centered
.popover.static-popover.fade.right.in.pull-left-sm.col-centered
.arrow.hidden-xs
h3.popover-title=env.t('ian')
.popover-content
p=env.t('ianText')
.row
.col-md-6
.clearfix
.npc_ian.pull-left-sm.col-centered
.popover.static-popover.fade.right.in.pull-left-sm.col-centered
.arrow.hidden-xs
h3.popover-title=env.t('ian')
.popover-content
p=env.t('ianText')
h3.equipment-title=env.t('yourQuests')
menu.inventory-list
li.customize-menu
p.muted(ng-show='questCount < 1')=env.t('noScrolls')
each caption,type in questCategories
menu.pets-menu(label=env.t(caption))
div(ng-repeat='(quest_key,points) in ownedItems(user.items.quests)', ng-init='quest = Content.quests[quest_key]', ng-if='Content.quests[quest_key].category === "#{type}"')
button.customize-option(ng-class='lockQuest(quest) ? "inventory_quest_scroll_locked inventory_quest_scroll_{{::quest.key}}_locked locked" : "inventory_quest_scroll inventory_quest_scroll_{{::quest.key}}"',
ng-click='showQuest(quest_key)',
data-popover-html="{{::lockQuest(quest) ? env.t('scrollsPre') : questPopover(quest) | markdown}}",
popover-title='{{::quest.text()}}', popover-trigger='mouseenter',
popover-placement='right', popover-append-to-body='true')
.badge.badge-info.stack-count {{points}}
+ownedQuests(true,'right')
.col-md-6.border-left
li.customize-menu
h3.equipment-title=env.t('questsForSale')
each caption,type in questCategories
menu.pets-menu(label=env.t(caption))
div(ng-repeat='quest in Content.questsByLevel', ng-if='quest.canBuy && quest.category === "#{type}"')
div(ng-repeat='type in Content.userCanOwnQuestCategories')
menu.pets-menu(label='{{env.t(type + "Quests")}}')
div(ng-repeat='quest in Content.questsByLevel', ng-if='quest.canBuy && quest.category === type')
button.customize-option(ng-class='lockQuest(quest) ? "inventory_quest_scroll_locked inventory_quest_scroll_{{::quest.key}}_locked locked" : "inventory_quest_scroll inventory_quest_scroll_{{::quest.key}}"',
data-popover-html="{{::lockQuest(quest,true) ? env.t('scrollsPre') : questPopover(quest) | markdown}}",
popover-title='{{::quest.text()}}', popover-append-to-body="true",
+28 -9
View File
@@ -39,9 +39,14 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
strong
!=env.t('helpWithTranslation')
hr
.form-horizontal
h5=env.t('dateFormat')
select.form-control(ng-model='user.preferences.dateFormat', ng-options='DF for DF in availableFormats', ng-change='set({"preferences.dateFormat": user.preferences.dateFormat})')
hr
.checkbox
label
input(type='checkbox', ng-click='hideHeader() ', ng-checked='user.preferences.hideHeader!==true')
@@ -71,20 +76,34 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
input(type='checkbox', ng-model='user.preferences.displayInviteToPartyWhenPartyIs1', ng-change='set({"preferences.displayInviteToPartyWhenPartyIs1": user.preferences.displayInviteToPartyWhenPartyIs1 ? true : false})')
span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('displayInviteToPartyWhenPartyIs1'))=env.t('displayInviteToPartyWhenPartyIs1')
// button.btn.btn-default(ng-click='showTour()', popover-placement='right', popover-trigger='mouseenter', popover=env.t('restartTour'))= env.t('showTour')
hr
button.btn.btn-default(ng-click='showBailey()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('showBaileyPop'))= env.t('showBailey')
button.btn.btn-default(ng-click='openRestoreModal()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('fixValPop'))= env.t('fixVal')
button.btn.btn-default(ng-if='user.preferences.disableClasses==true', ng-click='user.ops.changeClass({})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('enableClassPop'))= env.t('enableClass')
button.btn.btn-default(ng-if='!user.preferences.disableClasses && user.flags.classSelected', ng-click='showClassesTour()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('classTourPop'))= env.t('showClass')
div.alert.alert-warning(style='padding:2px;margin-top:7px')
h5=env.t('customDayStart')
h5=env.t('customDayStartInfo1')
a(ng-click='showCustomDayStartInfo = !showCustomDayStartInfo') {{!showCustomDayStartInfo ? env.t('showMoreMore') : env.t('showMoreLess')}}
h5(ng-if='showCustomDayStartInfo')!=env.t('customDayStartInfo4')
hr
h5=env.t('customDayStart')
alert.alert-warning=env.t('customDayStartInfo1')
.form-horizontal
.form-group
.input-group
input.form-control(type='number', min='0', max='23', ng-model='user.preferences.dayStart', ng-blur='saveDayStart()')
span.input-group-addon= ':00 (' + env.t('24HrClock') + ')'
.col-sm-7
select.form-control(ng-model='dayStart')
- var number = 0
while number < 24
- var value = number
- var meridian = number < 12 ? 'AM' : 'PM'
- var hour = number++ % 12
option(value=value) #{hour ? hour : 12}:00 #{meridian}
.col-sm-5
br.visible-xs
button.btn.btn-block.btn-primary(ng-click='openDayStartModal(dayStart)',
ng-disabled='dayStart == user.preferences.dayStart')
| Save Custom Day Start
.personal-options.col-md-6
.panel.panel-default
+2 -2
View File
@@ -7,7 +7,7 @@ div.chat-form.guidelines-not-accepted(ng-if='!user.flags.communityGuidelinesAcce
div
button.btn.btn-warning(ng-click='acceptCommunityGuidelines()')=env.t('iAcceptCommunityGuidelines')
.chat-buttons
button(type="button", ng-click='sync(group)')=env.t('toolTipMsg')
button(ng-if='group.chat.length > 0 || group.memberCount > 1', type="button", ng-click='sync(group)')=env.t('toolTipMsg')
form.chat-form(ng-if='user.flags.communityGuidelinesAccepted' ng-submit='postChat(group,message.content)')
div(ng-controller='AutocompleteCtrl')
@@ -19,5 +19,5 @@ form.chat-form(ng-if='user.flags.communityGuidelinesAccepted' ng-submit='postCha
.chat-controls.clearfix
.chat-buttons
input.btn(type='submit', value=env.t('sendChat'), ng-disabled='_sending')
button.btn(type="button", ng-click='sync(group)', ng-disabled='_sending')=env.t('toolTipMsg')
button.btn(ng-if='group.chat.length > 0 || group.memberCount > 1', type="button", ng-click='sync(group)', ng-disabled='_sending')=env.t('toolTipMsg')
include ../../shared/formatting-help
+47 -31
View File
@@ -16,7 +16,7 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
.panel.panel-default
.panel-heading(bindonce='group')
h3.panel-title
| {{group.name}}
span {{group.name}}
span.group-leave-join(ng-if='group')
a.btn.btn-sm.btn-danger.pull-right(ng-if=":: isMemberOfGroup(User.user._id, group)", ng-click='clickLeave(group, $event)')
span.glyphicon.glyphicon-ban-circle
@@ -50,16 +50,18 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
div(ng-show='!group._editing')
img.img-rendering-auto.pull-right(ng-show='group.logo', ng-src='{{group.logo}}')
markdown(text='group.description')
hr
br
p=env.t('groupLeader')
|:
a(class='badge badge-info', ng-click='clickMember(group.leader._id, true)')
a.badge.badge-info(ng-click='clickMember(group.leader._id, true)')
| {{group.leader.profile.name}}
.slight-vertical-padding
small.muted=env.t('groupID')
|: {{group._id}}
include ./challenge-box
.text-center(ng-if='group.type === "party"')
.row.row-margin: .col-sm-6.col-sm-offset-3
button.btn.btn-success.btn-block(
ng-if='!party.quest.key',
ng-click='clickStartQuest();'
)=env.t('startAQuest')
// ------ Members -------
.panel.panel-default
@@ -67,56 +69,68 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
h3.panel-title
=env.t('members')
button.pull-right.btn.btn-primary(ng-click="openInviteModal(group)")=env.t("inviteFriends")
.panel-body.modal-fixed-height
div.form-group(ng-if='::group.type=="party"')
p=env.t('partyList')
select.form-control#partyOrder(
ng-model='user.party.order',
ng-controller='ChatCtrl',
ng-options='k as v for (k , v) in partyOrderChoices',
ng-change='set({"party.order": user.party.order})'
)
|&nbsp;
select.form-control#partyOrderAscending(
ng-model='user.party.orderAscending',
ng-controller='ChatCtrl',
ng-options='k as v for (k , v) in partyOrderAscendingChoices',
ng-change='set({"party.orderAscending": user.party.orderAscending})'
)
table.table.table-striped(bindonce='group')
h4(ng-show='::group.memberCount === 1 && group.type === "party"')=env.t('partyEmpty')
table.table.table-striped(ng-show='::group.memberCount > 1 || group.type !== "party"' bindonce='group')
tr(ng-repeat='member in group.members track by member._id')
td.media
// allow leaders to ban members
div.pull-left(ng-show='group.leader._id == user.id && member._id != user._id')
.pull-left(ng-show='group.leader._id == user.id && member._id != user._id')
a.media-object(ng-click='removeMember(group, member, true)')
span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip'))
a.media-body
span(ng-click='clickMember(member._id, true)')
| {{member.profile.name}}
tr(ng-if='group.memberCount > group.members.length')
tr(ng-if='::group.memberCount > group.members.length')
td
|<span class='badge'>{{group.memberCount - group.members.length}}
span.badge {{group.memberCount - group.members.length}}
= ' ' + env.t('moreMembers')
|</span>
h4(ng-show='group.invites.length > 0')=env.t('invited')
table.table.table-striped
tr(ng-repeat='invite in group.invites')
td.media
// allow leaders to ban members
div.pull-left(ng-show='group.leader._id == user.id')
.pull-left(ng-show='group.leader._id == user.id')
a.media-object(ng-click='removeMember(group, invite, false)')
span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip'))
a.media-body
span(ng-click='clickMember(invite._id, true)')
| {{invite.profile.name}}
.panel.panel-default(ng-if='::group.type=="party" && group.memberCount > 1')
.panel-heading
h3.panel-title=env.t('partyList')
.panel-body
.form-group
select.form-control#partyOrder(
ng-model='user.party.order',
ng-controller='ChatCtrl',
ng-options='k as v for (k , v) in partyOrderChoices',
ng-change='set({"party.order": user.party.order})'
)
| &nbsp;
select.form-control#partyOrderAscending(
ng-model='user.party.orderAscending',
ng-controller='ChatCtrl',
ng-options='k as v for (k , v) in partyOrderAscendingChoices',
ng-change='set({"party.orderAscending": user.party.orderAscending})'
)
include ./challenge-box
div(ng-if="group")
a.btn.btn-danger(ng-if=':: isMemberOfGroup(User.user._id, group)', data-id='{{group.id}}', ng-click='clickLeave(group, $event)')=env.t('leave')
a.btn.btn-success(ng-if=':: !isMemberOfGroup(User.user._id, group)', ng-click='join(group)')=env.t('join')
.slight-vertical-padding
small.muted=env.t('groupID')
| : {{group._id}}
.slight-vertical-padding(ng-if='group.type === "party" && group.memberCount === 1')
small.muted=env.t('userId')
| : {{user._id}}
.col-md-8
div()
div
textarea.form-control(ng-show='group._editing', rows=6, placeholder=env.t('leaderMsg'), ng-model='group.leaderMessage')
table(ng-show='group.leaderMessage')
tr
@@ -131,3 +145,5 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
include ./chat-box
+chatMessages()
h4(ng-if='group.chat.length < 1 && group.type === "party"')=env.t('partyChatEmpty')
h4(ng-if='group.chat.length < 1 && group.type === "guild"')=env.t('guildChatEmpty')
+1 -17
View File
@@ -5,6 +5,7 @@ include ./challenges
include ./hall
include ./quests/index
include ./chat-message
include ./party
script(type='text/ng-template', id='partials/options.social.inbox.html')
.options-blurbmenu
@@ -26,23 +27,6 @@ script(type='text/ng-template', id='partials/options.social.inbox.html')
script(type='text/ng-template', id='partials/options.social.tavern.html')
include ./tavern
script(type='text/ng-template', id='partials/options.social.party.html')
div(ng-if='group._id')
include ./group
div(ng-if='!group._id')
div(ng-show='user.invitations.party.id').container-fluid
h2=env.t('invitedTo', {name: '{{user.invitations.party.name}}'})
a.btn.btn-success(data-type='party', ng-click='join(user.invitations.party)')=env.t('accept')
a.btn.btn-danger(ng-click='reject()')=env.t('reject')
div(ng-hide='user.invitations.party.id').container-fluid
h2=env.t('createAParty')
p
=env.t('noPartyText')
pre.prettyprint.
{{user.id}}
p!=env.t('LFG', {linkStart: "<a href='https://habitica.com/#/options/groups/guilds/f2db2a7f-13c5-454d-b3ee-ea1f5089e601'>", linkEnd: "</a>"})
include ./create-group
script(type='text/ng-template', id='partials/options.social.guilds.public.html')
.options-blurbmenu
small.muted=env.t('blurbGuildsPage')
+13
View File
@@ -0,0 +1,13 @@
include ../../shared/avatar/generated_avatar
script(type='text/ng-template', id='partials/options.social.party.html')
div(ng-if='group.loadingNewParty')
include ./party/loading-new-party
div(ng-if='group._id')
include ./party/leave-party-and-join-another
include ./group
div(ng-if='!group._id && !group.loadingNewParty')
include ./party/party-invitation
include ./party/start-a-party
@@ -0,0 +1,8 @@
- var newParty = 'User.user.invitations.party'
.containter-fluid(ng-if='#{newParty}.id && party._id')
.row.text-center
.col-sm-6.col-sm-offset-3.alert.alert-warning
p {{::env.t('invitedToNewParty', { partyName: #{newParty}.name })}}
p
button.btn.btn-success(ng-click='leaveOldPartyAndJoinNewParty(#{newParty}.id, #{newParty}.name)')=env.t('joinNewParty')
button.btn.btn-default(ng-click='reject()')=env.t('declineInvitation')
@@ -0,0 +1,5 @@
.container-fluid
.row.text-center: .col-xs-12
h2=env.t('loadingNewParty')
img(src='/page-loader.gif')
@@ -0,0 +1,9 @@
.container-fluid(ng-show='user.invitations.party.id')
h2=env.t('invitedTo', {name: '{{user.invitations.party.name}}'})
a.btn.btn-success(
data-type='party',
ng-click='join(user.invitations.party)'
)=env.t('accept')
a.btn.btn-danger(ng-click='reject()')=env.t('reject')
@@ -0,0 +1,23 @@
.container.text-center(ng-hide='user.invitations.party.id')
.row.row-margin: .col-sm-6.col-sm-offset-3
a.btn.btn-primary.btn-lg.btn-block(ng-click="inviteOrStartParty(group)")=env.t("startPartyWithFriends")
.row.row-margin(ng-controller='UserCtrl')
span.inline-block.party_preview
span.figure.herobox.inline-block
.character-sprites(style='margin:0 0 0 4.2em')
+generatedAvatar
.row.row-margin
ul
li=env.t('partyExplanation1')
li=env.t('partyExplanation2')
li=env.t('partyExplanation3')
.row.row-margin
h4=env.t('wantToStartParty')
.row.row-margin
.btn.btn-primary(ng-click='inviteOrStartParty(group)')=env.t('inviteFriendsNow')
.btn.btn-default(ng-click='create(newGroup); group.loadingNewParty = true')=env.t('inviteFriendsLater')
.btn.btn-default(ng-click='openModal("user-id",{track:"Join Existing Party"})')=env.t('joinExistingParty')
@@ -2,7 +2,7 @@ mixin participants(questStart)
-var isMember = 'group.quest.members[member._id]'
table.table.table-striped
tr(ng-repeat='member in group.members track by member._id',
ng-if='::#{!questStart} || #{isMember}')
ng-if='#{!questStart} || #{isMember}')
td
span(ng-if=':: group.quest.leader && group.quest.leader==member._id') *&nbsp;
a: span(ng-click='clickMember(member._id, true)') {{::member.profile.name}}
@@ -20,5 +20,7 @@ div(ng-if='group.quest.active==true')
include ./ianQuestInfo
unless tavern
button.btn.btn-sm.btn-warning(ng-if=':: (group.quest.leader && group.quest.leader==user._id && isMemberOfGroup(group.quest.leader,group) && isMemberOfRunningQuest(group.quest.leader,group))',
button.btn.btn-sm.btn-warning(ng-if=':: (group.quest.leader && group.quest.leader==user._id && isMemberOfRunningQuest(group.quest.leader,group))',
ng-click='questAbort(party)')=env.t('abort')
button.btn.btn-sm.btn-warning(ng-if='!(group.quest.leader && group.quest.leader === user._id) && isMemberOfRunningQuest(user._id,group)',
ng-click='questLeave(party)')=env.t('leaveQuest')
+3 -1
View File
@@ -1,3 +1,5 @@
include generated_avatar
mixin avatar(opts)
.character-sprites
.addthis_native_toolbox(ng-if='::profile._id==user._id',
@@ -16,7 +18,7 @@ mixin avatar(opts)
// Show avatar only if not currently affected by visual buff
- var buffs = '!profile.stats.buffs'
span(ng-if='#{buffs}.snowball && #{buffs}.spookDust && #{buffs}.shinySeed && #{buffs}.seafoam')
include generated_avatar
+generatedAvatar
// Mount Head
if !opts.minimal
@@ -4,39 +4,24 @@ mixin costumeSetting(type, options)
- var equipped = (options.prefix || '') + 'profile.items.gear.equipped.' + type + (options.suffix || '')
span(ng-class="profile.preferences.costume ? #{costume} : #{equipped}")
// Back accessory
+costumeSetting('back')
span(ng-class="profile.preferences.sleep ? 'skin_' + profile.preferences.skin + '_sleep' : 'skin_' + profile.preferences.skin")
// Shirt
span(class='{{profile.preferences.size}}_shirt_{{profile.preferences.shirt}}')
// Armor
+costumeSetting('armor', {prefix: "profile.preferences.size + '_' + "})
// Cape collar
+costumeSetting('back', {suffix: " + '_collar'"})
// Body
+costumeSetting('body')
// Empty outline of head
span(class='head_0')
// Hair
- var hairTypes = ['base', 'bangs', 'mustache', 'beard']
each type in hairTypes
span(class='hair_#{type}_{{profile.preferences.hair.#{type}}}_{{profile.preferences.hair.color}}')
// Head
+costumeSetting('eyewear')
+costumeSetting('head')
+costumeSetting('headAccessory')
// Flower
span(class='hair_flower_{{profile.preferences.hair.flower}}')
// Items held in hands
+costumeSetting('shield')
+costumeSetting('weapon')
mixin generatedAvatar(options)
- options = options || {}
+costumeSetting('back')
if options.sleep
span(ng-class="'skin_' + profile.preferences.skin + '_sleep'")
else
span(ng-class="profile.preferences.sleep ? 'skin_' + profile.preferences.skin + '_sleep' : 'skin_' + profile.preferences.skin")
span(class='{{profile.preferences.size}}_shirt_{{profile.preferences.shirt}}')
+costumeSetting('armor', {prefix: "profile.preferences.size + '_' + "})
+costumeSetting('back', {suffix: " + '_collar'"})
+costumeSetting('body')
span.head_0
- var hairTypes = ['base', 'bangs', 'mustache', 'beard']
each type in hairTypes
span(class='hair_#{type}_{{profile.preferences.hair.#{type}}}_{{profile.preferences.hair.color}}')
+costumeSetting('eyewear')
+costumeSetting('head')
+costumeSetting('headAccessory')
span(class='hair_flower_{{profile.preferences.hair.flower}}')
+costumeSetting('shield')
+costumeSetting('weapon')
+6 -7
View File
@@ -55,6 +55,8 @@ nav.toolbar(ng-controller='MenuCtrl')
li
a(ui-sref='options.settings.export')=env.t('exportData')
ul.toolbar-submenu
li
a(target='_blank' href='https://habitica.com/static/faq/')=env.t('FAQ')
li
a(target='_blank' href='http://habitica.wikia.com/wiki/')=env.t('overview')
li
@@ -65,8 +67,6 @@ nav.toolbar(ng-controller='MenuCtrl')
a(target='_blank' href='https://trello.com/c/odmhIqyW/440-read-first-table-of-contents')=env.t('requestAF')
li
a(target='_blank' href='http://habitica.wikia.com/wiki/Contributing_to_Habitica')=env.t('contributeToHRPG')
li
a(target='_blank' href='http://habitica.wikia.com/wiki/FAQ')=env.t('FAQ')
ul.toolbar-controls
li.toolbar-subscribe-button
button(ng-if='!user.purchased.plan.customerId',ui-sref='options.settings.subscription',popover-trigger='mouseenter',popover-placement='bottom',popover-title=env.t('subscriptions'),popover=env.t('subDescription'),popover-append-to-body='true')=env.t('subscribe')
@@ -147,13 +147,15 @@ nav.toolbar(ng-controller='MenuCtrl')
li
a(ui-sref='options.settings.export')=env.t('exportData')
li.toolbar-button-dropdown.highlight
a(target='_blank' href='http://habitica.wikia.com/wiki/')
a(target='_blank' href='https://habitica.com/static/faq/')
span.glyphicon.glyphicon-question-sign
span=env.t('help')
a(ng-class='{active: _expandedMenu.menu === "help"}', data-expand-menu, menu='help')
span &#9776;
div(ng-show='_expandedMenu.menu === "help"', data-close-menu)
ul.toolbar-submenu
li
a(target='_blank' href='https://habitica.com/static/faq/')=env.t('FAQ')
li
a(target='_blank' href='http://habitica.wikia.com/wiki/')=env.t('overview')
li
@@ -164,8 +166,6 @@ nav.toolbar(ng-controller='MenuCtrl')
a(target='_blank' href='https://trello.com/c/odmhIqyW/440-read-first-table-of-contents')=env.t('requestAF')
li
a(target='_blank' href='http://habitica.wikia.com/wiki/Contributing_to_Habitica')=env.t('contributeToHRPG')
li
a(target='_blank' href='http://habitica.wikia.com/wiki/FAQ')=env.t('FAQ')
ul.toolbar-subscribe(ng-if='!user.purchased.plan.customerId')
li.toolbar-subscribe-button
button.highlight(ui-sref='options.settings.subscription',popover-trigger='mouseenter',popover-placement='bottom',popover-title=env.t('subscriptions'),popover=env.t('subDescription'),popover-append-to-body='true')=env.t('subscribe')
@@ -252,7 +252,7 @@ nav.toolbar(ng-controller='MenuCtrl')
a(ui-sref='options.settings.notifications')=env.t('notifications')
ul.toolbar-submenu
li
a(href='http://habitica.wikia.com/wiki/FAQ', target='_blank')=env.t('FAQ')
a(href='https://habitica.com/static/faq', target='_blank')=env.t('FAQ')
li
a(href='https://vimeo.com/57654086', target='_blank')=env.t('tutorials')
ul.toolbar-controls
@@ -261,7 +261,6 @@ nav.toolbar(ng-controller='MenuCtrl')
ul.toolbar-wallet
li.toolbar-gems(popover-trigger='mouseenter', popover-title=env.t('gemsPopoverTitle'), popover=env.t('gemsWhatFor'), popover-placement='bottom',popover-append-to-body='true')
a.gem-wallet(ng-click='openModal("buyGems",{track:"Gems > Toolbar"})')
//-span.task-action-btn.tile.flush.bright.add-gems-btn
span.Pet_Currency_Gem2x.Gems
span.gem-text {{user.balance * 4 | number:0}}
li.toolbar-currency.gold(popover=env.t('gold') + ' ({{Shared.gold(user.stats.gp)}})', popover-placement='bottom',popover-trigger='mouseenter')
+17 -1
View File
@@ -16,4 +16,20 @@ mixin previewMarkdown(text)
.panel.panel-warning
.panel-heading=env.t('msgPreviewHeading')
.panel-body.markdown-preview
markdown(text='#{text}')
markdown(text='#{text}')
mixin ownedQuests(popoverAppend, popoverPlacement)
menu.inventory-list
li.customize-menu
p.muted(ng-show='questCount < 1')=env.t('noScrolls')
div(ng-repeat='type in Content.userCanOwnQuestCategories track by $index')
menu.pets-menu(ng-if='Shared.count.questsOfCategory(user.items.quests,type) > 0', label='{{::env.t(type + "Quests")}}')
div(ng-repeat='(quest_key,points) in ownedItems(user.items.quests)', ng-init='quest = Content.quests[quest_key]', ng-if='Content.quests[quest_key].category === type')
button.customize-option(ng-class='lockQuest(quest) ? "inventory_quest_scroll_locked inventory_quest_scroll_{{::quest.key}}_locked locked" : "inventory_quest_scroll inventory_quest_scroll_{{::quest.key}}"',
ng-click='$close(); showQuest(quest_key)',
data-popover-html="{{::lockQuest(quest) ? env.t('scrollsPre') : questPopover(quest) | markdown}}",
popover-title='{{::quest.text()}}',
popover-trigger='mouseenter',
popover-append-to-body='#{popoverAppend}',
popover-placement='#{popoverPlacement}')
.badge.badge-info.stack-count {{points}}
+1 -16
View File
@@ -15,22 +15,7 @@ script(type='text/ng-template', id='modals/death.html')
| {{Math.ceil(user.stats.hp)}} / {{::Shared.maxHealth}}
figure.herobox.text-center
.character-sprites
+costumeSetting('back')
span(ng-class="'skin_' + profile.preferences.skin + '_sleep'")
span(class='{{profile.preferences.size}}_shirt_{{profile.preferences.shirt}}')
+costumeSetting('armor', {prefix: "profile.preferences.size + '_' + "})
+costumeSetting('back', {suffix: " + '_collar'"})
+costumeSetting('body')
span(class='head_0')
- var hairTypes = ['base', 'bangs', 'mustache', 'beard']
each type in hairTypes
span(class='hair_#{type}_{{profile.preferences.hair.#{type}}}_{{profile.preferences.hair.color}}')
+costumeSetting('eyewear')
+costumeSetting('head')
+costumeSetting('headAccessory')
span(class='hair_flower_{{profile.preferences.hair.flower}}')
+costumeSetting('shield')
+costumeSetting('weapon')
+generatedAvatar({sleep:true})
span(class='knockout')
.col-md-6
h4(style='margin-top:1.5em')=env.t('dontDespair')
+5
View File
@@ -13,3 +13,8 @@ include ./rebirth
include ./limited
include ./invite-friends
include ./welcome.jade
include ./low-health.jade
//- Settings
script(type='text/ng-template', id='modals/change-day-start.html')
include ./settings/change-day-start.jade
+91 -19
View File
@@ -1,21 +1,29 @@
script(type='text/ng-template', id='modals/invite-friends.html')
script(type='text/ng-template', id='modals/invite-guild.html')
.modal-header
h4=env.t('inviteFriends')
.modal-body
p.alert.alert-info!=env.t('inviteAlertInfo')
form.form-inline(ng-submit='invite()')
//-.alert.alert-danger(ng-show='_groupError') {{_groupError}}
.form-group
input.form-control(type='text', placeholder=env.t('userId'), ng-model='invitee')
|&nbsp;
button.btn.btn-primary(type='submit')=env.t('inviteExistUser')
form.form-horizontal(ng-submit='inviteNewUsers("uuid")')
table.table.table-striped
thead
tr
th=env.t('userId')
tbody
tr(ng-repeat='user in invitees track by $index')
td
input.form-control(type='text', ng-model='user.uuid')
//- @TODO: Refactor src/controllers/groups api.invite to use promises before implimenting this
//- tr
//- td
//- a.btn.btn-xs.pull-right(ng-click='addUuid()')
//- i.glyphicon.glyphicon-plus
tr
td
.col-sm-4.col-sm-offset-8
button.btn.btn-primary.btn-block(type='submit')=env.t('sendInvitations')
hr
p.alert.alert-info=env.t('inviteByEmail')
form.form-horizontal(ng-submit='inviteEmails()')
form.form-horizontal(ng-submit='inviteNewUsers("email")')
table.table.table-striped
thead
tr
@@ -29,7 +37,7 @@ script(type='text/ng-template', id='modals/invite-friends.html')
input.form-control(type='email', ng-model='email.email')
tr
td(colspan=2)
a.btn.btn-xs.pull-right(ng-click='emails = emails.concat([{name:"",email:""}])')
a.btn.btn-xs.pull-right(ng-click='addEmail()')
i.glyphicon.glyphicon-plus
tr
td.form-group(colspan=2)
@@ -37,12 +45,76 @@ script(type='text/ng-template', id='modals/invite-friends.html')
.col-sm-7
input.form-control(type='text', ng-model='inviter')
.col-sm-4
button.btn.btn-primary.btn-wrap(type='submit')=env.t('inviteNewUsers')
//-
hr
p.alert.alert-info=env.t('inviteAlertInfo2')
input.form-control(type='text', ng-value='inviteLink({id: party._id, inviter: user._id, name: party.name})')
button.btn.btn-primary.btn-block(type='submit')=env.t('sendInvitations')
.modal-footer
button.btn.btn-default(ng-click='$close()')=env.t('close')
script(type='text/ng-template', id='modals/invite-party.html')
.modal-body
div(ng-if='!group._id')
h4(style='margin-top:0')=env.t('nameYourParty')
input.form-control#new-group-name.input-medium.option-content(required, type='text', ng-model='group.name')
tabset
tab(heading=env.t('inviteByEmail'))
p.alert.alert-info=env.t('inviteByEmailExplanation')
form.form-horizontal(ng-submit='inviteNewUsers("email")')
table.table.table-striped
thead
tr
th=env.t('name')
th=env.t('email')
tbody
tr(ng-repeat='email in emails')
td
input.form-control(type='text', ng-model='email.name')
td
input.form-control(type='email', ng-model='email.email')
tr
td(colspan=2)
a.btn.btn-xs.pull-right(ng-click='addEmail()')
i.glyphicon.glyphicon-plus
tr
td.form-group(colspan=2)
label.col-sm-1.control-label=env.t('byColon')
.col-sm-7
input.form-control(type='text', ng-model='inviter')
.col-sm-4
button.btn.btn-primary.btn-block(type='submit')=env.t('sendInvitations')
tab(heading=env.t('inviteExistUser'))
p.alert.alert-info!=env.t('inviteAlertInfo')
form.form-horizontal(ng-submit='inviteNewUsers("uuid")')
table.table.table-striped
thead
tr
th=env.t('userId')
tbody
tr(ng-repeat='user in invitees track by $index')
td
input.form-control(type='text', ng-model='user.uuid')
//- @TODO: Refactor src/controllers/groups api.invite to use promises before implimenting this
//- tr
//- td
//- a.btn.btn-xs.pull-right(ng-click='addUuid()')
//- i.glyphicon.glyphicon-plus
tr
td
.col-sm-4.col-sm-offset-8
button.btn.btn-primary.btn-block(type='submit')=env.t('sendInvitations')
.row
.quest_basilist.pull-left
p(style='margin:2em')=env.t('exclusiveQuestScroll')
button.btn.btn-default.pull-right(style='margin:auto 1em', ng-click='$close()')=env.t('close')
.modal-footer(style='padding-bottom:0',ng-if='!group._id || group.memberCount < 2')
p!=env.t('wantExistingParty', {linkStart: "<a href='/#/options/groups/guilds/f2db2a7f-13c5-454d-b3ee-ea1f5089e601'>", linkEnd: "</a>"})
pre(style='margin:1em 0 0 0').
{{::user._id}}
script(type='text/ng-template', id='modals/user-id.html')
.modal-header
h4=env.t('userId')
.modal-body
p!=env.t('wantExistingParty', {linkStart: "<a href='/#/options/groups/guilds/f2db2a7f-13c5-454d-b3ee-ea1f5089e601'>", linkEnd: "</a>"})
pre(style='margin:1em 0 0 0').
{{::user._id}}
.modal-footer
.btn.btn-default.pull-right(ng-click='$close()')=env.t('close')
@@ -0,0 +1,28 @@
include ../avatar/generated_avatar
script(type='text/ng-template', id='modals/lowHealth.html')
.modal-header
h3.text-center=env.t('losingHealthWarning')
.modal-body
.hero-stats(style='position:absolute; margin-left:9em; width:50%')
.meter-label(tooltip=env.t('health'))
span.glyphicon.glyphicon-heart
.meter.health(tooltip='{{Math.round(user.stats.hp * 100) / 100}}')
.bar(ng-style='{"width": Shared.percent(user.stats.hp, Shared.maxHealth)+"%"}')
span.meter-text.value
| {{Math.ceil(user.stats.hp)}} / {{::Shared.maxHealth}}
.herobox.inline-block(style='padding-top:0em; margin-left:16em')
.character-sprites(style='display:inline-flex; margin:3em auto')
+generatedAvatar
p=env.t('losingHealthWarning2')
h4=env.t('toRegainHealth')
ul
li.spaced=env.t('lowHealthTips1')
li.spaced=env.t('lowHealthTips2')
h4=env.t('losingHealthQuickly')
ul
li.spaced=env.t('lowHealthTips3')
li.spaced=env.t('lowHealthTips4')
h4=env.t('goodLuck')
.modal-footer(style='margin-top: 0em')
a.btn.btn-primary(ng-click='acknowledgeHealthWarning(); $close()')=env.t('ok')
+30 -16
View File
@@ -1,24 +1,27 @@
include ./quest-rewards
include ../mixins
mixin questInfo
.pull-right-sm.text-center
.col-centered(class='quest_{{::selectedQuest.key}}')
div(ng-if='::selectedQuest.boss')
h4 {{::selectedQuest.boss.name()}}
p
strong=env.t('bossHP') + ': '
| {{::selectedQuest.boss.hp}}
p
strong=env.t('bossStrength') + ': '
| {{::selectedQuest.boss.str}}
.well.well-sm
p
strong=env.t('bossHP') + ': '
| {{::selectedQuest.boss.hp}}
p
strong=env.t('bossStrength') + ': '
| {{::selectedQuest.boss.str}}
div(ng-if='::selectedQuest.collect')
p(ng-repeat='(k,v) in ::selectedQuest.collect')
strong=env.t('collect') + ': '
| {{::selectedQuest.collect[k].count}} {{::selectedQuest.collect[k].text()}}
h4=env.t('itemsToCollect')
.well.well-sm
p(ng-repeat='(k,v) in ::selectedQuest.collect')
| {{:: env.t('collectionItems', { number: selectedQuest.collect[k].count, items: selectedQuest.collect[k].text() })}}
div(ng-bind-html='::selectedQuest.notes()')
quest-rewards(key='{{::selectedQuest.key}}', header=env.t('rewards'))
script(type='text/ng-template', id='modals/questCompleted.html')
script(type='text/ng-template', id='modals/questCompleted.html')
.modal-header
h4 "{{::Content.quests[user.party.quest.completed].text()}}"
=env.t('completed')
@@ -29,7 +32,7 @@ script(type='text/ng-template', id='modals/questCompleted.html')
.modal-footer
button.btn.btn-primary(ng-click='set({"party.quest.completed":""}); $close()')=env.t('ok')
script(type='text/ng-template', id='modals/showQuest.html')
script(type='text/ng-template', id='modals/showQuest.html')
.modal-header
h4 {{::selectedQuest.text()}}
.modal-body
@@ -40,9 +43,10 @@ script(type='text/ng-template', id='modals/showQuest.html')
p=env.t('questWarning')
.modal-footer
button.btn.btn-default(ng-click='closeQuest(); $close()')=env.t('cancel')
button.btn.btn-primary(ng-click='questInit(); $close()')=env.t('inviteParty')
button.btn.btn-primary(ng-click='questInit(); $close()')
| {{:: party.memberCount > 1 ? env.t('inviteParty') : env.t('startQuest')}}
script(type='text/ng-template', id='modals/buyQuest.html')
script(type='text/ng-template', id='modals/buyQuest.html')
.modal-header
h4 {{::selectedQuest.text()}}
.modal-body
@@ -52,7 +56,7 @@ script(type='text/ng-template', id='modals/buyQuest.html')
button.btn.btn-primary(ng-if='::selectedQuest.category !== "gold"', ng-click='purchase("quests", quest); closeQuest(); $close()')=env.t('buyQuest') + ': {{::selectedQuest.value}} ' + env.t('gems')
button.btn.btn-primary(ng-if='::selectedQuest.category === "gold"', ng-click='user.ops.buyQuest({params:{key:selectedQuest.key}}); closeQuest(); $close()')=env.t('buyQuest') + ': {{::selectedQuest.goldValue}} ' + env.t('gold')
script(type='text/ng-template', id='modals/questInvitation.html')
script(type='text/ng-template', id='modals/questInvitation.html')
.modal-header
h4=env.t('questInvitation')
|&nbsp;{{::Content.quests[user.party.quest.key].text()}}
@@ -62,7 +66,7 @@ script(type='text/ng-template', id='modals/questInvitation.html')
div(ng-if='::Content.quests[user.party.quest.key].boss')
h4 {{::Content.quests[user.party.quest.key].boss.name()}}
p
strong=env.t('bossHP') + ': '
strong=env.t('bossHP') + ': '
| {{::Content.quests[user.party.quest.key].boss.hp}}
p
strong=env.t('bossStrength') + ': '
@@ -70,7 +74,7 @@ script(type='text/ng-template', id='modals/questInvitation.html')
div(ng-if='::Content.quests[user.party.quest.key].collect')
p(ng-repeat='(k,v) in ::Content.quests[user.party.quest.key].collect')
strong=env.t('collect') + ': '
| {{::Content.quests[user.party.quest.key].collect[k].count}} {{::Content.quests[user.party.quest.key].collect[k].text()}}
| {{::Content.quests[user.party.quest.key].collect[k].count}} {{::Content.quests[user.party.quest.key].collect[k].text()}}
div(ng-bind-html='::Content.quests[user.party.quest.key].notes()')
quest-rewards(key='{{::user.party.quest.key}}', header=env.t('rewards'))
.modal-footer
@@ -92,3 +96,13 @@ script(type='text/ng-template', id='modals/questDrop.html')
button.btn.btn-default(ng-click='closeQuest(); $close()')=env.t('questLater')
button.btn.btn-primary(ng-click='inviteOrStartParty(group); $close()', ng-if='!party.members')=env.t('startAParty')
button.btn.btn-primary(ng-click='questInit(); $close()', ng-if='party.members')=env.t('inviteParty')
script(type='text/ng-template', id='modals/ownedQuests.html')
.modal-header
h4=env.t('whichQuestStart')
.modal-body
+ownedQuests(false,'top')
.modal-footer
h4.pull-left
a(ui-sref='options.inventory.quests', ng-click='$close()')=env.t('getMoreQuests')
button.btn.btn-default.pull-right(ng-click='$close()')=env.t('neverMind')
@@ -0,0 +1,11 @@
.text-center
.modal-header
h3=env.t('changeCustomDayStart')
.modal-body
p!=env.t('nextCron', {time: '<strong class="text-danger">{{::nextCron | date: user.preferences.dateFormat + " @ h:mm a"}}</strong>'})
.modal-footer
p.pull-left-sm=env.t('sureChangeCustomDayStart')
a.btn.btn-default(ng-click='$close()')=env.t('close')
a.btn.btn-primary(ng-click='saveDayStart(); $close()')=env.t('confirm')
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
extends ./layout
block vars
- var layoutEnv = env
block title
title Habitica &VerticalLine;&nbsp;
=env.t('FAQ')
block content
.row(ng-controller='AccordionCtrl')
.col-md-12
.page-header
h1=env.t('frequentlyAskedQuestions')
p.pagemeta
=env.t('lastUpdated')
|&nbsp;
=env.t('September')
|&nbsp;4&comma; 2015
- var headings = ['overview', 'set-up-tasks', 'sample-tasks', 'task-color', 'health', 'party-with-friends', 'pets-mounts', 'character-classes', 'blue-mana-bar', 'monsters-quests', 'gems', 'bugs-features']
- for heading, index in headings
a.h2.accordion.collapsed(ng-href='#' + heading, data-toggle='collapse')=env.t('faqQuestion' + index)
.clearfix.collapse(id=heading)
!=marked(env.t('webFaqAnswer' + index))
hr
h3!=marked(env.t('webFaqStillNeedHelp'))
+1 -1
View File
@@ -23,7 +23,7 @@ html(ng-app='habitrpg')
meta(charset='utf-8')
meta(name='viewport', content='width=device-width, initial-scale=1.0')
//FIXME for some reason this won't load when in footerCtrl.js#deferredScripts()
script(type="text/javascript", src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5016f6cc44ad68a4", async="async")
+3 -2
View File
@@ -8,5 +8,6 @@ block title
title Old News
block content
include ../shared/new-stuff
+oldNews()
.static-old-news
include ../shared/new-stuff
+oldNews()