Merge branch 'develop' of https://github.com/HabitRPG/habitrpg into merchant-button-fix

This commit is contained in:
Thrashmandicoot
2015-10-30 03:43:35 -07:00
594 changed files with 16411 additions and 12372 deletions
@@ -34,6 +34,18 @@ function($scope, $rootScope, User, $http, Notification, ApiUrl) {
// Twitter
$.getScript('https://platform.twitter.com/widgets.js');
// Facebook
(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.5";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
// Tumblr
$.getScript('https://assets.tumblr.com/share-button.js');
/* Google Content Experiments
if (window.env.NODE_ENV === 'production') {
$.getScript('//www.google-analytics.com/cx/api.js?experiment=boVO4eEyRfysNE5D53nCMQ', function(){
+23 -3
View File
@@ -2,8 +2,8 @@
// Make user and settings available for everyone through root scope.
habitrpg.controller('SettingsCtrl',
['$scope', 'User', '$rootScope', '$http', 'ApiUrl', 'Guide', '$location', '$timeout', 'Notification', 'Shared',
function($scope, User, $rootScope, $http, ApiUrl, Guide, $location, $timeout, Notification, Shared) {
['$scope', 'User', '$rootScope', '$http', 'ApiUrl', 'Guide', '$location', '$timeout', 'Content', 'Notification', 'Shared',
function($scope, User, $rootScope, $http, ApiUrl, Guide, $location, $timeout, Content, Notification, Shared) {
// FIXME we have this re-declared everywhere, figure which is the canonical version and delete the rest
// $scope.auth = function (id, token) {
@@ -150,6 +150,7 @@ habitrpg.controller('SettingsCtrl',
Notification.text(env.t('promoCodeApplied'));
});
}
$scope.generateCodes = function(codes){
$http.post(ApiUrl.get() + '/api/v2/coupons/generate/'+codes.event+'?count='+(codes.count || 1))
.success(function(res,code){
@@ -158,6 +159,7 @@ habitrpg.controller('SettingsCtrl',
window.location.href = '/api/v2/coupons?limit='+codes.count+'&_id='+User.user._id+'&apiToken='+User.user.apiToken;
})
}
$scope.releasePets = function() {
User.user.ops.releasePets({});
$rootScope.$state.go('tasks');
@@ -194,12 +196,30 @@ habitrpg.controller('SettingsCtrl',
$http.get(ApiUrl.get() + '/api/v2/coupons/valid-discount/'+coupon)
.success(function(){
Notification.text("Coupon applied!");
var subs = $scope.Content.subscriptionBlocks;
var subs = Content.subscriptionBlocks;
subs["basic_6mo"].discount = true;
subs["google_6mo"].discount = false;
});
}
$scope.gemGoldCap = function(subscription) {
var baseCap = 25;
var gemCapExtra = User.user.purchased.plan.consecutive.gemCapExtra;
// @TODO: What are these magic numbers? 3? 5?
var blocks = Content.subscriptionBlocks[subscription.key].months / 3 * 5;
var flooredBlocks = Math.floor(blocks);
var userTotalDropCap = baseCap + gemCapExtra + flooredBlocks;
var maxDropCap = 50;
return [userTotalDropCap, maxDropCap];
};
$scope.numberOfMysticHourglasses = function(subscription) {
var numberOfHourglasses = Content.subscriptionBlocks[subscription.key].months / 3;
return Math.floor(numberOfHourglasses);
};
function _calculateNextCron() {
$scope.dayStart;
@@ -108,13 +108,14 @@ angular.module("habitrpg").factory("Notification",
var stack_topright = {"dir1": "down", "dir2": "left", "spacing1": 15, "spacing2": 15, "firstpos1": 60};
function _notify(html, type, icon) {
console.log(type)
var notice = $.pnotify({
type: type || 'warning', //('info', 'text', 'warning', 'success', 'gp', 'xp', 'hp', 'lvl', 'death', 'mp', 'crit')
text: html,
opacity: 1,
addclass: 'alert-' + type,
delay: 7000,
hide: (type == 'error') ? false : true,
hide: (type == 'error' || type == 'danger') ? false : true,
mouse_reset: false,
width: "250px",
stack: stack_topright,
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

+7 -3
View File
@@ -31,7 +31,9 @@ function track(eventType, data) {
function _sendDataToAmplitude(eventType, data) {
var amplitudeData = _formatDataForAmplitude(data);
amplitudeData.event_type = eventType;
amplitude.track(amplitudeData);
amplitude.track(amplitudeData).catch(function(error) {
// @TODO log error with new relic
});
}
function _sendDataToGoogle(eventType, data) {
@@ -87,7 +89,9 @@ function _sendPurchaseDataToAmplitude(data) {
amplitudeData.event_type = 'purchase';
amplitudeData.revenue = data.purchaseValue;
amplitude.track(amplitudeData)
amplitude.track(amplitudeData).catch(function(error) {
// @TODO log error with new relic
});
}
function _formatDataForAmplitude(data) {
@@ -95,7 +99,7 @@ function _formatDataForAmplitude(data) {
var event_properties = _.omit(data, PROPERTIES_TO_SCRUB);
var ampData = {
user_id: data.uuid,
user_id: data.uuid || 'no-user-id-was-provided',
platform: 'server',
event_properties: event_properties
}
+46 -25
View File
@@ -25,10 +25,6 @@ var accountSuspended = function(uuid){
code: 'ACCOUNT_SUSPENDED'
};
}
// Allow case-insensitive regex searching for Mongo queries. See http://stackoverflow.com/a/3561711/362790
var RegexEscape = function(s){
return new RegExp('^' + s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$', 'i');
}
api.auth = function(req, res, next) {
var uid = req.headers['x-api-user'];
@@ -67,35 +63,42 @@ api.authWithUrl = function(req, res, next) {
}
api.registerUser = function(req, res, next) {
var regEmail = RegexEscape(req.body.email),
regUname = RegexEscape(req.body.username);
var email = req.body.email && req.body.email.toLowerCase();
var username = req.body.username;
// Get the lowercase version of username to check that we do not have duplicates
// So we can search for it in the database and then reject the choosen username if 1 or more results are found
var lowerCaseUsername = username && username.toLowerCase();
async.auto({
validate: function(cb) {
if (!(req.body.username && req.body.password && req.body.email))
if (!(username && req.body.password && email))
return cb({code:401, err: ":username, :email, :password, :confirmPassword required"});
if (req.body.password !== req.body.confirmPassword)
return cb({code:401, err: ":password and :confirmPassword don't match"});
if (!validator.isEmail(req.body.email))
if (!validator.isEmail(email))
return cb({code:401, err: ":email invalid"});
cb();
},
findReg: function(cb) {
User.findOne({$or:[{'auth.local.email': regEmail}, {'auth.local.username': regUname}]}, {'auth.local':1}, cb);
// Search for duplicates using lowercase version of username
User.findOne({$or:[{'auth.local.email': email}, {'auth.local.lowerCaseUsername': lowerCaseUsername}]}, {'auth.local':1}, cb);
},
findFacebook: function(cb){
User.findOne({_id: req.headers['x-api-user'], apiToken: req.headers['x-api-key']}, {auth:1}, cb);
},
register: ['validate', 'findReg', 'findFacebook', function(cb, data) {
if (data.findReg) {
if (regEmail.test(data.findReg.auth.local.email)) return cb({code:401, err:"Email already taken"});
if (regUname.test(data.findReg.auth.local.username)) return cb({code:401, err:"Username already taken"});
if (email === data.findReg.auth.local.email) return cb({code:401, err:"Email already taken"});
// Check that the lowercase username isn't already used
if (lowerCaseUsername === data.findReg.auth.local.lowerCaseUsername) return cb({code:401, err:"Username already taken"});
}
var salt = utils.makeSalt();
var newUser = {
auth: {
local: {
username: req.body.username,
email: req.body.email,
username: username,
lowerCaseUsername: lowerCaseUsername, // Store the lowercase version of the username
email: email, // Store email as lowercase
salt: salt,
hashed_password: utils.encryptPassword(req.body.password, salt)
},
@@ -115,12 +118,14 @@ api.registerUser = function(req, res, next) {
var analyticsData = {
category: 'acquisition',
type: 'local',
gaLabel: 'local'
gaLabel: 'local',
uuid: user._id,
};
analytics.track('register', analyticsData)
user.save(function(err, savedUser){
// Clean previous email preferences
// TODO when emails added to EmailUnsubcription they should use lowercase version
EmailUnsubscription.remove({email: savedUser.auth.local.email}, function(){
utils.txnEmail(savedUser, 'welcome');
});
@@ -143,7 +148,10 @@ api.loginLocal = function(req, res, next) {
var username = req.body.username;
var password = req.body.password;
if (!(username && password)) return res.json(401, {err:'Missing :username or :password in request body, please provide both'});
var login = validator.isEmail(username) ? {'auth.local.email':username} : {'auth.local.username':username};
var login = validator.isEmail(username) ?
{'auth.local.email':username.toLowerCase()} : // Emails are all lowercase
{'auth.local.username':username}; // Use the username as the user typed it
User.findOne(login, {auth:1}, function(err, user){
if (err) return next(err);
if (!user) return res.json(401, {err:"Uh-oh - your username or password is incorrect.\n- Make sure your username or email is typed correctly.\n- You may have signed up with Facebook, not email. Double-check by trying Facebook login.\n- If you forgot your password, click \"Forgot Password\"."});
@@ -205,7 +213,8 @@ api.loginSocial = function(req, res, next) {
var analyticsData = {
category: 'acquisition',
type: network,
gaLabel: network
gaLabel: network,
uuid: user._id,
};
analytics.track('register', analyticsData)
}]
@@ -233,12 +242,14 @@ api.deleteSocial = function(req,res,next){
}
api.resetPassword = function(req, res, next){
var email = req.body.email,
var email = req.body.email && req.body.email.toLowerCase(), // Emails are all lowercase
salt = utils.makeSalt(),
newPassword = utils.makeSalt(), // use a salt as the new password too (they'll change it later)
hashed_password = utils.encryptPassword(newPassword, salt);
User.findOne({'auth.local.email': RegexEscape(email)}, function(err, user){
if(!email) return res.json(400, {err: "Email not provided"});
User.findOne({'auth.local.email': email}, function(err, user){
if (err) return next(err);
if (!user) return res.send(401, {err:"Sorry, we can't find a user registered with email " + email + "\n- Make sure your email address is typed correctly.\n- You may have signed up with Facebook, not email. Double-check by trying Facebook login."});
user.auth.local.salt = salt;
@@ -266,15 +277,22 @@ var invalidPassword = function(user, password){
}
api.changeUsername = function(req, res, next) {
var user = res.locals.user;
var username = req.body.username;
var lowerCaseUsername = username && username.toLowerCase(); // we search for the lowercased version to intercept duplicates
if(!username) return res.json(400, {err: "Username not provided"});
async.waterfall([
function(cb){
User.findOne({'auth.local.username': RegexEscape(req.body.username)}, {auth:1}, cb);
User.findOne({'auth.local.lowerCaseUsername': lowerCaseUsername}, {auth:1}, cb);
},
function(found, cb){
if (found) return cb({code:401, err: "Username already taken"});
if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password));
res.locals.user.auth.local.username = req.body.username;
res.locals.user.save(cb);
if (invalidPassword(user, req.body.password)) return cb(invalidPassword(user, req.body.password));
user.auth.local.username = username;
user.auth.local.lowerCaseUsername = lowerCaseUsername;
user.save(cb);
}
], function(err){
if (err) return err.code ? res.json(err.code, err) : next(err);
@@ -283,14 +301,17 @@ api.changeUsername = function(req, res, next) {
}
api.changeEmail = function(req, res, next){
var email = req.body.email && req.body.email.toLowerCase(); // emails are all lowercase
if(!email) return res.json(400, {err: "Email not provided"});
async.waterfall([
function(cb){
User.findOne({'auth.local.email': RegexEscape(req.body.email)}, {auth:1}, cb);
User.findOne({'auth.local.email': email}, {auth:1}, cb);
},
function(found, cb){
if(found) return cb({code:401, err: "Email already taken"});
if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password));
res.locals.user.auth.local.email = req.body.email;
res.locals.user.auth.local.email = email;
res.locals.user.save(cb);
}
], function(err){
@@ -333,7 +354,7 @@ api.getFirebaseToken = function(req, res, next) {
.createToken({
uid: user._id,
isHabiticaUser: true
}, {
}, {
expires: expires
});
+5 -5
View File
@@ -230,7 +230,7 @@ api.update = function(req, res, next){
// before-save / after-save comparison to determine if we need to sync to users
before = _before;
var attrs = _.pick(req.body, 'name shortName description habits dailys todos rewards date'.split(' '));
Challenge.findByIdAndUpdate(cid, {$set:attrs}, cb);
Challenge.findByIdAndUpdate(cid, {$set:attrs}, {new: true}, cb);
},
function(saved, cb) {
@@ -271,7 +271,7 @@ function closeChal(cid, broken, cb) {
function(_removed, cb2) {
removed = _removed;
var pull = {'$pull':{}}; pull['$pull'][_removed._id] = 1;
Group.findByIdAndUpdate(_removed.group, pull);
Group.findByIdAndUpdate(_removed.group, {new: true}, pull);
User.find({_id:{$in: removed.members}}, cb2);
},
function(users, cb2) {
@@ -297,7 +297,7 @@ function closeChal(cid, broken, cb) {
/**
* Delete & close
*/
api['delete'] = function(req, res, next){
api.delete = function(req, res, next){
var user = res.locals.user;
var cid = req.params.cid;
@@ -370,7 +370,7 @@ api.join = function(req, res, next){
async.waterfall([
function(cb) {
Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, cb);
Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, {new: true}, cb);
},
function(chal, cb) {
@@ -403,7 +403,7 @@ api.leave = function(req, res, next){
async.waterfall([
function(cb){
Challenge.findByIdAndUpdate(cid, {$pull:{members:user._id}}, cb);
Challenge.findByIdAndUpdate(cid, {$pull:{members:user._id}}, {new: true}, cb);
},
function(chal, cb){
+8 -4
View File
@@ -521,6 +521,7 @@ api.leave = function(req, res, next) {
group.leave(user, keep, function(err){
if (err) return next(err);
user = group = keep = null;
return res.send(204);
});
};
@@ -683,7 +684,7 @@ api.invite = function(req, res, next){
} else if (req.body.emails) {
inviteByEmails(req.body.emails, group, req, res, next)
} else {
return res.json(400,{err: "Can invite only by email or uuid"});
return res.json(400, {err: "Can only invite by email or uuid"});
}
}
@@ -912,7 +913,8 @@ api.questAccept = function(req, res, next) {
owner: true,
response: 'accept',
gaLabel: 'accept',
questName: key
questName: key,
uuid: user._id,
};
analytics.track('quest',analyticsData);
group.quest.members[m] = true;
@@ -958,7 +960,8 @@ api.questAccept = function(req, res, next) {
owner: false,
response: 'accept',
gaLabel: 'accept',
questName: group.quest.key
questName: group.quest.key,
uuid: user._id,
};
analytics.track('quest',analyticsData);
group.quest.members[user._id] = true;
@@ -977,7 +980,8 @@ api.questReject = function(req, res, next) {
owner: false,
response: 'reject',
gaLabel: 'reject',
questName: group.quest.key
questName: group.quest.key,
uuid: user._id,
};
analytics.track('quest',analyticsData);
group.quest.members[user._id] = false;
+21 -3
View File
@@ -101,9 +101,27 @@ exports.iosVerify = function(req, res, next) {
if (iap.isValidated(appleRes)) {
var purchaseDataList = iap.getPurchaseData(appleRes);
if (purchaseDataList.length > 0) {
if (purchaseDataList[0].productId === 'com.habitrpg.ios.Habitica.20gems') {
//Correct receipt
payments.buyGems({user:user, paymentMethod:'IAP AppleStore'});
var correctReceipt = true;
for (var index in purchaseDataList) {
switch (purchaseDataList[index].productId) {
case 'com.habitrpg.ios.Habitica.4gems':
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 1});
break;
case 'com.habitrpg.ios.Habitica.8gems':
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 2});
break;
case 'com.habitrpg.ios.Habitica.20gems':
case 'com.habitrpg.ios.Habitica.21gems':
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 5.25});
break;
case 'com.habitrpg.ios.Habitica.42gems':
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 10.5});
break;
default:
correctReceipt = false;
}
}
if (correctReceipt) {
var resObj = {
ok: true,
data: appleRes
+2 -1
View File
@@ -137,7 +137,8 @@ exports.cancelSubscription = function(data, cb) {
}
exports.buyGems = function(data, cb) {
var amt = data.gift ? data.gift.gems.amount/4 : 5;
var amt = data.amount || 5;
amt = data.gift ? data.gift.gems.amount/4 : amt;
(data.gift ? data.gift.member : data.user).balance += amt;
data.user.purchased.txnCount++;
if(isProduction) {
+3 -3
View File
@@ -13,14 +13,14 @@ var gcm = gcmApiKey ? pushNotify.gcm({
if(gcm){
gcm.on('transmitted', function (result, message, registrationId) {
console.info("transmitted", result, message, registrationId);
//console.info("transmitted", result, message, registrationId);
});
gcm.on('transmissionError', function (error, message, registrationId) {
console.info("transmissionError", error, message, registrationId);
//console.info("transmissionError", error, message, registrationId);
});
gcm.on('updated', function (result, registrationId) {
console.info("updated", result, registrationId);
//console.info("updated", result, registrationId);
});
}
+2 -2
View File
@@ -13,9 +13,9 @@ api.unsubscribe = function(req, res, next){
if(data._id){
User.update({_id: data._id}, {
$set: {'preferences.emailNotifications.unsubscribeFromAll': true}
}, {multi: false}, function(err, nAffected){
}, {multi: false}, function(err, updateRes){
if(err) return next(err);
if(nAffected !== 1) return res.json(404, {err: 'User not found'});
if(updateRes.n !== 1) return res.json(404, {err: 'User not found'});
res.send('<h1>' + i18n.t('unsubscribedSuccessfully', null, req.language) + '</h1>' + i18n.t('unsubscribedTextUsers', null, req.language));
});
+25 -33
View File
@@ -298,10 +298,9 @@ acceptablePUTPaths = _.reduce(require('./../models/user').schema.paths, function
return m;
}, {})
//// Uncomment this if we we want to disable GP-restoring (eg, holiday events)
//_.each('stats.gp'.split(' '), function(removePath){
// delete acceptablePUTPaths[removePath];
//})
_.each('stats.class'.split(' '), function(removePath){
delete acceptablePUTPaths[removePath];
})
/**
* Update user
@@ -318,7 +317,7 @@ api.update = function(req, res, next) {
if (acceptablePUTPaths[k])
user.fns.dotSet(k, v);
else
errors.push("path `" + k + "` was not saved, as it's a protected path. See https://github.com/HabitRPG/habitrpg/blob/develop/API.md for PUT /api/v2/user.");
errors.push("path `" + k + "` was not saved, as it's a protected path.");
return true;
});
user.save(function(err) {
@@ -365,7 +364,7 @@ api.cron = function(req, res, next) {
// api.reroll // Shared.ops
// api.reset // Shared.ops
api['delete'] = function(req, res, next) {
api.delete = function(req, res, next) {
var user = res.locals.user;
var plan = user.purchased.plan;
@@ -397,38 +396,32 @@ api['delete'] = function(req, res, next) {
/*
------------------------------------------------------------------------
Gems
Development Only Operations
------------------------------------------------------------------------
*/
if (nconf.get('NODE_ENV') === 'development') {
// api.unlock // see Shared.ops
api.addTenGems = function(req, res, next) {
var user = res.locals.user;
api.addTenGems = function(req, res, next) {
var user = res.locals.user;
user.balance += 2.5;
user.save(function(err){
if (err) return next(err);
res.send(204);
})
}
user.balance += 2.5;
/*
------------------------------------------------------------------------
Hourglass
------------------------------------------------------------------------
*/
user.save(function(err){
if (err) return next(err);
res.send(204);
});
};
// api.unlock // see Shared.ops
api.addHourglass = function(req, res, next) {
var user = res.locals.user;
api.addHourglass = function(req, res, next) {
var user = res.locals.user;
user.purchased.plan.consecutive.trinkets += 1;
user.purchased.plan.consecutive.trinkets += 1;
user.save(function(err){
if (err) return next(err);
res.send(204);
})
user.save(function(err){
if (err) return next(err);
res.send(204);
});
};
}
/*
@@ -607,6 +600,7 @@ api.batchUpdate = function(req, res, next) {
return cb(code+": "+ (data.message ? data.message : data.err ? data.err : JSON.stringify(data)));
return cb();
};
if(!api[_req.op]) { return cb(_req.op + ' operation not found'); }
api[_req.op](_req, res, cb);
});
})
@@ -635,9 +629,7 @@ api.batchUpdate = function(req, res, next) {
// Fetch full user object
} else if (response.wasModified){
// Preen 3-day past-completed To-Dos from Angular & mobile app
response.todos = _.where(response.todos, function(t) {
return !t.completed || (t.challenge && t.challenge.id) || moment(t.dateCompleted).isAfter(moment().subtract({days:3}));
});
response.todos = shared.preenTodos(response.todos);
res.json(200, response);
// return only the version number
+11 -8
View File
@@ -1,9 +1,10 @@
var fs = require('fs'),
path = require('path'),
_ = require('lodash'),
User = require('./models/user').model,
shared = require('../../common'),
translations = {};
var fs = require('fs');
var path = require('path');
var _ = require('lodash');
var accepts = require('accepts');
var User = require('./models/user').model;
var shared = require('../../common');
var translations = {};
var localePath = path.join(__dirname, "/../../common/locales/")
@@ -74,7 +75,9 @@ var chineseVersions = ['zh-tw'];
var getUserLanguage = function(req, res, next){
var getFromBrowser = function(){
var acceptable = _(req.acceptedLanguages).map(function(lang){
var acceptedLanguages = accepts(req).languages();
var acceptable = _(acceptedLanguages).map(function(lang){
return lang.slice(0, 2);
}).uniq().value();
@@ -83,7 +86,7 @@ var getUserLanguage = function(req, res, next){
var iAcceptedCompleteLang = (matches.length > 0) ? multipleVersionsLanguages.indexOf(matches[0].toLowerCase()) : -1;
if(iAcceptedCompleteLang !== -1){
var acceptedCompleteLang = _.find(req.acceptedLanguages, function(accepted){
var acceptedCompleteLang = _.find(acceptedLanguages, function(accepted){
return accepted.slice(0, 2) == multipleVersionsLanguages[iAcceptedCompleteLang];
});
+8 -3
View File
@@ -12,8 +12,9 @@ module.exports = function(server,mongoose) {
useAvg = false, // use average over 3 minutes, or simply the last minute's report
url = 'https://api.newrelic.com/v2/applications/'+nconf.get('NEW_RELIC_APPLICATION_ID')+'/metrics/data.json?names[]=Apdex&values[]=score';
setInterval(function(){
// TODO, DISABLED UNTIL NEW RELIC IS ADDED AGAIN
// see https://docs.newrelic.com/docs/apm/apis/api-v2-examples/average-response-time-examples-api-v2, https://rpm.newrelic.com/api/explore/applications/data
request({
/*request({
url: useAvg ? url+'&from='+moment().subtract({minutes:mins}).utc().format()+'&to='+moment().utc().format()+'&summarize=true' : url,
headers: {'X-Api-Key': nconf.get('NEW_RELIC_API_KEY')}
}, function(err, response, body){
@@ -22,8 +23,12 @@ module.exports = function(server,mongoose) {
apdexBad = score < .75 || score == 1,
memory = os.freemem() / os.totalmem(),
memoryHigh = memory < 0.1;
if (/*apdexBad || */memoryHigh) throw '[Memory Leak] Apdex='+score+' Memory='+parseFloat(memory).toFixed(3)+' Time='+moment().format();
});
if (apdexBad || memoryHigh) throw '[Memory Leak] Apdex='+score+' Memory='+parseFloat(memory).toFixed(3)+' Time='+moment().format();
});*/
var memory = os.freemem() / os.totalmem(),
memoryHigh = memory < 0.1;
if (memoryHigh) throw '[Memory Leak] Memory='+parseFloat(memory).toFixed(3)+' Time='+moment().format();
}, mins*60*1000);
}
+5 -6
View File
@@ -6,14 +6,14 @@ var i18n = require('../i18n.js');
var buildManifest = require('../libs/buildManifest');
var shared = require('../../../common');
var forceRefresh = require('./forceRefresh');
var tavern = require('../models/group').tavern;
var tavernQuest = require('../models/group').tavernQuest;
var mods = require('../models/user').mods;
// To avoid stringifying more data then we need,
// items from `env` used on the client will have to be specified in this array
var clientVars = ['language', 'isStaticPage', 'avalaibleLanguages', 'translations',
'FACEBOOK_KEY', 'NODE_ENV', 'BASE_URL', 'GA_ID',
'AMAZON_PAYMENTS', 'STRIPE_PUB_KEY', 'AMPLITUDE_KEY',
'AMAZON_PAYMENTS', 'STRIPE_PUB_KEY', 'AMPLITUDE_KEY',
'worldDmg', 'mods', 'IS_MOBILE'];
var env = {
@@ -21,7 +21,6 @@ var env = {
getBuildUrl: buildManifest.getBuildUrl,
_: _,
clientVars: clientVars,
tavern: tavern, // for world boss
mods: mods,
Content: shared.content,
siteVersion: forceRefresh.siteVersion,
@@ -53,9 +52,9 @@ module.exports = function(req, res, next) {
args.push(language.code);
return shared.i18n.t.apply(null, args);
},
// Defined here and not outside of the middleware because tavern might be an
// Defined here and not outside of the middleware because tavernQuest might be an
// empty object until the query to fetch it finishes
worldDmg: (tavern && tavern.quest && tavern.quest.extra && tavern.quest.extra.worldDmg) || {},
worldDmg: (tavernQuest && tavernQuest.extra && tavernQuest.extra.worldDmg) || {},
});
// Put query-string party (& guild but use partyInvite for backward compatibility)
@@ -68,4 +67,4 @@ module.exports = function(req, res, next) {
}
next();
};
};
+15 -10
View File
@@ -269,12 +269,15 @@ GroupSchema.statics.collectQuest = function(user, progress, cb) {
}
// to set a boss: `db.groups.update({_id:'habitrpg'},{$set:{quest:{key:'dilatory',active:true,progress:{hp:1000,rage:1500}}}})`
module.exports.tavern = {};
module.exports.tavernQuest = {};
var tavernQ = {_id:'habitrpg','quest.key':{$ne:null}};
process.nextTick(function(){
mongoose.model('Group').findOne(tavernQ,function(err,tavern){
// Using _assign so we don't lose the reference to the exported tavern
_.assign(module.exports.tavern, tavern);
mongoose.model('Group').findOne(tavernQ, function(err,tavern){
if (!tavern) return; // No tavern quest
var quest = tavern.quest.toObject();
// Using _assign so we don't lose the reference to the exported tavernQuest
_.assign(module.exports.tavernQuest, quest);
});
});
@@ -291,14 +294,13 @@ GroupSchema.statics.tavernBoss = function(user,progress) {
},
function(tavern,cb){
if (!(tavern && tavern.quest && tavern.quest.key)) return cb(true);
module.exports.tavern = tavern;
var quest = shared.content.quests[tavern.quest.key];
if (tavern.quest.progress.hp <= 0) {
tavern.sendChat(quest.completionChat('en'));
tavern.finishQuest(quest, function(){});
tavern.save(cb);
module.exports.tavern = undefined;
_.assign(module.exports.tavernQuest, {extra: null});
} else {
// Deal damage. Note a couple things here, str & def are calculated. If str/def are defined in the database,
// use those first - which allows us to update the boss on the go if things are too easy/hard.
@@ -308,8 +310,7 @@ GroupSchema.statics.tavernBoss = function(user,progress) {
if (tavern.quest.progress.rage >= quest.boss.rage.value) {
if (!tavern.quest.extra.worldDmg) tavern.quest.extra.worldDmg = {};
var wd = tavern.quest.extra.worldDmg;
// var scene = wd.tavern ? wd.stables ? wd.market ? false : 'market' : 'stables' : 'tavern'; // Dilatory attacks tavern, stables, market
var scene = wd.stables ? wd.bailey ? wd.guide ? false : 'guide' : 'bailey' : 'stables'; // Stressbeast attacks stables, Bailey, Justin
var scene = wd.quests ? wd.seasonalShop ? wd.tavern ? false : 'tavern' : 'seasonalShop' : 'quests'; // Burnout attacks Ian, Seasonal Sorceress, tavern
if (!scene) {
tavern.sendChat('`'+quest.boss.name('en')+' tries to unleash '+quest.boss.rage.title('en')+', but is too tired.`');
tavern.quest.progress.rage = 0 //quest.boss.rage.value;
@@ -319,16 +320,20 @@ GroupSchema.statics.tavernBoss = function(user,progress) {
tavern.quest.extra.worldDmg.recent = scene;
tavern.markModified('quest.extra.worldDmg');
tavern.quest.progress.rage = 0;
tavern.quest.progress.hp += (quest.boss.rage.healing * tavern.quest.progress.hp);
if (quest.boss.rage.healing) {
tavern.quest.progress.hp += (quest.boss.rage.healing * tavern.quest.progress.hp);
}
}
}
if ((tavern.quest.progress.hp < quest.boss.desperation.threshold) && !tavern.quest.extra.desperate) {
if (quest.boss.desperation && (tavern.quest.progress.hp < quest.boss.desperation.threshold) && !tavern.quest.extra.desperate) {
tavern.sendChat(quest.boss.desperation.text('en'));
tavern.quest.extra.desperate = true;
tavern.quest.extra.def = quest.boss.desperation.def;
tavern.quest.extra.str = quest.boss.desperation.str;
tavern.markModified('quest.extra');
}
_.assign(module.exports.tavernQuest, tavern.quest.toObject());
tavern.save(cb);
}
}
+27 -3
View File
@@ -66,7 +66,8 @@ var UserSchema = new Schema({
email: String,
hashed_password: String,
salt: String,
username: String
username: String,
lowerCaseUsername: String // Store a lowercase version of username to check for duplicates
},
timestamps: {
created: {type: Date,'default': Date.now},
@@ -138,6 +139,29 @@ var UserSchema = new Schema({
hall: {type: Number, 'default': -1},
equipment: {type: Number, 'default': -1}
},
tutorial: {
common: {
habits: {type: Boolean, 'default': false},
dailies: {type: Boolean, 'default': false},
todos: {type: Boolean, 'default': false},
rewards: {type: Boolean, 'default': false},
party: {type: Boolean, 'default': false},
pets: {type: Boolean, 'default': false},
gems: {type: Boolean, 'default': false},
skills: {type: Boolean, 'default': false},
classes: {type: Boolean, 'default': false},
tavern: {type: Boolean, 'default': false},
equipment: {type: Boolean, 'default': false},
items: {type: Boolean, 'default': false},
},
ios: {
addTask: {type: Boolean, 'default': false},
editTask: {type: Boolean, 'default': false},
deleteTask: {type: Boolean, 'default': false},
filterTask: {type: Boolean, 'default': false},
groupPets: {type: Boolean, 'default': false},
}
},
dropsEnabled: {type: Boolean, 'default': false},
itemsEnabled: {type: Boolean, 'default': false},
newStuff: {type: Boolean, 'default': false},
@@ -171,7 +195,6 @@ var UserSchema = new Schema({
todos: Array //[{data: Date, value: Number}] // big peformance issues if these are defined
},
// FIXME remove?
invitations: {
guilds: {type: Array, 'default': []},
party: Schema.Types.Mixed
@@ -324,6 +347,7 @@ var UserSchema = new Schema({
language: String,
automaticAllocation: Boolean,
allocationMode: {type:String, enum: ['flat','classbased','taskbased'], 'default': 'flat'},
autoEquip: {type: Boolean, 'default': true},
costume: Boolean,
dateFormat: {type: String, enum:['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'], 'default': 'MM/dd/yyyy'},
sleep: {type: Boolean, 'default': false},
@@ -579,7 +603,7 @@ UserSchema.methods.unlink = function(options, cb) {
module.exports.schema = UserSchema;
module.exports.model = mongoose.model("User", UserSchema);
// Initially export an empty object so external requires will get
// Initially export an empty object so external requires will get
// the right object by reference when it's defined later
// Otherwise it would remain undefined if requested before the query executes
module.exports.mods = [];
+1 -1
View File
@@ -155,7 +155,7 @@ router.post('/user/tasks/:id/:direction', auth.auth, i18n.getUserLanguage, cron,
// Tasks
router.get('/user/tasks', auth.auth, i18n.getUserLanguage, cron, api.getTasks);
router.get('/user/task/:id', auth.auth, i18n.getUserLanguage, cron, api.getTask);
router["delete"]('/user/task/:id', auth.auth, i18n.getUserLanguage, cron, api.deleteTask);
router.delete('/user/task/:id', auth.auth, i18n.getUserLanguage, cron, api.deleteTask);
router.post('/user/task', auth.auth, i18n.getUserLanguage, cron, api.addTask);
// User
+21 -3
View File
@@ -265,7 +265,7 @@ module.exports = (swagger, v2) ->
method: 'DELETE'
description: "Delete a user object entirely, USE WITH CAUTION!"
middleware: [auth.auth, i18n.getUserLanguage]
action: user["delete"]
action: user.delete
"/user/revive":
spec:
@@ -347,8 +347,19 @@ module.exports = (swagger, v2) ->
action: user.batchUpdate
# Tags
"/user/tags":
"/user/tags/{id}:GET":
spec:
path: '/user/tags/{id}'
method: 'GET'
description: "Get a tag"
parameters: [
path 'id','The id of the tag to get','string'
]
action: user.getTag
"/user/tags:POST":
spec:
path: "/user/tags"
method: 'POST'
description: 'Create a new tag'
parameters: [
@@ -356,6 +367,13 @@ module.exports = (swagger, v2) ->
]
action: user.addTag
"/user/tags:GET":
spec:
path: "/user/tags"
method: 'GET'
description: 'List all of a user\'s tags'
action: user.getTags
"/user/tags/sort":
spec:
method: 'POST'
@@ -756,7 +774,7 @@ module.exports = (swagger, v2) ->
description: "Delete a challenge"
parameters: [path('cid','Challenge id','string')]
middleware: [auth.auth, i18n.getUserLanguage]
action: challenges["delete"]
action: challenges.delete
"/challenges/{cid}/close":
spec:
+58 -41
View File
@@ -1,5 +1,5 @@
// Only do the minimal amount of work before forking just in case of a dyno restart
var cluster = require("cluster");
var cluster = require('cluster');
var _ = require('lodash');
var nconf = require('nconf');
var utils = require('./utils');
@@ -7,11 +7,14 @@ utils.setupConfig();
var logging = require('./logging');
var isProd = nconf.get('NODE_ENV') === 'production';
var isDev = nconf.get('NODE_ENV') === 'development';
var cores = +nconf.get("WEB_CONCURRENCY") || 0;
var DISABLE_LOGGING = nconf.get('DISABLE_REQUEST_LOGGING');
var cores = +nconf.get('WEB_CONCURRENCY') || 0;
if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
// Fork workers. If config.json has CORES=x, use that - otherwise, use all cpus-1 (production)
_.times(cores, cluster.fork);
for (var i = 0, n = cores; i < n; i += 1) {
cluster.fork();
}
cluster.on('disconnect', function(worker, code, signal) {
var w = cluster.fork(); // replace the dead worker
@@ -20,10 +23,10 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
} else {
require('coffee-script'); // remove this once we've fully converted over
var express = require("express");
var http = require("http");
var path = require("path");
var swagger = require("swagger-node-express");
var express = require('express');
var http = require('http');
var path = require('path');
var swagger = require('swagger-node-express');
var autoinc = require('mongoose-id-autoinc');
var shared = require('../../common');
@@ -76,8 +79,8 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
// This auth strategy is no longer used. It's just kept around for auth.js#loginFacebook() (passport._strategies.facebook.userProfile)
// The proper fix would be to move to a general OAuth module simply to verify accessTokens
passport.use(new FacebookStrategy({
clientID: nconf.get("FACEBOOK_KEY"),
clientSecret: nconf.get("FACEBOOK_SECRET"),
clientID: nconf.get('FACEBOOK_KEY'),
clientSecret: nconf.get('FACEBOOK_SECRET'),
//callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback"
},
function(accessToken, refreshToken, profile, done) {
@@ -86,61 +89,75 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
));
// ------------ Server Configuration ------------
var publicDir = path.join(__dirname, "/../public");
var publicDir = path.join(__dirname, '/../public');
app.set("port", nconf.get('PORT'));
app.set('port', nconf.get('PORT'));
require('./middlewares/apiThrottle')(app);
app.use(require('./middlewares/domain')(server,mongoose));
if (!isProd) app.use(express.logger("dev"));
app.use(express.compress());
app.set("views", __dirname + "/../views");
app.set("view engine", "jade");
app.use(express.favicon(publicDir + '/favicon.ico'));
if (!isProd && !DISABLE_LOGGING) app.use(require('morgan')('dev'));
app.use(require('compression')());
app.set('views', __dirname + '/../views');
app.set('view engine', 'jade');
app.use(require('serve-favicon')(publicDir + '/favicon.ico'));
app.use(require('./middlewares/cors'));
var redirects = require('./middlewares/redirects');
app.use(redirects.forceHabitica);
app.use(redirects.forceSSL);
app.use(express.urlencoded());
app.use(express.json());
var bodyParser = require('body-parser');
// Default limit is 100kb, need that because we actually send whole groups to the server
// FIXME as soon as possible (need to move on the client from $resource -> $http)
app.use(bodyParser.urlencoded({
limit: '1mb',
parameterLimit: 10000, // Upped for safety from 1k, FIXME as above
extended: true // Uses 'qs' library as old connect middleware
}));
app.use(bodyParser.json({
limit: '1mb'
}));
app.use(require('method-override')());
//app.use(express.cookieParser(nconf.get('SESSION_SECRET')));
app.use(express.cookieParser());
app.use(express.cookieSession({ secret: nconf.get('SESSION_SECRET'), httpOnly: false, cookie: { maxAge: TWO_WEEKS }}));
//app.use(express.session());
app.use(require('cookie-parser')());
app.use(require('cookie-session')({
name: 'connect:sess', // Used to keep backward compatibility with Express 3 cookies
secret: nconf.get('SESSION_SECRET'),
httpOnly: false,
maxAge: TWO_WEEKS
}));
// Initialize Passport! Also use passport.session() middleware, to support
// persistent login sessions (recommended).
app.use(passport.initialize());
app.use(passport.session());
app.use(app.router);
// Custom Directives
app.use(require('./routes/pages'));
app.use(require('./routes/payments'));
app.use(require('./routes/auth'));
app.use(require('./routes/coupon'));
app.use(require('./routes/unsubscription'));
var v2 = express();
app.use('/api/v2', v2);
app.use('/api/v1', require('./routes/apiv1'));
app.use('/export', require('./routes/dataexport'));
require('./routes/apiv2.coffee')(swagger, v2);
var maxAge = isProd ? 31536000000 : 0;
// Cache emojis without copying them to build, they are too many
app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge }));
app.use('/common/dist', express['static'](publicDir + "/../../common/dist", { maxAge: maxAge }));
app.use('/common/audio', express['static'](publicDir + "/../../common/audio", { maxAge: maxAge }));
app.use('/common/script/public', express['static'](publicDir + "/../../common/script/public", { maxAge: maxAge }));
app.use('/common/img', express['static'](publicDir + "/../../common/img", { maxAge: maxAge }));
app.use(express['static'](publicDir));
app.use(express.static(path.join(__dirname, '/../build'), { maxAge: maxAge }));
app.use('/common/dist', express.static(publicDir + '/../../common/dist', { maxAge: maxAge }));
app.use('/common/audio', express.static(publicDir + '/../../common/audio', { maxAge: maxAge }));
app.use('/common/script/public', express.static(publicDir + '/../../common/script/public', { maxAge: maxAge }));
app.use('/common/img', express.static(publicDir + '/../../common/img', { maxAge: maxAge }));
app.use(express.static(publicDir));
// Custom Directives
app.use(require('./routes/pages').middleware);
app.use(require('./routes/payments').middleware);
app.use(require('./routes/auth').middleware);
app.use(require('./routes/coupon').middleware);
app.use(require('./routes/unsubscription').middleware);
var v2 = express();
app.use('/api/v2', v2);
app.use('/api/v1', require('./routes/apiv1').middleware);
app.use('/export', require('./routes/dataexport').middleware);
require('./routes/apiv2.coffee')(swagger, v2);
app.use(require('./middlewares/errorHandler'));
server.on('request', app);
server.listen(app.get("port"), function() {
return logging.info("Express server listening on port " + app.get("port"));
server.listen(app.get('port'), function() {
return logging.info('Express server listening on port ' + app.get('port'));
});
module.exports = server;
+13 -5
View File
@@ -159,6 +159,10 @@ module.exports.makeSalt = function() {
return crypto.randomBytes(Math.ceil(len / 2)).toString('hex').substring(0, len);
}
// Prepare to export analytics object
// Export emoty methods until the right ones are ready
module.exports.analytics = { track: function() { }, trackPurchase: function() { } };
/**
* Load nconf and define default configuration values if config.json or ENV vars are not found
*/
@@ -170,8 +174,8 @@ module.exports.setupConfig = function(){
if (nconf.get('NODE_ENV') === "development")
Error.stackTraceLimit = Infinity;
//if (nconf.get('NODE_ENV') === 'production')
// require('newrelic');
if (nconf.get('NODE_ENV') === 'production')
require('newrelic');
isProd = nconf.get('NODE_ENV') === 'production';
baseUrl = nconf.get('BASE_URL');
@@ -182,9 +186,13 @@ module.exports.setupConfig = function(){
googleAnalytics: nconf.get('GA_ID')
}
module.exports.analytics = analytics
? analytics(analyticsTokens)
: { track: function() { }, trackPurchase: function() { } };
if(analytics){
analytics = analytics(analyticsTokens);
// Use the right analytics methods, don't substitute the entire object
// or all the require() across the code will keep the empty methods
module.exports.analytics.track = analytics.track;
module.exports.analytics.trackPurchase = analytics.trackPurchase;
}
};
var algorithm = 'aes-256-ctr';
@@ -4,6 +4,13 @@
h3.equipment-title.hint(popover-trigger='mouseenter',
popover-placement='right', popover-append-to-body='true',
popover=env.t('battleGearText'))=env.t('battleGear')
.checkbox.equipment-title
label
input(type="checkbox", ng-model="user.preferences.autoEquip",
ng-change='set({"preferences.autoEquip":user.preferences.autoEquip ? true : false})')
| &nbsp;
=env.t('autoEquipBattleGear')
div
button.btn.btn-default(type="button", ng-click='dequip("battleGear");') {{env.t("unequipBattleGear")}}
+3 -2
View File
@@ -4,12 +4,13 @@ include ../../shared/mixins
.row
.col-md-6
.clearfix
.npc_ian.pull-left-sm.col-centered
div(class="#{env.worldDmg.quests ? 'npc_ian_broken' : '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')
p(ng-if='env.worldDmg.quests')=env.t('ianBrokenText')
p(ng-if='!env.worldDmg.quests')=env.t('ianText')
h3.equipment-title=env.t('yourQuests')
+ownedQuests(true,'right')
@@ -1,11 +1,12 @@
.container-fluid
.stable.row: .col-xs-12
.seasonalshop_open.pull-left-sm.col-centered
div(class="#{env.worldDmg.seasonalShop ? 'seasonalshop_broken' : 'seasonalshop_open'}").pull-left-sm.col-centered
.popover.static-popover.fade.right.in.pull-left-sm.col-centered
.arrow.hidden-xs
h3.popover-title!=env.t('seasonalShopTitle', {linkStart:"<a href='http://blog.habitrpg.com/who' target='_blank'>", linkEnd: "</a>"})
.popover-content
p!=env.t('seasonalShopFallText')
p(ng-if='!env.worldDmg.seasonalShop')!=env.t('seasonalShopFallText')
p(ng-if='env.worldDmg.seasonalShop')!=env.t('seasonalShopFallTextBroken')
.well(ng-if='User.user.achievements.rebirths > 0')=env.t('seasonalShopRebirth')
@@ -12,7 +12,7 @@
.pull-right
span.inventory_special_trinket.inline-gems
b x{{user.purchased.plan.consecutive.trinkets}}
p!=env.t('timeTravelersPopover', {linkStart: "<a href='http://habitica.wikia.com/wiki/Mystery_Item' target='_blank'>", linkEnd: "</a>"})
p!=env.t('timeTravelersPopover')
.row: .col-md-7
.alert.alert-info(ng-if='hasAllTimeTravelerItems()')=env.t('timeTravelersAlreadyOwned')
+22 -22
View File
@@ -103,19 +103,19 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
br.visible-xs
button.btn.btn-block.btn-primary(ng-click='openDayStartModal(dayStart)',
ng-disabled='dayStart == user.preferences.dayStart')
| Save Custom Day Start
=env.t('saveCustomDayStart')
.personal-options.col-md-6
.panel.panel-default
.panel-heading
span Registration
span=env.t('registration')
.panel-body
div(ng-if='user.auth.facebook.id')
button.btn.btn-primary(disabled='disabled', ng-if='!user.auth.local.username')=env.t('registeredWithFb')
button.btn.btn-danger(ng-click='http("delete","/api/v2/user/auth/social",null,"detachedFacebook")', ng-if='user.auth.local.username')=env.t('detachFacebook')
hr
div(ng-if='!user.auth.local.username')
p Add local authentication:
p=env.t('addLocalAuth')
form(ng-submit='http("post","/api/v2/register",localAuth,"addedLocalAuth")', ng-init='localAuth={}', name='localAuth', novalidate)
//-.alert.alert-danger(ng-messages='changeUsername.$error && changeUsername.submitted')=env.t('fillAll')
.form-group
@@ -189,15 +189,15 @@ script(type='text/ng-template', id='partials/options.settings.promo.html')
small= env.t('couponText')
div(ng-if='user.contributor.sudo')
hr
h4 Generate Codes
h4=env.t('generateCodes')
form.form(role='form',ng-submit='generateCodes(_codes)',ng-init='_codes={}')
.form-group
input.form-control(type='text',ng-model='_codes.event',placeholder="Event code (eg, 'wondercon')")
.form-group
input.form-control(type='number',ng-model='_codes.count',placeholder="Number of codes to generate (eg, 250)")
.form-group
button.btn.btn-primary(type='submit') Generate
a.btn.btn-default(href='/api/v2/coupons?_id={{user._id}}&apiToken={{user.apiToken}}') Get Codes
button.btn.btn-primary(type='submit')=env.t('generate')
a.btn.btn-default(href='/api/v2/coupons?_id={{user._id}}&apiToken={{user.apiToken}}')=env.t('getCodes')
script(type='text/ng-template', id='partials/options.settings.api.html')
.container-fluid
@@ -214,12 +214,12 @@ script(type='text/ng-template', id='partials/options.settings.api.html')
hr
h2 Webhooks
h2=env.t('webhooks')
table.table.table-striped
thead(ng-if='hasWebhooks')
tr
th Enabled
th Webhook URL
th=env.t('enabled')
th=env.t('webhookURL')
th
tbody
tr(ng-repeat="webhook in user.preferences.webhooks | toArray:true | orderBy:'sort'")
@@ -235,9 +235,9 @@ script(type='text/ng-template', id='partials/options.settings.api.html')
td(colspan=2)
form.form-horizontal(ng-submit='addWebhook(_newWebhook.url)')
.form-group.col-sm-10
input.form-control(type='url', ng-model='_newWebhook.url', placeholder='Webhook URL')
input.form-control(type='url', ng-model='_newWebhook.url', placeholder=env.t('webhookURL'))
.col-sm-2
button.btn.btn-sm.btn-primary(type='submit') Add
button.btn.btn-sm.btn-primary(type='submit')=env.t('add')
script(id='partials/options.settings.export.html', type="text/ng-template")
.container-fluid
@@ -258,7 +258,7 @@ mixin subPerks()
tr
td
span.hint(popover=env.t('buyGemsGoldText', {gemCost: "{{Shared.planGemLimits.convRate}}", gemLimit: "{{Shared.planGemLimits.convCap}}"}),popover-trigger='mouseenter',popover-placement='right') #{env.t('buyGemsGold')}&nbsp;
span.badge.badge-success(ng-show='_subscription.key!="basic_earned"') Cap raised to {{ [25 + user.purchased.plan.consecutive.gemCapExtra + Math.floor(Content.subscriptionBlocks[_subscription.key].months/3*5), 50] | min }}
span.badge.badge-success(ng-show='_subscription.key!="basic_earned"')=env.t('buyGemsGoldCap', {amount: '{{:: gemGoldCap(_subscription) | min }}'})
tr
td
span.hint(popover=env.t('retainHistoryText'),popover-trigger='mouseenter',popover-placement='right')=env.t('retainHistory')
@@ -269,8 +269,8 @@ mixin subPerks()
td
span.hint(popover=env.t('mysteryItemText'),popover-trigger='mouseenter',popover-placement='right') #{env.t('mysteryItem')}&nbsp;
div(ng-show='_subscription.key!="basic_earned"')
.badge.badge-success +{{Math.floor(Content.subscriptionBlocks[_subscription.key].months/3)}} Mystic Hourglass
.small.muted Mystic Hourglasses allow purchasing a previous month's Mystery Item set.
.badge.badge-success=env.t('mysticHourglass', {amount: '+{{:: numberOfMysticHourglasses(_subscription)}}'})
.small.muted=env.t('mysticHourglassText')
tr
td
span.hint(popover=env.t('supportDevsText'),popover-trigger='mouseenter',popover-placement='right')=env.t('supportDevs')
@@ -324,17 +324,17 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template')
| #{env.t('subCanceled')} <strong>{{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
tr(ng-if='!user.purchased.plan.dateTerminated'): td
h4=env.t('subscribed')
p(ng-if='user.purchased.plan.planId') Recurring ${{Content.subscriptionBlocks[user.purchased.plan.planId].price}} each {{Content.subscriptionBlocks[user.purchased.plan.planId].months}} Month(s) ({{user.purchased.plan.paymentMethod}})
p(ng-if='user.purchased.plan.planId')=env.t('purchasedPlanId', {price: '{{Content.subscriptionBlocks[user.purchased.plan.planId].price}}', months: '{{Content.subscriptionBlocks[user.purchased.plan.planId].months}}', plan: '{{user.purchased.plan.paymentMethod}}'})
tr(ng-if='user.purchased.plan.extraMonths'): td
span.glyphicon.glyphicon-credit-card
| You have {{user.purchased.plan.extraMonths | number:2}} months of subscription credit.
| &nbsp;#{env.t('purchasedPlanExtraMonths', {months: '{{user.purchased.plan.extraMonths | number:2}}'})}
tr(ng-if='user.purchased.plan.consecutive.count || user.purchased.plan.consecutive.offset'): td
span.glyphicon.glyphicon-forward
| &nbsp;Consecutive Subscription
| &nbsp;#{env.t('consecutiveSubscription')}
ul.list-unstyled
li Consecutive Months: {{user.purchased.plan.consecutive.count + user.purchased.plan.consecutive.offset}}
li Gem Cap Extra: {{user.purchased.plan.consecutive.gemCapExtra}}
li Mystic Hourglasses: {{user.purchased.plan.consecutive.trinkets}}
li #{env.t('consecutiveMonths')} {{user.purchased.plan.consecutive.count + user.purchased.plan.consecutive.offset}}
li #{env.t('gemCapExtra')} {{user.purchased.plan.consecutive.gemCapExtra}}
li #{env.t('mysticHourglasses')} {{user.purchased.plan.consecutive.trinkets}}
div(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')
.form-group
.radio(ng-repeat='block in Content.subscriptionBlocks | toArray | omit: "discount==true" | orderBy:"months"')
@@ -355,8 +355,8 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template')
h3(ng-if='(user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')= env.t("resubscribe")
a.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon})', ng-disabled='!_subscription.key')= env.t('card')
a.btn.btn-success(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', ng-disabled='!_subscription.key') PayPal
a.btn.btn-warning(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon})") Amazon Payments
a.btn.btn-success(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', ng-disabled='!_subscription.key')=env.t('paypal')
a.btn.btn-warning(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon})")=env.t('amazonPayments')
div(ng-if='user.purchased.plan.customerId')
.btn.btn-primary(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit()')=env.t('subUpdateCard')
.btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription()')=env.t('cancelSub')
+6 -4
View File
@@ -186,11 +186,13 @@ script(type='text/ng-template', id='partials/options.social.challenges.html')
| {{challenge.group.name}}
a(ui-sref="options.social.party" ng-if="challenge.group.type=='party'")
| {{challenge.group.name}}
li
li(ng-if='challenge.leader.profile.name')
=env.t('by') + ' '
a(ng-click='clickMember(challenge.leader._id, true)') {{challenge.leader.profile.name}}
li
=env.t('participants', {membercount: "{{challenge.memberCount}}"})
a(ng-click='clickMember(challenge.leader._id, true)') {{::challenge.leader.profile.name}}
li(ng-if='!challenge.leader.profile.name',
popover=env.t('noChallengeOwnerPopover'),
popover-trigger='mouseenter')=env.t('noChallengeOwner')
li=env.t('participants', {membercount: "{{challenge.memberCount}}"})
li(ng-show='challenge.prize')
p!=env.t('prizeValue', {gemcount: "{{challenge.prize}}", gemicon: "<span class='inline-block Pet_Currency_Gem1x'></span>"})
li.bg-transparent
+1 -4
View File
@@ -1,8 +1,5 @@
div.chat-form.guidelines-not-accepted(ng-if='!user.flags.communityGuidelinesAccepted')
p If you would like to post messages in the Tavern or any party or guild chat, please first read our
|&nbsp;
a(target='_blank', href='/static/community-guidelines')=env.t('communityGuidelines')
|&nbsp;and then click the button below to indicate that you accept them.
p!=env.t('requestAcceptGuidelines', {linkStart: '<a target="_blank", href="/static/community-guidelines">', linkEnd: '</a>'})
.chat-controls
div
button.btn.btn-warning(ng-click='acceptCommunityGuidelines()')=env.t('iAcceptCommunityGuidelines')
@@ -8,17 +8,17 @@ else
.meter-label(tooltip=env.t('bossHP'))
span.glyphicon.glyphicon-heart
.meter.health
.bar(ng-style='{width: Shared.percent(progress.hp, boss.hp) + "%"}')
.bar(ng-style='{width: Shared.percent(group.quest.progress.hp, Content.quests[group.quest.key].boss.hp) + "%"}')
span.meter-text.value
| {{Math.ceil(progress.hp) | roundLargeNumbers}}
| &nbsp;/ {{::boss.hp | roundLargeNumbers}}
div(ng-if='boss.rage')
.meter-label(tooltip='Rage')
| {{Math.ceil(group.quest.progress.hp) | roundLargeNumbers}}
| &nbsp;/ {{::Content.quests[group.quest.key].boss.hp | roundLargeNumbers}}
div(ng-if='Content.quests[group.quest.key].boss.rage')
.meter-label(tooltip=env.t('rage'))
span.glyphicon.glyphicon-fire
.meter.mana(popover="{{::boss.rage.description()}}",
popover-title="{{::boss.rage.title()}}",
.meter.mana(popover="{{::Content.quests[group.quest.key].boss.rage.description()}}",
popover-title="{{::Content.quests[group.quest.key].boss.rage.title()}}",
popover-trigger='mouseenter', popover-placement='bottom')
.bar(ng-style='{width: Shared.percent(progress.rage, boss.rage.value) + "%"}')
.bar(ng-style='{width: Shared.percent(group.quest.progress.rage, Content.quests[group.quest.key].boss.rage.value) + "%"}')
span.meter-text.value
| {{Math.ceil(progress.rage) | roundLargeNumbers}}
| &nbsp;/ {{::boss.rage.value | roundLargeNumbers}}
| {{Math.ceil(group.quest.progress.rage) | roundLargeNumbers}}
| &nbsp;/ {{::Content.quests[group.quest.key].boss.rage.value | roundLargeNumbers}}
@@ -1,14 +1,19 @@
.npc_ian.pull-left
div(class="#{env.worldDmg.quests ? 'npc_ian_broken' : 'npc_ian'}").pull-left
div(ng-if='::Content.quests[group.quest.key].boss')
if tavern
p!=env.t('tavernBossInfo')
p(ng-if='!env.worldDmg.quests')!=env.t('tavernBossInfo')
p(ng-if='env.worldDmg.quests')!=env.t('tavernBossInfoBroken')
else
p!=env.t('bossDmg1')
p(ng-if='!env.worldDmg.quests')!=env.t('bossDmg1')
p(ng-if='env.worldDmg.quests')!=env.t('bossDmg1Broken')
br
p=env.t('bossDmg2')
p(ng-if='!env.worldDmg.quests')=env.t('bossDmg2')
p(ng-if='env.worldDmg.quests')=env.t('bossDmg2Broken')
div(ng-if='::Content.quests[group.quest.key].collect')
p=env.t('bossColl1')
p(ng-if='!env.worldDmg.quests')=env.t('bossColl1')
p(ng-if='env.worldDmg.quests')=env.t('bossColl1Broken')
br
p=env.t('bossColl2')
p(ng-if='!env.worldDmg.quests')=env.t('bossColl2')
p(ng-if='env.worldDmg.quests')=env.t('bossColl2Broken')
@@ -9,12 +9,15 @@ div(ng-if='group.quest.active===true')
include ./collectionStats
div(ng-bind-html='::Content.quests[group.quest.key].notes()')
unless tavern
quest-rewards(key='{{::group.quest.key}}')
tab(heading=env.t('questParticipants'))
+participants(true)
if tavern
include ./bossStats
div(ng-bind-html='::Content.quests[group.quest.key].notes()')
hr
include ./ianQuestInfo
@@ -18,8 +18,9 @@ div(ng-if='group.quest.active===false')
hr
.npc_ian.pull-left
p=env.t('questStart')
div(class="#{env.worldDmg.quests ? 'npc_ian_broken' : 'npc_ian'}").pull-left
p(ng-if='!env.worldDmg.quests')=env.t('questStart')
p(ng-if='env.worldDmg.quests')=env.t('questStartBroken')
span(ng-if='user.party.quest.RSVPNeeded')
button.btn.btn-sm.btn-success(ng-click='questAccept()')=env.t('accept')
+4 -2
View File
@@ -14,10 +14,12 @@
h3.popover-title
a(target='_blank', href='http://www.kickstarter.com/profile/2014640723')=env.t('daniel')
.popover-content
| {{user.preferences.sleep ? env.t('innText',{name: user.profile.name}) : env.t('danielText')}}
span(ng-if='!env.worldDmg.tavern') {{user.preferences.sleep ? env.t('innText',{name: user.profile.name}) : env.t('danielText')}}
span(ng-if='env.worldDmg.tavern') {{user.preferences.sleep ? env.t('innTextBroken',{name: user.profile.name}) : env.t('danielTextBroken')}}
button.btn-block.btn.btn-lg.btn-success(ng-click='User.user.ops.sleep({})')
| {{user.preferences.sleep ? env.t('innCheckOut') : env.t('innCheckIn')}}
span(ng-if='!user.preferences.sleep')=env.t('danielText2')
span(ng-if='!user.preferences.sleep && !env.worldDmg.tavern')=env.t('danielText2')
span(ng-if='!user.preferences.sleep && env.worldDmg.tavern')=env.t('danielText2Broken')
// Resources
+6 -2
View File
@@ -1,4 +1,5 @@
footer.footer(ng-controller='FooterCtrl')
div(id='fb-root')
.container
.row
.col-sm-3
@@ -63,16 +64,19 @@ footer.footer(ng-controller='FooterCtrl')
table
tr
td
a.addthis_button_facebook_like(fb:like:layout='button_count')
.fb-like(data-href='https://habitica.com/static/front', data-layout='button', data-action='like', data-share='true')
tr
td
a.twitter-share-button(href='https://twitter.com/intent/tweet?text=Improve+yourself+in+the+land+of+Habitica!&via=habitica&url=https://habitica.com/&count=none')=env.t('tweet')
tr
td
iframe(src='/bower_components/github-buttons/github-btn.html?user=habitrpg&repo=habitrpg&type=watch&count=true', allowtransparency='true', frameborder='0', scrolling='0', width='85px', height='20px')
a.tumblr-share-button(data-href='https://habitica.com/static/front', data-notes='none')
tr
td
a.addthis_button_google_plusone(g:plusone:size='medium')
tr
td
iframe(src='/bower_components/github-buttons/github-btn.html?user=habitrpg&repo=habitrpg&type=watch&count=true', allowtransparency='true', frameborder='0', scrolling='0', width='85px', height='20px')
else if (env.NODE_ENV==='development' || env.NODE_ENV==='test') && !env.isStaticPage
h4 Debug
.btn-group-vertical
+5 -3
View File
@@ -38,9 +38,11 @@ script(type='text/ng-template', id='modals/showQuest.html')
.modal-body
+questInfo
hr
.npc_ian.pull-left
p=env.t('questSend')
p=env.t('questWarning')
div(class="#{env.worldDmg.quests ? 'npc_ian_broken' : 'npc_ian'}").pull-left
p(ng-if='!env.worldDmg.quests')=env.t('questSend')
p(ng-if='!env.worldDmg.quests')=env.t('questWarning')
p(ng-if='env.worldDmg.quests')=env.t('questSendBroken')
p(ng-if='env.worldDmg.quests')=env.t('questWarningBroken')
.modal-footer
button.btn.btn-default(ng-click='closeQuest(); $close()')=env.t('cancel')
button.btn.btn-primary(ng-click='questInit()')
+1 -1
View File
@@ -9,7 +9,7 @@ script(type='text/ng-template', id='modals/reroll.html')
.modal-footer
button.btn.btn-default(ng-click='$close()')=env.t('neverMind')
span(ng-if='user.balance < 1')
a.btn.btn-success(ng-click='openModal("buyGems",{track:{"Gems > Reroll"}})')=env.t('buyMoreGems')
a.btn.btn-success(ng-click='openModal("buyGems",{track:"Gems > Reroll"})')=env.t('buyMoreGems')
span.gem-cost=env.t('notEnoughGems')
span(ng-if='user.balance >= 1', ng-controller='SettingsCtrl')
a.btn.btn-danger(ng-click='$close(); reroll()')=env.t('fortify')
+111 -17
View File
@@ -1,32 +1,126 @@
h2 10/5/2015 - OCTOBER BACKGROUNDS REVEALED, COSTUME CHALLENGE BEGINS, AND FALL PLOT-LINE CONTINUES
h2 10/27/2015 - BURNOUT STRIKES AGAIN! PLUS, SPOOKY POTIONS VANISHING SOON
hr
tr
td
.background_harvest_moon.pull-right
h3 October Backgrounds Revealed
p There are three new avatar backgrounds in the <a href='/#/options/profile/backgrounds'>Background Shop!</a> Now your avatar can slog through a Slimy Swamp, cackle under the Harvest Moon, or shiver in the Swarming Darkness!
p.small.muted by Leephon, midori88, and Draayder
.npc_daniel_broken.pull-right
h3 World Boss: Burnout Uses Exhaust Strike!
p Many Habiticans have been hiding from Burnout in the Tavern, but no longer! With a screeching howl, Burnout rakes the Tavern with its white-hot hands. As the Tavern patrons flee, Daniel is caught in Burnout's grip, and transforms into an Exhaust Spirit right in front of you!
br
p This hot-headed horror has gone on for too long. Don't give up... we're so close to vanquishing Burnout for once and for all!
br
p Late to the fight? Learn about Burnout and how to defeat World Bosses <a href='http://habitica.wikia.com/wiki/Boss#World_Bosses' target='_blank'>here</a>.
tr
td
h3 Costume Challenge Begins
p The Community Costume Challenge has begun! Between now and October 31st, dress up as your avatar in real life and post a photo on social media to get the coveted Costume Challenge badge! Read the full rules on the Challenge page <a href='/#/options/groups/challenges/0b3d1116-89d3-4025-8a9e-593bc28e9cff'>here</a>.
p.small.muted by Lemoness
tr
td
h3 Fall Plot-Line Continues
p Although we are now well into autumn, the Flourishing Fields are locked in a sweltering heat wave. The candies grow sticky in the sun, and the Spooky Pets lie panting in the shade. But this is minor news compared to the calamity that has just occurred.
.Pet-Wolf-Spooky.pull-right
h3 Spooky Potions Vanishing Soon
p If you're hoping to collect all the Spooky Pets, be sure to do it soon. The Spooky Hatching Potions will disappear from the Market on November 1st!
br
p The Joyful Reaper is gone.
br
p The citizens of the Flourishing Fields are working frantically to find their lost ruler, but many of their search parties have not returned. Those few that do make it back are empty-handed. Worse yet, they report frightening hordes of spirits drifting at the edges of the Fields.
br
p Redphoenix grimly gathers her scouts. "If these spirits are a threat, we need to face them directly," she says. "There's no point in procrastination." Without fuss or fanfare, they set off into the night.
p If you purchase Spooky Hatching Potions before November 1st but don't have a chance to use them, they will remain in your inventory. Be sure to stock up now, or you'll have to wait a whole year for them to return!
if menuItem !== 'oldNews'
hr
a(href='/static/old-news', target='_blank') Read older news
mixin oldNews
h2 10/26/2015 - OCTOBER SUBSCRIBER ITEM REVEALED
tr
td
.promo_mystery_201510.pull-right
h3 October Subscriber Item: Horned Goblin!
p The October Subscriber Item has been revealed: the Horned Goblin Set! All October subscribers will receive the Goblin Horns and the Goblin Tail. You still have four days to <a href='/#/options/settings/subscription'>subscribe</a> and receive the item set! Thank you so much for your support - we really do rely on you to keep Habitica free to use and running smoothly.
p.small.muted by Lemoness
h2 10/21/2015 - FROG PET QUEST, TO-DO SORTING FIX, AND SECOND WORLD BOSS EXHAUST STRIKE
tr
td
span.Mount_Body_Frog-Base.pull-right
span.Mount_Head_Frog-Base.pull-right(style='margin:0')
h3 Frog Pet Quest
p Deep in the Swamps of Stagnation, you find your path obstructed by debris... and an angry amphibian. Yuck! If you complete the <a href='/#/options/inventory/quests'>Clutter Frog quest</a>, you'll be rewarded with some princely Frog pets!
p.small.muted Art by starsystemic, RosemonkeyCT, Jon Arjinborn, and Breadstrings
p.small.muted Writing by Fluitare
tr
td
h3 To-Do Sorting Fix
p To-dos will now stay sorted when you drag and drop them! Often, To-Dos used to change their order unless you cleared your completed To-Dos on a regular basis. That should no longer be a problem.
p.small.muted by ikadzuchi and Alys
tr
td
.seasonalshop_broken.pull-right
h3 World Boss: Burnout Uses Exhaust Strike!
p Ahh!!! Our incomplete Dailies have fed the flames of Burnout, and now it has enough energy to strike again! It lets loose a gout of spectral flame that sears the Seasonal Shop. You're horrified to see that the cheery Seasonal Sorceress has been transformed into a sorrowful Exhaust Spirit!
br
p We have to rescue our NPCs! Hurry, Habiticans, complete your tasks and defeat Burnout before it strikes for a third time!
br
p Late to the fight? Learn about Burnout and how to defeat World Bosses <a href='http://habitica.wikia.com/wiki/Boss#World_Bosses' target='_blank'>here</a>.
h2 10/14/2015 - JACK O'LANTERN PETS AND MOUNTS! WORLD BOSS EXHAUST STRIKE!
hr
tr
td
.Pet-JackOLantern-Base.pull-right
h3 Jack O'Lantern Pets and Mounts
p The Flourishing Fields are full of cute carved pumpkins - and it looks like one has followed you home! It must be frightened of Burnout and looking for an adventurer to protect it.
br
p Those of you who weren't around last Fall Festival have received a pet Jack-O-Lantern, and those of you who got one last year have received a Jack-O-Lantern Mount! Happy Fall Festival.
p.small.muted by Lemoness
tr
td
.npc_ian_broken.pull-right
h3 World Boss: Burnout Uses Exhaust Strike!
p Oh no! Despite our best efforts, we've let some Dailies get away from us, and now Burnout is inflamed with energy! With a crackling snarl, it engulfs Ian the Quest Master in a surge of spectral fire. As fallen quest scrolls smolder, the smoke clears, and you see that Ian has been drained of energy and turned into a drifting Exhaust Spirit!
br
p Only defeating Burnout can break the spell and save our beloved Quest Master. Let's keep our Dailies in check and defeat this monster before it attacks again!
br
p Late to the fight? Learn about Burnout and how to defeat World Bosses <a href='http://habitica.wikia.com/wiki/Boss#World_Bosses' target='_blank'>here</a>.
h2 10/8/2015 - WORLD BOSS REVEALED: BURNOUT!
tr
td
.promo_burnout.pull-right
h3 Burnout, Scourge of the Flourishing Fields
p It is well past midnight, still and stiflingly hot, when Redphoenix and scout captain Kiwibot abruptly burst through the city gates. "We need to evacuate all the wooden buildings!" Redphoenix shouts. "Hurry!"
br
p Kiwibot grips the wall as she catches her breath."It's draining people and turning them into Exhaust Spirits! That's why everything was delayed. That's where the missing people have gone. It's been stealing their energy!"
br
p "'It'?'" asks Lemoness.
br
p And then the heat takes form.
br
p It rises from the earth in a billowing, twisting mass, and the air chokes with the scent of smoke and sulphur. Flames lick across the molten ground and contort into limbs, writhing to horrific heights. Smoldering eyes snap open, and the creature lets out a deep and crackling cackle.
br
p Kiwibot whispers a single word.
br
p "Burnout."
hr
p Quickly, Habiticans, we have to save the Flourishing Fields from incineration! All of us will battle this World Boss together by completing tasks, but it won't attack us individually. However, the more Dailies we skip, the closer we get to triggering its fearsome Exhaust Strike, which will target our NPCs!
br
p Knowing your enemy is the first step to defeating it. <a href='http://habitica.wikia.com/wiki/Boss#World_Bosses' target='_blank'>Read more about World Bosses here</a>.
br
p.small.muted by Lemoness, SabreCat, and Kiwibot
h2 10/5/2015 - OCTOBER BACKGROUNDS REVEALED, COSTUME CHALLENGE BEGINS, AND FALL PLOT-LINE CONTINUES
tr
td
.background_harvest_moon.pull-right
h3 October Backgrounds Revealed
p There are three new avatar backgrounds in the <a href='/#/options/profile/backgrounds'>Background Shop!</a> Now your avatar can slog through a Slimy Swamp, cackle under the Harvest Moon, or shiver in the Swarming Darkness!
p.small.muted by Leephon, midori88, and Draayder
tr
td
h3 Costume Challenge Begins
p The Community Costume Challenge has begun! Between now and October 31st, dress up as your avatar in real life and post a photo on social media to get the coveted Costume Challenge badge! Read the full rules on the Challenge page <a href='/#/options/groups/challenges/0b3d1116-89d3-4025-8a9e-593bc28e9cff'>here</a>.
p.small.muted by Lemoness
tr
td
h3 Fall Plot-Line Continues
p Although we are now well into autumn, the Flourishing Fields are locked in a sweltering heat wave. The candies grow sticky in the sun, and the Spooky Pets lie panting in the shade. But this is minor news compared to the calamity that has just occurred.
br
p The Joyful Reaper is gone.
br
p The citizens of the Flourishing Fields are working frantically to find their lost ruler, but many of their search parties have not returned. Those few that do make it back are empty-handed. Worse yet, they report frightening hordes of spirits drifting at the edges of the Fields.
br
p Redphoenix grimly gathers her scouts. "If these spirits are a threat, we need to face them directly," she says. "There's no point in procrastination." Without fuss or fanfare, they set off into the night.
h2 10/1/2015 - SPOOKY HATCHING POTION; NEW ITEMS IN THE ENCHANTED ARMOIRE
tr
td
@@ -21,7 +21,7 @@ div(ng-if='::profile.backer.npc')
hr
div(ng-if='::profile.contributor.level || user._id == profile._id')
.achievement.achievement-firefox(ng-if='::profile.contributor.level')
.achievement.achievement-boot(ng-if='::profile.contributor.level')
div(ng-class='::{muted: !profile.contributor.level}')
h5
span.label.label-default(ng-if='::profile.contributor.level', class='label-contributor-{{::profile.contributor.level}}') {{::contribText(profile.contributor, profile.backer)}}
@@ -219,6 +219,13 @@ div(ng-if='::profile.achievements.quests.stressbeast')
=env.t('achievementStressbeastText')
hr
div(ng-if='::profile.achievements.quests.burnout')
.achievement.achievement-burnout
h5=env.t('achievementBurnout')
small
=env.t('achievementBurnoutText')
hr
div(ng-if='::profile.achievements.costumeContest')
.achievement.achievement-costumeContest
h5=env.t('costumeContest')
+1 -1
View File
@@ -19,7 +19,7 @@ block content
br
=env.t('reportBug')
| &colon;&nbsp;
a(target='_blank', href='https://github.com/HabitRPG/habitrpg/issues?q=is%3Aopen') Github
a(target='_blank', href='https://github.com/HabitRPG/habitrpg/issues?q=is%3Aopen') GitHub
br
=env.t('reportCommunityIssues')
| &colon;&nbsp;
+1 -1
View File
@@ -18,7 +18,7 @@ block content
=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']
- 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', 'world-boss']
- for heading, index in headings
a.h2.accordion.collapsed(ng-href='#' + heading, data-toggle='collapse')=env.t('faqQuestion' + index)
.clearfix.collapse(id=heading)
+8 -8
View File
@@ -2,9 +2,6 @@ doctype html
html(ng-app='habitrpg', ng-controller='RootCtrl')
head
meta(charset='utf-8')
script(type='text/javascript').
window.env = !{JSON.stringify(env._.pick(env, env.clientVars))};
!= env.getManifestFiles("tmp_static_front")
link(href='//fonts.googleapis.com/css?family=Lato:400,700', rel='stylesheet', type='text/css')
title Habitica | Your Life the Role Playing Game
meta(name='description', content='')
@@ -12,16 +9,19 @@ html(ng-app='habitrpg', ng-controller='RootCtrl')
meta(name='author', content='')
meta(name='geo.placename', content='')
meta(name='viewport', content='width=device-width, maximum-scale=1')
meta(property='og:title', content='')
meta(property='og:description', content='')
meta(property='og:url', content='')
meta(property='og:image', content='')
meta(property='og:site_name', content='')
meta(property='og:url', content='https://habitica.com/static/front')
meta(property='og:type', content='website')
meta(property='og:title', content='Habitica: Your Life the Role Playing Game')
meta(property='og:description', content='Habitica is a free habit building and productivity app that treats your real life like a game. With in-game rewards and punishments to motivate you and a strong social network to inspire you, Habitica can help you achieve your goals to become healthy, hard-working, and happy.')
meta(property='og:site_name', content='Habitica')
meta(property='og:image', content='https://s3.amazonaws.com/habitica-assets/assets/gryphon_logo_300x300.png')
meta(property='fb:app_id', content='128307497299777')
meta(name='twitter:card' content='summary')
meta(name='twitter:site' content='@habitica')
meta(name='twitter:title' content='Habitica: Your Life the Role Playing Game')
meta(name='twitter:description' content='Habitica is a free habit-building and productivity app that treats your real life like a game! Motivate yourself with in-game rewards and social encouragement.')
meta(name='twitter:image' content='https://s3.amazonaws.com/habitica-assets/assets/gryphon_logo.png')
meta(name='apple-itunes-app' content='app-id=994882113')
link(rel='canonical', href='', type='text/html')
link(rel='shortcut icon', href='/favicon.ico')
+1 -1
View File
@@ -19,7 +19,7 @@ block content
p= env.t('presskitDownload')
| &nbsp;
a.btn.btn-success(href='/presskit/presskit.zip') presskit.zip
- var imgs = ['habitrpg_pixel', 'HabitRPGdevices', 'Market Sample Screen', 'Challenges Sample Screen', 'Equipment Sample Screen', 'Guilds Sample Screen', 'HabitRPGPromoPostCard6', 'HabitRPGPromoThin', 'Laundromancer_by_Arcosine', 'promo mobile inventorySTILL1', 'promo mobile inventorySTILL2', 'PROMOdevices2', 'SnackLessMonster_by_Arcosine', 'stag_battle_by_leephon', 'stagnant_dishes_by_kiwibot', 'vice_reborn_by_baconsaur']
- var imgs = ['Habitica Text Logo', 'Habitica Logo Promo', 'Habitica Gryphon', 'Dread Drag\'on of Dilatory', 'Market Sample Screen', 'Challenges Sample Screen', 'Equipment Sample Screen', 'Guilds Sample Screen', 'HabiticaPromoThin', 'Laundromancer', 'Tasks Page', 'SnackLess Monster', 'Battling the Ghost Stag', 'Stagnant Dishes Boss', 'Necro-Vice']
//-#carousel.carousel.slide(data-ride='carousel')
ol.carousel-indicators