Merge branch 'develop' of github.com:HabitRPG/habitrpg into add-flag-to-chat

Conflicts:
	views/options/social/chat-message.jade
This commit is contained in:
Blade Barringer 2014-12-18 21:55:50 -06:00
commit 7f10049b37
32 changed files with 431 additions and 155 deletions

View file

@ -41,5 +41,13 @@
"mode":"sandbox",
"client_id":"client_id",
"client_secret":"client_secret"
},
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
"LOGGLY": {
"enabled": false,
"subdomain": "subdomain",
"token": "token",
"username": "username",
"password": "password"
}
}

1
keys/google/iap-live Normal file
View file

@ -0,0 +1 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk6EVccD7D6y5vLmsUxUXVYTMyByq+NjfTzjMBaKCk93fq8Ahn6Ipha1K4oBIvArUj3CsP5Tb/Yf6c6SrEsSnE3eqgKQgeq+fYyfIm+Fb+tFGTttLonZ2Sqemqeyx2PjOG2CVZBXj1+Zr7jb0ocMNYraYUTvklWnpcK6/5ZaZLLent1PGiU9l5jIfRAsxmJH/UpssgpFPnCs+8PRKUALkJATTfVCCF+PJef6mcKAafAArKUEo0lR2/NeaE62Wpii33DLuKDPfMqL3g4EHZTEVE1ezFl/3WPeLlxuQm38hZLpEfEYKp17qzplU+GmWIfjZt6OdtVdonlYxIxNAZLnWEQIDAQAB

View file

@ -0,0 +1,4 @@
db.users.update({'purchased.plan.consecutive.count':NaN}, {$set:{'purchased.plan.consecutive.count':0}}, {multi:1});
db.users.update({'purchased.plan.consecutive.offset':NaN}, {$set:{'purchased.plan.consecutive.offset':0}}, {multi:1});
db.users.update({'purchased.plan.consecutive.gemCapExtra':NaN}, {$set:{'purchased.plan.consecutive.gemCapExtra':0}}, {multi:1});
db.users.update({'purchased.plan.consecutive.trinkets':NaN}, {$set:{'purchased.plan.consecutive.trinkets':0}}, {multi:1});

View file

