Merge branch 'develop' into refactor-content

This commit is contained in:
Blade Barringer
2015-09-25 12:22:11 -05:00
40 changed files with 3335 additions and 3210 deletions

View File

@@ -1,15 +0,0 @@
{
"globals": {
"confirm": false
},
"browser": true,
"node": true,
"mocha": true,
"esnext": true,
"asi": true,
"boss": true,
"newcap": false
}

View File

@@ -1,6 +1,6 @@
.2014_Fall_HealerPROMO2 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -640px -843px;
background-position: -564px -843px;
width: 90px;
height: 90px;
}
@@ -18,7 +18,7 @@
}
.2014_Fall_Warrior_PROMO {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -367px -843px;
background-position: -473px -843px;
width: 90px;
height: 90px;
}
@@ -42,7 +42,7 @@
}
.promo_dilatoryDistress {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -94px -843px;
background-position: -837px -843px;
width: 90px;
height: 90px;
}
@@ -54,7 +54,7 @@
}
.promo_enchanted_armoire_201507 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -882px -641px;
background-position: -882px -550px;
width: 217px;
height: 90px;
}
@@ -66,7 +66,7 @@
}
.promo_enchanted_armoire_201509 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -276px -843px;
background-position: -291px -843px;
width: 90px;
height: 90px;
}
@@ -76,6 +76,18 @@
width: 175px;
height: 175px;
}
.promo_haunted_hair {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1023px -402px;
width: 100px;
height: 137px;
}
.customize-option. {
background-image: url();
background-position: ;
width: ;
height: ;
}
.promo_item_notif {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -882px 0px;
@@ -84,7 +96,7 @@
}
.promo_mystery_201405 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -822px -843px;
background-position: -200px -843px;
width: 90px;
height: 90px;
}
@@ -96,7 +108,7 @@
}
.promo_mystery_201407 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: 0px -934px;
background-position: -195px -949px;
width: 42px;
height: 62px;
}
@@ -108,7 +120,7 @@
}
.promo_mystery_201409 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -549px -843px;
background-position: -746px -843px;
width: 90px;
height: 90px;
}
@@ -120,13 +132,13 @@
}
.promo_mystery_201411 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -731px -843px;
background-position: -928px -843px;
width: 90px;
height: 90px;
}
.promo_mystery_201412 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1065px -843px;
background-position: -152px -949px;
width: 42px;
height: 66px;
}
@@ -138,25 +150,25 @@
}
.promo_mystery_201502 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -185px -843px;
background-position: -382px -843px;
width: 90px;
height: 90px;
}
.promo_mystery_201503 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -913px -843px;
background-position: 0px -949px;
width: 90px;
height: 90px;
}
.promo_mystery_201504 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1004px -843px;
background-position: -91px -949px;
width: 60px;
height: 69px;
}
.promo_mystery_201505 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -458px -843px;
background-position: -655px -843px;
width: 90px;
height: 90px;
}
@@ -174,19 +186,25 @@
}
.promo_mystery_201508 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: 0px -843px;
background-position: -106px -843px;
width: 93px;
height: 90px;
}
.promo_mystery_201509 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1019px -843px;
width: 90px;
height: 90px;
}
.promo_mystery_3014 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -882px -550px;
background-position: -882px -641px;
width: 217px;
height: 90px;
}
.promo_orca {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1023px -402px;
background-position: 0px -843px;
width: 105px;
height: 105px;
}
@@ -198,7 +216,7 @@
}
.promo_pastel_skin {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -331px -668px;
background-position: 0px -668px;
width: 330px;
height: 83px;
}
@@ -222,7 +240,7 @@
}
.promo_shimmer_hair {
background-image: url(spritesmith-largeSprites-0.png);
background-position: 0px -668px;
background-position: -331px -668px;
width: 330px;
height: 83px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 146 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 132 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 321 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 153 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 148 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 150 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -336,6 +336,8 @@
"armorMystery201506Notes": "Snorkel through a coral reef in this brightly-colored swim suit! Confers no benefit. June 2015 Subscriber Item.",
"armorMystery201508Text": "Cheetah Costume",
"armorMystery201508Notes": "Run fast as a flash in the fluffy Cheetah Costume! Confers no benefit. August 2015 Subscriber Item.",
"armorMystery201509Text": "Werewolf Costume",
"armorMystery201509Notes": "This IS a costume, right? Confers no benefit. September 2015 Subscriber Item.",
"armorMystery301404Text": "Steampunk Suit",
"armorMystery301404Notes": "Dapper and dashing, wot! Confers no benefit. February 3015 Subscriber Item.",
@@ -509,6 +511,8 @@
"headMystery201505Notes": "The green plume on this iron helm waves proudly. Confers no benefit. May 2015 Subscriber Item.",
"headMystery201508Text": "Cheetah Hat",
"headMystery201508Notes": "This cozy cheetah hat is very fuzzy! Confers no benefit. August 2015 Subscriber Item.",
"headMystery201509Text": "Werewolf Mask",
"headMystery201509Notes": "This IS a mask, right? Confers no benefit. September 2015 Subscriber Item.",
"headMystery301404Text": "Fancy Top Hat",
"headMystery301404Notes": "A fancy top hat for the finest of gentlefolk! January 3015 Subscriber Item. Confers no benefit.",
"headMystery301405Text": "Basic Top Hat",

View File

@@ -1834,6 +1834,11 @@ api.wrap = (user, main=true) ->
user.stats.mp += _.max([10,.1 * user._statsComputed.maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked)
user.stats.mp = user._statsComputed.maxMP if user.stats.mp > user._statsComputed.maxMP
# After all is said and done, progress up user's effect on quest, return those values & reset the user's
progress = user.party.quest.progress; _progress = _.cloneDeep progress
_.merge progress, {down:0,up:0}
progress.collect = _.transform progress.collect, ((m,v,k)->m[k]=0)
# Analytics
user.flags.cronCount?=0
user.flags.cronCount++
@@ -1845,14 +1850,12 @@ api.wrap = (user, main=true) ->
uuid: user._id,
user: user,
resting: user.preferences.sleep,
cronCount: user.flags.cronCount
cronCount: user.flags.cronCount,
progressUp: _progress.up,
progressDown: _progress.down
}
options.analytics?.track('Cron', analyticsData)
# After all is said and done, progress up user's effect on quest, return those values & reset the user's
progress = user.party.quest.progress; _progress = _.cloneDeep progress
_.merge progress, {down:0,up:0}
progress.collect = _.transform progress.collect, ((m,v,k)->m[k]=0)
_progress
# Registered users with some history

View File

@@ -593,8 +593,13 @@ let head = {
text: t('headMystery201508Text'),
notes: t('headMystery201508Notes'),
mystery: '201508',
value: 0,
int: 0
value: 0
},
201509: {
text: t('headMystery201509Text'),
notes: t('headMystery201509Notes'),
mystery:'201509',
value: 0
},
301404: {
text: t('headMystery301404Text'),

View File

@@ -84,8 +84,13 @@ export var armor = {
text: t('armorMystery201508Text'),
notes: t('armorMystery201508Notes'),
mystery: '201508',
value: 0,
int: 0
value: 0
},
201509: {
text: t('armorMystery201509Text'),
notes: t('armorMystery201509Notes'),
mystery: '201509',
value: 0
},
301404: {
text: t('armorMystery301404Text'),

View File

@@ -97,6 +97,11 @@ let mysterySets = {
end: '2015-09-02',
text: 'Cheetah Costume Set'
},
201509: {
start:'2015-09-24',
end:'2015-10-02',
text:'Werewolf Set'
},
301404: {
start: '3014-03-24',
end: '3014-04-02',

View File

@@ -2,7 +2,7 @@ var _id = '';
var update = {
$addToSet: {
'purchased.plan.mysteryItems':{
$each:['head_mystery_201508','armor_mystery_201508']
$each:['head_mystery_201509','armor_mystery_201509']
}
}
};

View File

@@ -34,6 +34,7 @@
"grunt-karma": "~0.6.2",
"gulp": "^3.9.0",
"gulp-clean": "^0.3.1",
"gulp-eslint": "^1.0.0",
"gulp-grunt": "^0.5.2",
"gulp-imagemin": "^2.3.0",
"gulp-nodemon": "^2.0.4",

45
tasks/gulp-eslint.js Normal file
View File

@@ -0,0 +1,45 @@
import gulp from 'gulp';
import eslint from 'gulp-eslint';
import _ from 'lodash';
// TODO remove once we upgrade to lodash 3
const defaultsDeep = _.partialRight(_.merge, _.defaults);
const shared = {
rules: {
indent: [2, 2],
quotes: [2, 'single'],
'linebreak-style': [2, 'unix'],
semi: [2, 'always']
},
extends: 'eslint:recommended',
env: {
es6: true
}
};
gulp.task('lint:client', () => {
// Ignore .coffee files
return gulp.src(['./website/public/js/**/*.js'])
.pipe(eslint(defaultsDeep({
env: {
node: true
}
}, shared)))
.pipe(eslint.format())
.pipe(eslint.failAfterError());
});
gulp.task('lint:server', () => {
// Ignore .coffee files
return gulp.src(['./website/src/**/*.js'])
.pipe(eslint(defaultsDeep({
env: {
browser: true
}
}, shared)))
.pipe(eslint.format())
.pipe(eslint.failAfterError());
});
gulp.task('lint', ['lint:server', 'lint:client']);

View File

@@ -7,7 +7,7 @@ var User = require('mongoose').model('User');
var shared = require('../../../../common');
var payments = require('./index');
var cc = require('coupon-code');
var isProd = nconf.get("NODE_ENV") === 'production';
var isProd = nconf.get('NODE_ENV') === 'production';
var amzPayment = amazonPayments.connect({
environment: amazonPayments.Environment[isProd ? 'Production' : 'Sandbox'],
@@ -122,7 +122,7 @@ exports.checkout = function(req, res, next){
executePayment: function(cb){
async.waterfall([
function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2) },
function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2); },
function(member, cb2){
var data = {user:user, paymentMethod:'Amazon Payments'};
var method = 'buyGems';
@@ -151,7 +151,7 @@ exports.subscribe = function(req, res, next){
return res.json(400, {err: 'Billing Agreement Id not supplied.'});
}
var billingAgreementId = req.body.billingAgreementId
var billingAgreementId = req.body.billingAgreementId;
var sub = req.body.subscription ? shared.content.subscriptionBlocks[req.body.subscription] : false;
var coupon = req.body.coupon;
var user = res.locals.user;
@@ -167,7 +167,7 @@ exports.subscribe = function(req, res, next){
mongoose.model('Coupon').findOne({_id:cc.validate(coupon), event:sub.key}, function(err, coupon){
if(err) return cb(err);
if(!coupon) return cb(new Error('Coupon code not found.'));
cb()
cb();
});
},
@@ -236,7 +236,7 @@ exports.subscribe = function(req, res, next){
exports.subscribeCancel = function(req, res, next){
var user = res.locals.user;
if (!user.purchased.plan.customerId)
return res.json(401, {err: "User does not have a plan subscription"});
return res.json(401, {err: 'User does not have a plan subscription'});
var billingAgreementId = user.purchased.plan.customerId;

View File

@@ -6,7 +6,7 @@ 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")
googlePublicKeyPath: nconf.get('IAP_GOOGLE_KEYDIR')
});
// Validation ERROR Codes
@@ -101,7 +101,7 @@ 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") {
if (purchaseDataList[0].productId === 'com.habitrpg.ios.Habitica.20gems') {
//Correct receipt
payments.buyGems({user:user, paymentMethod:'IAP AppleStore'});
var resObj = {
@@ -117,7 +117,7 @@ exports.iosVerify = function(req, res, next) {
ok: false,
data: {
code: INVALID_PAYLOAD,
message: "Incorrect receipt content"
message: 'Incorrect receipt content'
}
};
return res.json(resObj);
@@ -127,7 +127,7 @@ exports.iosVerify = function(req, res, next) {
ok: false,
data: {
code: INVALID_PAYLOAD,
message: "Invalid receipt"
message: 'Invalid receipt'
}
};

View File

@@ -1,5 +1,5 @@
var nconf = require('nconf');
var stripe = require("stripe")(nconf.get('STRIPE_API_KEY'));
var stripe = require('stripe')(nconf.get('STRIPE_API_KEY'));
var async = require('async');
var payments = require('./index');
var User = require('mongoose').model('User');
@@ -38,10 +38,10 @@ exports.checkout = function(req, res, next) {
], cb);
} else {
stripe.charges.create({
amount: !gift ? "500" //"500" = $5
: gift.type=='subscription' ? ""+shared.content.subscriptionBlocks[gift.subscription.key].price*100
: ""+gift.gems.amount/4*100,
currency: "usd",
amount: !gift ? '500' //"500" = $5
: gift.type=='subscription' ? ''+shared.content.subscriptionBlocks[gift.subscription.key].price*100
: ''+gift.gems.amount/4*100,
currency: 'usd',
card: token
}, cb);
}
@@ -49,7 +49,7 @@ exports.checkout = function(req, res, next) {
function(response, cb) {
if (sub) return payments.createSubscription({user:user, customerId:response.id, paymentMethod:'Stripe', sub:sub}, cb);
async.waterfall([
function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2) },
function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2); },
function(member, cb2){
var data = {user:user, customerId:response.id, paymentMethod:'Stripe', gift:gift};
var method = 'buyGems';
@@ -72,7 +72,7 @@ exports.checkout = function(req, res, next) {
exports.subscribeCancel = function(req, res, next) {
var user = res.locals.user;
if (!user.purchased.plan.customerId)
return res.json(401, {err: "User does not have a plan subscription"});
return res.json(401, {err: 'User does not have a plan subscription'});
async.auto({
get_cus: function(cb){

View File

@@ -22,7 +22,7 @@ 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();
});
}, mins*60*1000);
}

View File

@@ -1,7 +1,7 @@
var nconf = require('nconf');
var IS_PROD = nconf.get('NODE_ENV') === 'production';
var ignoreRedirect = nconf.get('IGNORE_REDIRECT');
var BASE_URL = nconf.get("BASE_URL");
var BASE_URL = nconf.get('BASE_URL');
function isHTTP(req) {
return (

View File

@@ -10,7 +10,7 @@ var i18n = require('../i18n');
// -------- App --------
router.get('/', i18n.getUserLanguage, locals, function(req, res) {
if (!req.headers['x-api-user'] && !req.headers['x-api-key'] && !(req.session && req.session.userId))
return res.redirect('/static/front')
return res.redirect('/static/front');
return res.render('index', {
title: 'Habitica | Your Life The Role Playing Game',
@@ -29,7 +29,7 @@ _.each(pages, function(name){
marked: require('marked')
});
});
})
});
// --------- Redirects --------

View File

@@ -12,10 +12,10 @@ router.get('/paypal/subscribe/success', i18n.getUserLanguage, payments.paypalSub
router.get('/paypal/subscribe/cancel', auth.authWithUrl, i18n.getUserLanguage, payments.paypalSubscribeCancel);
router.post('/paypal/ipn', i18n.getUserLanguage, payments.paypalIPN); // misc ipn handling
router.post("/stripe/checkout", auth.auth, i18n.getUserLanguage, payments.stripeCheckout);
router.post("/stripe/subscribe/edit", auth.auth, i18n.getUserLanguage, payments.stripeSubscribeEdit)
//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('/stripe/checkout', auth.auth, i18n.getUserLanguage, payments.stripeCheckout);
router.post('/stripe/subscribe/edit', auth.auth, i18n.getUserLanguage, payments.stripeSubscribeEdit);
//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('/amazon/verifyAccessToken', auth.auth, i18n.getUserLanguage, payments.amazonVerifyAccessToken);
router.post('/amazon/createOrderReferenceId', auth.auth, i18n.getUserLanguage, payments.amazonCreateOrderReferenceId);
@@ -23,9 +23,9 @@ router.post('/amazon/checkout', auth.auth, i18n.getUserLanguage, payments.amazon
router.post('/amazon/subscribe', auth.auth, i18n.getUserLanguage, payments.amazonSubscribe);
router.get('/amazon/subscribe/cancel', auth.authWithUrl, i18n.getUserLanguage, payments.amazonSubscribeCancel);
router.post("/iap/android/verify", auth.authWithUrl, /*i18n.getUserLanguage, */payments.iapAndroidVerify);
router.post("/iap/ios/verify", auth.auth, /*i18n.getUserLanguage, */ payments.iapIosVerify);
router.post('/iap/android/verify', auth.authWithUrl, /*i18n.getUserLanguage, */payments.iapAndroidVerify);
router.post('/iap/ios/verify', auth.auth, /*i18n.getUserLanguage, */ payments.iapIosVerify);
router.get("/api/v2/coupons/valid-discount/:code", /*auth.authWithUrl, i18n.getUserLanguage, */ payments.validCoupon);
router.get('/api/v2/coupons/valid-discount/:code', /*auth.authWithUrl, i18n.getUserLanguage, */ payments.validCoupon);
module.exports = router;

View File

@@ -84,7 +84,7 @@ mixin customizeProfile(mobile)
button(type='button', ng-if='user.purchased.hair.color.#{color}', class='customize-option hair hair_bangs_1_#{color}', ng-click='unlock("hair.color.#{color}")', ng-class='{selectableInventory: user.preferences.hair.color == "#{color}"}')
+buyPref('hair.color', ['rainbow','yellow','green','purple','blue','TRUred'], 'rainbowColors')
+buyPref('hair.color', ['pblue2','pgreen2','porange2','ppink2','ppurple2','pyellow2'], 'shimmerColors', 'disabled')
+buyPref('hair.color', ['candycorn','ghostwhite','halloween','midnight','pumpkin','zombie'], 'hauntedColors', 'disabled')
+buyPref('hair.color', ['candycorn','ghostwhite','halloween','midnight','pumpkin','zombie'], 'hauntedColors')
+buyPref('hair.color', ['aurora','festive','hollygreen','peppermint','snowy','winterstar'], 'winteryColors', 'disabled')
li.customize-menu
@@ -152,7 +152,7 @@ mixin customizeProfile(mobile)
// Seasonal event skins. Note that Spooky Skins are a legacy set and should always be disabled for purchase
+buyPref('skin', ['pastelPink','pastelOrange','pastelYellow','pastelGreen','pastelBlue','pastelPurple','pastelRainbowChevron','pastelRainbowDiagonal'], 'pastelSkins', 'disabled')
+buyPref('skin', ['monster','pumpkin','skeleton','zombie','ghost','shadow'], 'spookySkins', 'disabled')
+buyPref('skin', ['candycorn','ogre','pumpkin2','reptile','shadow2','skeleton2','transparent','zombie2'], 'supernaturalSkins', 'disabled')
+buyPref('skin', ['candycorn','ogre','pumpkin2','reptile','shadow2','skeleton2','transparent','zombie2'], 'supernaturalSkins')
+buyPref('skin', ['clownfish','deepocean','merblue','mergold','mergreen','merruby','shark','tropicalwater'], 'splashySkins', 'disabled')

View File

@@ -1,5 +1,36 @@
h2 9/21/2015 - FALL FESTIVAL! LIMITED-EDITION OUTFITS, SEASONAL SHOP, CANDY FOOD DROPS, AND NPC DRESS-UP
h2 9/24/2015 - HAUNTED HAIR COLORS, SUPERNATURAL SKIN SET, AND WEREWOLF SUBSCRIBER ITEM! PLUS, THE FALL PLOT-LINE CONTINUES...
hr
tr
td
h3 Haunted Hair Colors and Supernatural Skin Set
.promo_haunted_hair.pull-right
p The Seasonal Edition Haunted Hair Colors are now available for purchase in <a href='/#/options/profile/avatar'>the avatar customizations page</a>! Now you can dye your avatar's hair Pumpkin, Midnight, Candy Corn, Ghost White, Zombie, or Halloween. Get them before October 31st!
br
p The Supernatural Skin Set is also available until October 31st! Now your avatar can become an Ogre, Skeleton, Pumpkin, Candy Corn, Reptile, or Dread Shade.
br
p Seasonal Edition items recur unchanged every year, but they are only available to purchase during a short period of time. Get them now, or you'll have to wait until next year!
p.small.muted by Lemoness, mariahm, and crystal phoenix
tr
td
h3 September Subscriber Items Revealed
.promo_mystery_201509.pull-right
p The September Subscriber Item has been revealed: the Werewolf Armor Set! All September subscribers will receive the Werewolf Mask and the Werewolf Costume. You still have six 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
tr
td
h3 Fall Plot-Line Continues
p In general, we've all been enjoying the Flourishing Fields. Habiticans are posing in fun costumes, taking pictures of the orange-and-black wildlife, and casting Spooky Sparkles on each other.
br
p Unfortunately, there does seem to be a serious problem with production for the first time in the history of the Fields. Deadlines are being missed. Shipments are not arriving. As you walk down the street, you overhear worried whispers among the citizens, speculating on the cause.
br
p Some blame the unseasonal heat wave that has begun in the past few days. Others point to the difficulty of the tasks, and their ever-increasing quantity. And a few people -- just a few -- murmur that some of the hardest-working citizens have been disappearing, one by one, leaving their obligations abandoned. But surely that is nothing more than rumor?
if menuItem !== 'oldNews'
hr
a(href='/static/old-news', target='_blank') Read older news
mixin oldNews
h2 9/21/2015 - FALL FESTIVAL! LIMITED-EDITION OUTFITS, SEASONAL SHOP, CANDY FOOD DROPS, AND NPC DRESS-UP
tr
td
h3 Fall Festival Begins
@@ -28,14 +59,7 @@ h2 9/21/2015 - FALL FESTIVAL! LIMITED-EDITION OUTFITS, SEASONAL SHOP, CANDY FOOD
h3 NPC Dress Up
p The NPCs have decided to blend in with the locals of the Flourishing Fields by dressing up for the Fall Festival! Browse through the site to admire their new costumes.
p.small.muted by Lemoness
if menuItem !== 'oldNews'
hr
a(href='/static/old-news', target='_blank') Read older news
mixin oldNews
h2 9/16/2015 - MAMMOTHS AND MANTIS SHRIMPS IN TIME TRAVELER SHOP! PLUS, FALL FESTIVAL PLOT-LINE CONTINUES
hr
tr
td
.Pet-MantisShrimp-Base.pull-right