@ -26,9 +26,11 @@
"grunt-nodemon": "~0.3.0",
"habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#develop",
"icalendar": "git://github.com/lefnire/node-icalendar#master",
"in-app-purchase": "^0.2.0",
"jade": "~1.7.0",
"js2xmlparser": "~0.1.2",
"lodash": "~2.4.1",
"loggly": "~1.0.8",
"method-override": "~2.2.0",
"moment": "~2.8.3",
"mongoose": "~3.8.17",
@ -61,7 +63,7 @@
"www.habitrpg.com"
],
"engines": {
"node": "0.10.x",
"node": "0.10.22",
"npm": "1.2.x"
},
"scripts": {

View file

@ -31,16 +31,17 @@ future re: pets and whatnot, this is just temporary.
position: absolute
@extend $hrpg-label
.avatar-level
bottom: 2px
right: 2px
bottom: 4px
right: 4px
// when a buff/rebirth exists, add space between the icon and the level
.glyphicon-circle-arrow-up
padding-right: 4px
.glyphicon-plus-sign
padding-left: 4px
.avatar-name
top: 2px
left: 2px
max-width: 132px
top: 4px
left: 4px
opacity: 0 // Hide username by default
transition: opacity 0.2s ease-out
@ -83,8 +84,9 @@ future re: pets and whatnot, this is just temporary.
width: 50px !important
.addthis_native_toolbox
position: absolute
top:2px
right:2px
bottom:4px
left:4px
z-index: 2
opacity:0
&:hover .addthis_native_toolbox
opacity:1

View file

@ -37,6 +37,9 @@
markdown, span.time
padding: 0 3px
line-height: 1.618em
.long-title .tooltip
white-space: normal
span.time
white-space: nowrap

View file

@ -32,4 +32,33 @@
visibility: hidden
.transparent
opacity: 0.5
opacity: 0.5
// Allow block element to be centered
.col-centered
margin-left: auto
margin-right: auto
// .pull-right & .pull-left for specific screen sizes
// Follows conventions for col-x-SIZE classes
@media (min-width: 768px)
.pull-left-sm
float: left
.pull-right-sm
float: right
@media (min-width: 992px)
.pull-left-md
float: left
.pull-right-md
float: right
@media (min-width: 1200px)
.pull-left-lg
float: left
.pull-right-lg
float: right

View file

@ -1,3 +1,7 @@
.quest_collected_true
color: #ccc
text-decoration:line-through
text-decoration:line-through
quest-rewards
hr
clear:both

View file

@ -118,6 +118,8 @@ for $stage in $stages
.habit-wide .task-text
padding-left: 7em
.habit-narrow .task-text
padding-left: 0.75em
// when a task is being dragged
.task.ui-sortable-helper

View file

@ -7,7 +7,6 @@ window.habitrpg = angular.module('habitrpg',
// @see https://github.com/angular-ui/ui-router/issues/110 and https://github.com/HabitRPG/habitrpg/issues/1705
// temporary hack until they have a better solution
.value('$anchorScroll', angular.noop)
.constant("API_URL", "")
.constant("STORAGE_USER_ID", 'habitrpg-user')
@ -185,6 +184,10 @@ window.habitrpg = angular.module('habitrpg',
url: '/timetravelers',
templateUrl: "partials/options.inventory.timetravelers.html"
})
.state('options.inventory.seasonalshop', {
url: '/seasonalshop',
templateUrl: "partials/options.inventory.seasonalshop.html"
})
// Options > Settings
.state('options.settings', {

View file

@ -42,6 +42,7 @@
//$.getScript("//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5016f6cc44ad68a4"); //FIXME why isn't this working when here? instead it's now in <head>
var addthisServices = 'facebook,twitter,googleplus,tumblr,'+window.env.BASE_URL.replace('https://','').replace('http://','');
window.addthis_config = {
ui_click: true,
services_custom:{
name: "Download",
url: window.env.BASE_URL+"/export/avatar-"+User.user._id+".png",

View file

@ -148,21 +148,19 @@ api.loginSocial = function(req, res, next) {
network = req.body.network;
if (network!=='facebook')
return res.json(401, {err:"Only Facebook supported currently."});
async.waterfall([
function(cb){
async.auto({
profile: function (cb) {
passport._strategies[network].userProfile(access_token, cb);
},
function(profile, cb) {
var q = {};q['auth.'+network+'.id'] = profile.id;
User.findOne(q, {_id:1, apiToken:1, auth:1}, function(err, user){
if (err) return cb(err);
cb(null, {user:user, profile:profile});
});
},
function(data, cb){
if (data.user) return cb(null, data.user);
user: ['profile', function (cb, results) {
var q = {};
q['auth.' + network + '.id'] = results.profile.id;
User.findOne(q, {_id: 1, apiToken: 1, auth: 1}, cb);
}],
register: ['profile', 'user', function (cb, results) {
if (results.user) return cb(null, results.user);
// Create new user
var prof = data.profile;
var prof = results.profile;
var user = {
preferences: {
language: req.language // User language detected from browser, not saved
@ -175,15 +173,16 @@ api.loginSocial = function(req, res, next) {
user = new User(user);
user.save(cb);
if(isProd && prof.emails && prof.emails[0] && prof.emails[0].value){
utils.txnEmail({name:prof.displayName || prof.username, email:prof.emails[0].value}, 'welcome');
if (isProd && prof.emails && prof.emails[0] && prof.emails[0].value) {
utils.txnEmail({name: prof.displayName || prof.username, email: prof.emails[0].value}, 'welcome');
}
ga.event('register', network).send();
}
], function(err, user){
}]
}, function(err, results){
if (err) return res.json(401, {err: err.toString ? err.toString() : err});
if (user.auth.blocked) return res.json(401, accountSuspended(user._id));
return res.json(200, {id: user.id, token:user.apiToken});
var acct = results.register[0] ? results.register[0] : results.register;
if (acct.auth.blocked) return res.json(401, accountSuspended(acct._id));
return res.json(200, {id:acct._id, token:acct.apiToken});
})
};

View file

@ -147,69 +147,63 @@ api.getMember = function(req, res, next) {
// CREATE
api.create = function(req, res, next){
var user = res.locals.user;
var group, chal;
// First, make sure they've selected a legit group, and store it for later
var waterfall = [
function(cb){
Group.findById(req.body.group).exec(cb);
async.auto({
get_group: function(cb){
var q = {_id:req.body.group};
if (req.body.group!='habitrpg') q.members = {$in:[user._id]}; // make sure they're a member of the group
Group.findOne(q, cb);
},
function(_group, cb){
if (!_group) return cb("Group." + req.body.group + " not found");
group = _group;
cb(null);
}
];
save_chal: ['get_group', function(cb, results){
var group = results.get_group,
prize = +req.body.prize;
if (!group)
return cb({code:404, err:"Group." + req.body.group + " not found"});
if (group.leaderOnly && group.leaderOnly.challenges && group.leader !== user._id)
return cb({code:401, err: "Only the group leader can create challenges"});
// If they're adding a prize, do some validation
if (prize < 0)
return cb({code:401, err: 'Challenge prize must be >= 0'});
if (req.body.group=='habitrpg' && prize < 1)
return cb({code:401, err: 'Prize must be at least 1 Gem for public challenges.'});
if (prize > 0) {
var groupBalance = ((group.balance && group.leader==user._id) ? group.balance : 0);
var prizeCost = prize/4; // I really should have stored user.balance as gems rather than dollars... stupid...
if (prizeCost > user.balance + groupBalance)
return cb("You can't afford this prize. Purchase more gems or lower the prize amount.")
// If they're adding a prize, do some validation
if (+req.body.prize < 0) return res.json(401, {err: 'Challenge prize must be >= 0'});
if (req.body.group=='habitrpg' && +req.body.prize < 1) return res.json(401, {err: 'Prize must be at least 1 Gem for public challenges.'});
if (+req.body.prize > 0) {
waterfall.push(function(cb){
var groupBalance = ((group.balance && group.leader==user._id) ? group.balance : 0);
var prizeCost = req.body.prize/4; // I really should have stored user.balance as gems rather than dollars... stupid...
if (prizeCost > user.balance + groupBalance)
return cb("You can't afford this prize. Purchase more gems or lower the prize amount.")
if (groupBalance >= prizeCost) {
// Group pays for all of prize
group.balance -= prizeCost;
} else if (groupBalance > 0) {
// User pays remainder of prize cost after group
var remainder = prizeCost - group.balance;
group.balance = 0;
user.balance -= remainder;
} else {
// User pays for all of prize
user.balance -= prizeCost;
}
cb(null);
});
}
waterfall = waterfall.concat([
function(cb) { // if we're dealing with prize above, arguemnts will be `group, numRows, cb` - else `cb`
if (groupBalance >= prizeCost) {
// Group pays for all of prize
group.balance -= prizeCost;
} else if (groupBalance > 0) {
// User pays remainder of prize cost after group
var remainder = prizeCost - group.balance;
group.balance = 0;
user.balance -= remainder;
} else {
// User pays for all of prize
user.balance -= prizeCost;
}
}
req.body.leader = user._id;
req.body.official = user.contributor.admin && req.body.official;
var chal = new Challenge(req.body); // FIXME sanitize
chal.members.push(user._id);
chal.save(cb)
},
function(_chal, num, cb){
chal = _chal;
group.challenges.push(chal._id);
group.save(cb);
},
function(_group, num, cb) {
chal.save(cb);
}],
save_group: ['save_chal', function(cb, results){
results.get_group.challenges.push(results.save_chal[0]._id);
results.get_group.save(cb);
}],
sync_user: ['save_group', function(cb, results){
// Auto-join creator to challenge (see members.push above)
chal.syncToUser(user, cb);
}
]);
async.waterfall(waterfall, function(err){
if (err) return next(err);
res.json(chal);
user = group = chal = null;
});
results.save_chal[0].syncToUser(user, cb);
}]
}, function(err, results){
if (err) return err.code? res.json(err.code, err) : next(err);
return res.json(results.save_chal[0]);
user = null;
})
}
// UPDATE

View file

@ -187,7 +187,7 @@ api.update = function(req, res, next) {
if(group.leader !== user._id)
return res.json(401, {err: "Only the group leader can update the group!"});
'name description logo logo leaderMessage leader'.split(' ').forEach(function(attr){
'name description logo logo leaderMessage leader leaderOnly'.split(' ').forEach(function(attr){
group[attr] = req.body[attr];
});

View file

@ -0,0 +1,129 @@
var iap = require('in-app-purchase');
var async = require('async');
var payments = require('./index');
var nconf = require('nconf');
var inAppPurchase = require('in-app-purchase');
inAppPurchase.config({
// this is the path to the directory containing iap-sanbox/iap-live files
googlePublicKeyPath: nconf.get("IAP_GOOGLE_KEYDIR")
});
// Validation ERROR Codes
var INVALID_PAYLOAD = 6778001;
var CONNECTION_FAILED = 6778002;
var PURCHASE_EXPIRED = 6778003;
exports.androidVerify = function(req, res, next) {
var iapBody = req.body;
var user = res.locals.user;
iap.setup(function (error) {
if (error) {
var resObj = {
ok: false,
data: 'IAP Error'
};
console.error('IAP Setup ERROR');
console.error(error);
res.json(resObj);
return;
}
/*
google receipt must be provided as an object
{
"data": "{stringified data object}",
"signature": "signature from google"
}
*/
var testObj = {
data: iapBody.transaction.receipt,
signature: iapBody.transaction.signature
};
// iap is ready
iap.validate(iap.GOOGLE, testObj, function (err, googleRes) {
if (err) {
var resObj = {
ok: false,
data: {
code: INVALID_PAYLOAD,
message: err.toString()
}
};
res.json(resObj);
console.error(err);
return;
}
if (iap.isValidated(googleRes)) {
var resObj = {
ok: true,
data: googleRes
};
payments.buyGems({user:user, paymentMethod:'IAP GooglePlay'});
// yay good!
res.json(resObj);
}
});
});
};
exports.iosVerify = function(req, res, next) {
console.info(req.body);
var iapBody = req.body;
var user = res.locals.user;
iap.setup(function (error) {
if (error) {
var resObj = {
ok: false,
data: 'IAP Error'
};
console.error('IAP Setup ERROR');
console.error(error);
res.json(resObj);
return;
}
// iap is ready
iap.validate(iap.APPLE, iapBody.transaction.receipt, function (err, appleRes) {
if (err) {
var resObj = {
ok: false,
data: {
code: INVALID_PAYLOAD,
message: err.toString()
}
};
res.json(resObj);
console.error(err);
return;
}
if (iap.isValidated(appleRes)) {
var resObj = {
ok: true,
data: appleRes
};
payments.buyGems({user:user, paymentMethod:'IAP AppleStore'});
// yay good!
res.json(resObj);
}
});
});
};

View file

@ -7,9 +7,9 @@ var moment = require('moment');
var isProduction = nconf.get("NODE_ENV") === "production";
var stripe = require('./stripe');
var paypal = require('./paypal');
var User = require('mongoose').model('User');
var members = require('../members')
var async = require('async');
var iap = require('./iap');
function revealMysteryItems(user) {
_.each(shared.content.gear.flat, function(item) {
@ -123,4 +123,7 @@ exports.paypalSubscribeSuccess = paypal.executeBillingAgreement;
exports.paypalSubscribeCancel = paypal.cancelSubscription;
exports.paypalCheckout = paypal.createPayment;
exports.paypalCheckoutSuccess = paypal.executePayment;
exports.paypalIPN = paypal.ipn;
exports.paypalIPN = paypal.ipn;
exports.iapAndroidVerify = iap.androidVerify;
exports.iapIosVerify = iap.iosVerify;

View file

@ -257,37 +257,45 @@ api.update = function(req, res, next) {
};
api.cron = function(req, res, next) {
var user = res.locals.user,
progress = user.fns.cron(),
ranCron = user.isModified(),
quest = shared.content.quests[user.party.quest.key];
try{
var user = res.locals.user,
progress = user.fns.cron(),
ranCron = user.isModified(),
quest = shared.content.quests[user.party.quest.key];
if (ranCron) res.locals.wasModified = true;
if (!ranCron) return next(null,user);
Group.tavernBoss(user,progress);
if (!quest) return user.save(next);
if (ranCron) res.locals.wasModified = true;
if (!ranCron) return next(null,user);
Group.tavernBoss(user,progress);
if (!quest) return user.save(next);
// If user is on a quest, roll for boss & player, or handle collections
// FIXME this saves user, runs db updates, loads user. Is there a better way to handle this?
async.waterfall([
function(cb){
user.save(cb); // make sure to save the cron effects
},
function(saved, count, cb){
var type = quest.boss ? 'boss' : 'collect';
Group[type+'Quest'](user,progress,cb);
},
function(){
var cb = arguments[arguments.length-1];
// User has been updated in boss-grapple, reload
User.findById(user._id, cb);
}
], function(err, saved) {
res.locals.user = saved;
next(err,saved);
user = progress = quest = null;
});
// If user is on a quest, roll for boss & player, or handle collections
// FIXME this saves user, runs db updates, loads user. Is there a better way to handle this?
async.waterfall([
function(cb){
user.save(cb); // make sure to save the cron effects
},
function(saved, count, cb){
var type = quest.boss ? 'boss' : 'collect';
Group[type+'Quest'](user,progress,cb);
},
function(){
var cb = arguments[arguments.length-1];
// User has been updated in boss-grapple, reload
User.findById(user._id, cb);
}
], function(err, saved) {
if(err) logging.loggly({error: "Cron caught", stack: err.stack || err});
res.locals.user = saved;
next(err,saved);
user = progress = quest = null;
});
}catch(e){
logging.loggly({
error: "Cron uncaught",
stack: e.stack || e
});
throw e;
}
};

View file

@ -3,7 +3,23 @@ var winston = require('winston');
require('winston-mail').Mail;
require('winston-newrelic');
var logger;
var logger, loggly;
if (nconf.get('LOGGLY:enabled')){
loggly = require('loggly').createClient({
token: nconf.get('LOGGLY:token'),
subdomain: nconf.get('LOGGLY:subdomain'),
auth: {
username: nconf.get('LOGGLY:username'),
password: nconf.get('LOGGLY:password')
},
//
// Optional: Tag to send with EVERY log message
//
tags: [('heroku-'+nconf.get('BASE_URL'))],
json: true
});
}
if (logger == null) {
logger = new (winston.Logger)({});
@ -49,3 +65,8 @@ module.exports.error = function(/* variable args */) {
if (logger)
logger.error.apply(logger, arguments);
};
module.exports.loggly = function(/* variable args */){
if (loggly)
loggly.log.apply(loggly, arguments);
};

View file

@ -75,6 +75,13 @@ module.exports.errorHandler = function(err, req, res, next) {
"\n\nbody: " + JSON.stringify(req.body) +
(res.locals.ops ? "\n\ncompleted ops: " + JSON.stringify(res.locals.ops) : "");
logging.error(stack);
logging.loggly({
error: "Uncaught error",
stack: (err.stack || err.message || err),
body: req.body, headers: req.header,
auth: (req.headers['x-api-user'] + ' | ' + req.headers['x-api-key']),
originalUrl: req.originalUrl
});
var message = err.message ? err.message : err;
message = (message.length < 200) ? message : message.substring(0,100) + message.substring(message.length-100,message.length);
res.json(500,{err:message}); //res.end(err.message);
@ -173,12 +180,9 @@ module.exports.locals = function(req, res, next) {
language.momentLang = ((!isStaticPage && i18n.momentLangs[language.code]) || undefined);
var tavern = require('./models/group').tavern;
res.locals.habitrpg = {
NODE_ENV: nconf.get('NODE_ENV'),
BASE_URL: nconf.get('BASE_URL'),
GA_ID: nconf.get("GA_ID"),
var envVars = _.pick(nconf.get(), 'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY'.split(' '));
res.locals.habitrpg = _.merge(envVars, {
IS_MOBILE: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header('User-Agent')),
STRIPE_PUB_KEY: nconf.get('STRIPE_PUB_KEY'),
getManifestFiles: getManifestFiles,
getBuildUrl: getBuildUrl,
avalaibleLanguages: i18n.avalaibleLanguages,
@ -193,11 +197,9 @@ module.exports.locals = function(req, res, next) {
siteVersion: siteVersion,
Content: shared.content,
mods: require('./models/user').mods,
FACEBOOK_KEY: nconf.get('FACEBOOK_KEY'),
tavern: tavern, // for world boss
worldDmg: (tavern && tavern.quest && tavern.quest.extra && tavern.quest.extra.worldDmg) || {}
};
});
// Put query-string party invitations into session to be handled later
try{

View file

@ -26,7 +26,10 @@ var GroupSchema = new Schema({
# id: String
# }]
*/
leaderOnly: { // restrict group actions to leader (members can't do them)
challenges: {type:Boolean, 'default':false},
//invites: {type:Boolean, 'default':false}
},
memberCount: {type: Number, 'default': 0},
challengeCount: {type: Number, 'default': 0},
balance: Number,

View file

@ -17,4 +17,7 @@ router.post("/stripe/subscribe/edit", auth.auth, i18n.getUserLanguage, payments.
//router.get("/stripe/subscribe", auth.authWithUrl, i18n.getUserLanguage, payments.stripeSubscribe); // checkout route is used (above) with ?plan= instead
router.get("/stripe/subscribe/cancel", auth.authWithUrl, i18n.getUserLanguage, payments.stripeSubscribeCancel);
router.post("/iap/android/verify", auth.authWithUrl, /*i18n.getUserLanguage, */payments.iapAndroidVerify);
router.post("/iap/ios/verify", /*auth.authWithUrl, i18n.getUserLanguage, */ payments.iapIosVerify);
module.exports = router;

View file

@ -17,7 +17,10 @@ script(type='text/ng-template', id='partials/options.inventory.html')
=env.t('equipment')
li.equipment-tab(ng-class="{ active: $state.includes('options.inventory.timetravelers') }")
a(ui-sref='options.inventory.timetravelers')
| Time Travelers
=env.t('timeTravelers')
li.equipment-tab(ng-class="{ active: $state.includes('options.inventory.seasonalshop') }")
a(ui-sref='options.inventory.seasonalshop')
=env.t('seasonalShop')
.tab-content
.tab-pane.active

View file

@ -19,6 +19,18 @@ script(type='text/ng-template', id='partials/options.inventory.equipment.html')
div(ng-repeat='item in gear[klass]')
button.customize-option(popover='{{::item.notes()}}', popover-title='{{::item.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='user.ops.equip({params:{type:"costume", key:item.key}})', class='shop_{{::item.key}}', ng-class='{selectableInventory: user.items.gear.costume[item.type] == item.key}')
script(type='text/ng-template', id='partials/options.inventory.seasonalshop.html')
.container-fluid
.stable.row
.col-md-2
.seasonalshop_closed
.col-md-10
.popover.static-popover.fade.right.in
.arrow
h3.popover-title!=env.t('seasonalShopClosedTitle', {linkStart:"<a href='http://blog.habitrpg.com/who' target='_blank'>", linkEnd: "</a>"})
.popover-content
p!=env.t('seasonalShopClosedText', {linkStart: "<a href='https://habitrpg.wikia.com/wiki/Community#World_Events' target='_blank'>", linkEnd: "</a>"})
script(type='text/ng-template', id='partials/options.inventory.timetravelers.html')
.container-fluid
.stable.row(ng-if='user.purchased.plan.consecutive.trinkets <= 0')

View file

@ -15,8 +15,9 @@ mixin customizeProfile(mobile)
if ~['limited','seasonal'].indexOf(status)
.label.label-info.pull-right.hint(popover=limited, popover-title=env.t(status+'Edition'), popover-placement='right', popover-trigger='mouseenter')=env.t(status+'Edition')
menu(label=env.t(title))
+gemCost(2)
button.btn.btn-xs(ng-hide='#{status=="disabled"} || #{showPath("user.purchased."+path, colors, "||")}', ng-click='#{unlockPath(path, colors)}')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
span(ng-hide='#{status=="disabled"} || #{showPath("user.purchased."+path, colors, "&&")}')
+gemCost(2)
button.btn.btn-xs(ng-click='#{unlockPath(path, colors)}')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
each color in colors
button.customize-option(type='button', class='#{path=="skin" ? "skin_"+color : "customize-option hair hair_bangs_1_"+color}', ng-class='{locked: !user.purchased.#{path}["#{color}"]}', ng-if='#{status!="disabled"} || user.purchased.#{path}["#{color}"]', ng-click='unlock("#{path}.#{color}")')
@ -38,8 +39,9 @@ mixin customizeProfile(mobile)
menu(label=env.t('specialShirts'))
- var specialShirts = ['convict', 'cross', 'fire', 'horizon', 'ocean', 'purple', 'rainbow', 'redblue', 'thunder', 'tropical', 'zombie']
+gemCost(2)
button.btn.btn-xs(ng-hide='#{showPath("user.purchased.shirt", specialShirts, "&&")}', ng-click='#{unlockPath("shirt",specialShirts)}')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
span(ng-hide='#{showPath("user.purchased.shirt", specialShirts, "&&")}')
+gemCost(2)
button.btn.btn-xs(ng-click='#{unlockPath("shirt",specialShirts)}')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
each shirt in specialShirts
button.customize-option(type='button', class='{{user.preferences.size}}_shirt_'+shirt, ng-class='{locked: !user.purchased.shirt.'+shirt+'}', ng-click='unlock("shirt.'+shirt+'")')
@ -80,8 +82,9 @@ mixin customizeProfile(mobile)
// Purchasable hairstyles
menu(label=env.t('hairSet1'))
- var colors = [2,4,5,6,7,8]
+gemCost(2)
button.btn.btn-xs(ng-hide='#{showPath("user.purchased.hair", colors, "&&")}', ng-click='#{unlockPath("hair.base",colors)}')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
span(ng-hide='#{showPath("user.purchased.hair.base", colors, "&&")}')
+gemCost(2)
button.btn.btn-xs(ng-click='#{unlockPath("hair.base",colors)}')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
each num in colors
button(class='hair_base_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.base["#{num}"]}', ng-click='unlock("hair.base.#{num}")')
@ -93,9 +96,10 @@ mixin customizeProfile(mobile)
button(class='hair_flower_#{num} customize-option', type='button', ng-click='set({"preferences.hair.flower":#{num}})')
li.customize-menu
h5=env.t('bodyFacialHair')
+gemCost(2)
button.btn.btn-xs(ng-hide='user.purchased.hair.mustache["1"] && user.purchased.hair.mustache["2"] && user.purchased.hair.beard["1"] && user.purchased.hair.beard["2"] && user.purchased.hair.beard["3"]', ng-click='unlock("hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3")')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
menu(label=env.t('bodyFacialHair'))
span(ng-hide='user.purchased.hair.mustache["1"] && user.purchased.hair.mustache["2"] && user.purchased.hair.beard["1"] && user.purchased.hair.beard["2"] && user.purchased.hair.beard["3"]')
+gemCost(2)
button.btn.btn-xs(ng-click='unlock("hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3")')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
// Beard
menu(label=env.t('beard'))
@ -249,9 +253,9 @@ mixin backgrounds(mobile)
- var k = bgsKeys[i], bgs = env.Content.backgrounds[k];
li.customize-menu
menu(label=env.t(k))
+gemCost(7)
//-button.btn.btn-xs(ng-hide="ownsSet('background',Content.backgrounds['#{k}'])",ng-click="unlock(setKeys('background',Content.backgrounds['#{k}']))")!= env.t('unlockSet',{cost:15}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
button.btn.btn-xs(ng-hide="ownsSet('background',#{JSON.stringify(bgs)})",ng-click="unlock(setKeys('background',#{JSON.stringify(bgs)}))")!= env.t('unlockSet',{cost:15}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
span(ng-hide="ownsSet('background',#{JSON.stringify(bgs)})")
+gemCost(7)
button.btn.btn-xs(ng-click="unlock(setKeys('background',#{JSON.stringify(bgs)}))")!= env.t('unlockSet',{cost:15}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
each bg,k in bgs
button.customize-option(type='button', class='background_#{k}', ng-class="user.purchased.background.#{k} ? 'background-unlocked' : 'background-locked'", ng-click='unlock("background.#{k}")', popover-title=bg.text(), popover=bg.notes(),popover-trigger='mouseenter')
i.glyphicon.glyphicon-lock(ng-if="!user.purchased.background.#{k}")

View file

@ -31,7 +31,7 @@ mixin chatMessages(inbox)
| &nbsp; &nbsp;
a(ng-click="flagChatMessage(group._id, message)", ng-if=':: user.contributor.admin || (!message.sent && user.flags.communityGuidelinesAccepted && message.uuid != user.id && message.uuid != "system")')
span.glyphicon.glyphicon-flag(tooltip="{{message.flags[user._id] ? env.t('abuseAlreadyReported') : env.t('abuseFlag')}}" ng-class='message.flags[user._id] ? "text-danger" : ""')
span.float-label
span.float-label(ng-class='::contribText(message.contributor, message.backer).length > 30 ? "long-title" : ""')
a.label.label-default.chat-message(ng-if=':: message.user', ng-class='::userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)')
span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent')
| &nbsp;

View file

@ -19,5 +19,10 @@ form.col-md-12.form-horizontal(ng-submit='create(newGroup)')
span.gem-cost= '4 ' + env.t('gems')
p
small=env.t('gemCost')
.form-group
.checkbox
label
input(type='checkbox', ng-model='newGroup.leaderOnly.challenges')
| Only group leader can create challenges
.form-group(ng-show='type=="party"')
input.btn.btn-default.form-control(type='submit', value=env.t('create'))

View file

@ -33,6 +33,11 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
.form-group
label=env.t('logoUrl')
input.form-control(type='url', placeholder=env.t('logoUrl'), ng-model='group.logo')
.form-group
.checkbox
label
input(type='checkbox', ng-model='group.leaderOnly.challenges')
| Only group leader can create challenges
h4=env.t('assignLeader')
select#group-leader-selection(ng-model='group._newLeader', ng-options='member.profile.name for member in group.members')

View file

@ -105,13 +105,13 @@ script(type='text/ng-template', id='partials/options.social.html')
a(ui-sref='options.social.party')
=env.t('party')
li(ng-class="{ active: $state.includes('options.social.guilds') }")
a(ui-sref='options.social.guilds')
a(ui-sref='options.social.guilds.public')
=env.t('guilds')
li(ng-class="{ active: $state.includes('options.social.challenges') }")
a(ui-sref='options.social.challenges')
=env.t('challenges')
li(ng-class="{ active: $state.includes('options.social.hall') }")
a(ui-sref='options.social.hall')
a(ui-sref='options.social.hall.heroes')
=env.t('hall')
.tab-content

View file

@ -21,7 +21,7 @@
span(ng-hide='user.preferences.sleep')=env.t('innCheckIn')
=env.t('danielText2')
.alert.alert-info(ng-show='user.preferences.sleep')
=env.t('innText')
=env.t('innText',{name:"{{user.profile.name}}"})
// Resources
.panel.panel-default

View file

@ -34,11 +34,11 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li
a(ui-sref='options.social.party')=env.t('party')
li
a(ui-sref='options.social.guilds')=env.t('guilds')
a(ui-sref='options.social.guilds.public')=env.t('guilds')
li
a(ui-sref='options.social.challenges')=env.t('challenges')
li
a(ui-sref='options.social.hall')=env.t('hall')
a(ui-sref='options.social.hall.heroes')=env.t('hall')
ul.toolbar-submenu
li
a(ui-sref='options.inventory.drops')=env.t('market')
@ -49,7 +49,9 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li
a(ui-sref='options.inventory.equipment')=env.t('equipment')
li
a(ui-sref='options.inventory.timetravelers') Time Travelers
a(ui-sref='options.inventory.timetravelers')=env.t('timeTravelers')
li
a(ui-sref='options.inventory.seasonalshop')=env.t('seasonalShop')
ul.toolbar-submenu
li
a(target="_blank" ng-href='http://data.habitrpg.com?uuid={{user._id}}')=env.t('dataTool')
@ -110,11 +112,11 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li
a(ui-sref='options.social.party')=env.t('party')
li
a(ui-sref='options.social.guilds')=env.t('guilds')
a(ui-sref='options.social.guilds.public')=env.t('guilds')
li
a(ui-sref='options.social.challenges')=env.t('challenges')
li
a(ui-sref='options.social.hall')=env.t('hall')
a(ui-sref='options.social.hall.heroes')=env.t('hall')
li.toolbar-button-dropdown
a(ui-sref='options.inventory.drops')
span=env.t('inventory')
@ -131,7 +133,9 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li
a(ui-sref='options.inventory.equipment')=env.t('equipment')
li
a(ui-sref='options.inventory.timetravelers') Time Travelers
a(ui-sref='options.inventory.timetravelers')=env.t('timeTravelers')
li
a(ui-sref='options.inventory.seasonalshop')=env.t('seasonalShop')
li.toolbar-button-dropdown
a(target="_blank" ng-href='http://data.habitrpg.com?uuid={{user._id}}')
span=env.t('data')

View file

@ -5,6 +5,7 @@ script(type='text/ng-template', id='modals/questCompleted.html')
h4 "{{::Content.quests[user.party.quest.completed].text()}}"
=env.t('completed')
.modal-body
div(ng-class='::Content.quests[user.party.quest.completed].completion() ? "pull-right-sm" : ""', class='col-centered quest_{{user.party.quest.completed}}')
p(ng-bind-html='::Content.quests[user.party.quest.completed].completion()')
quest-rewards(key='{{user.party.quest.completed}}', header=env.t('youReceived'))
.modal-footer

View file

@ -1,19 +1,40 @@
h5 PENGUIN PET QUEST AND WINTER PLOT-LINE
h5 ANDROID APP UPDATE, SEASONAL SHOP, AND WINTER PLOT-LINE CONTINUES
tr
td
h5 Penguin Pet Quest
.quest_penguin.pull-right
p Habiticans wanted to go ice-skating, but instead, a giant penguin is freezing everything in sight! All we wanted was to go ice-skating... Can you get this penguin to chill out? If so, you'll be rewarded with some penguins of your own!
p.small.muted by Melynnrose, Breadstrings, Rattify, Painter de Cluster, Daniel the Bard, and Leephon
h5 Android App Update: December Art and Buying Gems!
p The December backgrounds and penguin pet quest are now visible in the Android mobile app! Also, weve made it possible to buy gems directly from the app. Now you dont have to switch to the website to stock up!
p You can get the Android app <a href='https://play.google.com/store/apps/details?id=com.ocdevel.habitrpg&hl=en' target='_blank'>here</a>! We will announce when the iOS app is available as well.
p.small.muted by negue
tr
td
h5 Winter Plot-Line
p Lemoness enters the Tavern with worrying news from the far north of Habitica. "Nobody's heard from the Stoïkalm Steppes for over a week," she says. "It's hard to imagine anything troubling the citizens there, since it's such a placid part of the continent... But just in case, maybe I should pay a visit." Sounds like a good plan to us!
h5 Seasonal Shop Tab
.seasonalshop_closed.pull-right
p Looks like a new tab has appeared under Inventory - the <a href='https://habitrpg.com/#/options/inventory/seasonalshop'>Seasonal Shop</a>! It's still closed, but I've heard a rumor that it will open soon...
p.small.muted by SabreCat and Lemoness
tr
td
h5 Winter Plot-Line Continues
p Lemoness bursts into the Tavern, shaking icicles off her hat. "The Stoïkalm Steppes are completely abandoned!" she says, gulping the cup of tea that Daniel the Barkeep offers her. "No people milling about, no mounts and pets playing in the snow - and when I tried to fly closer, my dragon spooked and refused to land!"
p A cloaked figure in the corner steps into the fire light - SabreCat, a powerful adventurer from the north. "The Stoïkalm Steppes are the last home of many animals that have long since gone extinct elsewhere," he says. "The stoic Stoïkalmers would never flee their lands unless something was threatening their pets and mounts!"
p He turns to Lemoness. "I can speak the language of the northern beasts. I'll try to contact the roaming sabertooth prides to see if they know what happened." As he lopes off into the distance, a cold wind begins to blow.
p.muted Missed the first part of the Winter Plot-Line? Read it <a href='http://habitrpg.wikia.com/wiki/Whats_new'>here</a>.
hr
a(href='/static/old-news', target='_blank') Read older news
mixin oldNews
h5 12/9/2014 - Penguin Pet Quest and Winter Plot-Line
tr
td
h5 Penguin Pet Quest
.quest_penguin.pull-right
p Habiticans wanted to go ice-skating, but instead, a giant penguin is freezing everything in sight! All we wanted was to go ice-skating... Can you get this penguin to chill out? If so, you'll be rewarded with some penguins of your own!
p.small.muted by Melynnrose, Breadstrings, Rattify, Painter de Cluster, Daniel the Bard, and Leephon
tr
td
h5 Winter Plot-Line
p Lemoness enters the Tavern with worrying news from the far north of Habitica. "Nobody's heard from the Stoïkalm Steppes for over a week," she says. "It's hard to imagine anything troubling the citizens there, since it's such a placid part of the continent... But just in case, maybe I should pay a visit." Sounds like a good plan to us!
h5 12/3/2014 - Gifting Subscriptions And Gems, New Subscription Benefits, Mysterious Time Travelers, Steampunk Item Sets, And Block Subscriptions!
table.table.table-striped
tr