Merge branch 'develop' into api-v3

This commit is contained in:
Matteo Pagliazzi
2015-11-27 17:05:29 +01:00
56 changed files with 3747 additions and 3338 deletions

View File

@@ -1,6 +1,6 @@
.2014_Fall_HealerPROMO2 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -731px -995px;
background-position: -822px -995px;
width: 90px;
height: 90px;
}
@@ -18,7 +18,7 @@
}
.2014_Fall_Warrior_PROMO {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1095px -995px;
background-position: -276px -995px;
width: 90px;
height: 90px;
}
@@ -48,7 +48,7 @@
}
.promo_dilatoryDistress {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -367px -995px;
background-position: -458px -995px;
width: 90px;
height: 90px;
}
@@ -72,7 +72,7 @@
}
.promo_enchanted_armoire_201509 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -458px -995px;
background-position: -549px -995px;
width: 90px;
height: 90px;
}
@@ -114,7 +114,7 @@
}
.promo_mystery_201405 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1004px -995px;
background-position: -1095px -995px;
width: 90px;
height: 90px;
}
@@ -138,7 +138,7 @@
}
.promo_mystery_201409 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -640px -995px;
background-position: -731px -995px;
width: 90px;
height: 90px;
}
@@ -150,7 +150,7 @@
}
.promo_mystery_201411 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -822px -995px;
background-position: -913px -995px;
width: 90px;
height: 90px;
}
@@ -168,13 +168,13 @@
}
.promo_mystery_201502 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -276px -995px;
background-position: -367px -995px;
width: 90px;
height: 90px;
}
.promo_mystery_201503 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: 0px -1101px;
background-position: -91px -1101px;
width: 90px;
height: 90px;
}
@@ -186,7 +186,7 @@
}
.promo_mystery_201505 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -549px -995px;
background-position: -640px -995px;
width: 90px;
height: 90px;
}
@@ -210,7 +210,7 @@
}
.promo_mystery_201509 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -913px -995px;
background-position: -1004px -995px;
width: 90px;
height: 90px;
}
@@ -220,6 +220,12 @@
width: 93px;
height: 90px;
}
.promo_mystery_201511 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: 0px -1101px;
width: 90px;
height: 90px;
}
.promo_mystery_3014 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -943px -764px;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 194 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

After

Width:  |  Height:  |  Size: 387 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 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: 144 KiB

After

Width:  |  Height:  |  Size: 143 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 142 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 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.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -342,6 +342,8 @@
"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.",
"armorMystery201511Text": "Wooden Armor",
"armorMystery201511Notes": "Considering this armor was carved directly from a magical log, it's surprisingly comfortable. Confers no benefit. November 2015 Subscriber Item.",
"armorMystery301404Text": "Steampunk Suit",
"armorMystery301404Notes": "Dapper and dashing, wot! Confers no benefit. February 3015 Subscriber Item.",
@@ -521,6 +523,8 @@
"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.",
"headMystery201511Text": "Log Crown",
"headMystery201511Notes": "Count the number of rings to learn how old this crown is. Confers no benefit. November 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

@@ -21,6 +21,7 @@
"valentineCardAchievementText": "Aww, you and your friend must really care about each other! Sent or received <%= cards %> Valentine's Day cards.",
"polarBear": "Polar Bear",
"turkey": "Turkey",
"gildedTurkey": "Gilded Turkey",
"polarBearPup": "Polar Bear Cub",
"jackolantern": "Jack-O-Lantern",
"seasonalShop": "Seasonal Shop",

View File

@@ -144,5 +144,8 @@
"gemCapExtra": "Gem Cap Extra:",
"mysticHourglasses": "Mystic Hourglasses:",
"paypal": "PayPal",
"amazonPayments": "Amazon Payments"
"amazonPayments": "Amazon Payments",
"timezone": "Time Zone",
"timezoneUTC": "Habitica uses the time zone set on your PC, which is: <strong><%= utc %></strong>",
"timezoneInfo": "If that time zone is wrong, first reload this page using your browser's reload or refresh button to ensure that Habitica has the most recent information. If it is still wrong, adjust the time zone on your PC and then reload this page again.<br><br> <strong>If you use Habitica on other PCs or mobile devices, the time zone must be the same on them all.</strong> If your Dailies have been reseting at the wrong time, repeat this check on all other PCs and on a browser on your mobile devices."
}

View File

@@ -91,6 +91,12 @@ let armor = {
mystery: '201509',
value: 0,
},
201511: {
text: t('armorMystery201511Text'),
notes: t('armorMystery201511Notes'),
mystery: '201511',
value: 0,
},
301404: {
text: t('armorMystery301404Text'),
notes: t('armorMystery301404Notes'),
@@ -238,6 +244,12 @@ let head = {
mystery: '201509',
value: 0,
},
201511: {
text: t('headMystery201511Text'),
notes: t('headMystery201511Notes'),
mystery: '201511',
value: 0,
},
301404: {
text: t('headMystery301404Text'),
notes: t('headMystery301404Notes'),

View File

@@ -952,7 +952,8 @@ api.specialPets = {
'JackOLantern-Base': 'jackolantern',
'Mammoth-Base': 'mammoth',
'Tiger-Veteran': 'veteranTiger',
'Phoenix-Base': 'phoenix'
'Phoenix-Base': 'phoenix',
'Turkey-Gilded': 'gildedTurkey',
};
api.specialMounts = {

View File

@@ -106,6 +106,11 @@ let mysterySets = {
end: '2015-11-02',
text: 'Horned Goblin Set',
},
201511: {
start: '2015-11-25',
end: '2015-12-02',
text: 'Wood Warrior Set',
},
301404: {
start: '3014-03-24',
end: '3014-04-02',

110
common/script/cron.js Normal file
View File

@@ -0,0 +1,110 @@
/*
------------------------------------------------------
Cron and time / day functions
------------------------------------------------------
*/
import _ from 'lodash';
import moment from 'moment';
export const DAY_MAPPING = {
0: 'su',
1: 'm',
2: 't',
3: 'w',
4: 'th',
5: 'f',
6: 's',
};
/*
Each time we perform date maths (cron, task-due-days, etc), we need to consider user preferences.
Specifically {dayStart} (custom day start) and {timezoneOffset}. This function sanitizes / defaults those values.
{now} is also passed in for various purposes, one example being the test scripts scripts testing different "now" times.
*/
function sanitizeOptions (o) {
let ref = Number(o.dayStart || 0);
let dayStart = !_.isNaN(ref) && ref >= 0 && ref <= 24 ? ref : 0;
let timezoneOffset = o.timezoneOffset ? Number(o.timezoneOffset) : Number(moment().zone());
// TODO: check and clean timezoneOffset as for dayStart (e.g., might not be a number)
let now = o.now ? moment(o.now).zone(timezoneOffset) : moment().zone(timezoneOffset);
// return a new object, we don't want to add "now" to user object
return {
dayStart,
timezoneOffset,
now,
};
}
export function startOfWeek (options = {}) {
let o = sanitizeOptions(options);
return moment(o.now).startOf('week');
}
/*
This is designed for use with any date that has an important time portion (e.g., when comparing the current date-time with the previous cron's date-time for determing if cron should run now).
It changes the time portion of the date-time to be the Custom Day Start hour, so that the date-time is now the user's correct start of day.
It SUBTRACTS a day if the date-time's original hour is before CDS (e.g., if your CDS is 5am and it's currently 4am, it's still the previous day).
This is NOT suitable for manipulating any dates that are displayed to the user as a date with no time portion, such as a Daily's Start Dates (e.g., a Start Date of today shows only the date, so it should be considered to be today even if the hidden time portion is before CDS).
*/
export function startOfDay (options = {}) {
let o = sanitizeOptions(options);
let dayStart = moment(o.now).startOf('day').add({ hours: o.dayStart });
if (moment(o.now).hour() < o.dayStart) {
dayStart.subtract({ days: 1 });
}
return dayStart;
}
/*
Absolute diff from "yesterday" till now
*/
export function daysSince (yesterday, options = {}) {
let o = sanitizeOptions(options);
return startOfDay(_.defaults({ now: o.now }, o)).diff(startOfDay(_.defaults({ now: yesterday }, o)), 'days');
}
/*
Should the user do this task on this date, given the task's repeat options and user.preferences.dayStart?
*/
export function shouldDo (day, dailyTask, options = {}) {
if (dailyTask.type !== 'daily') {
return false;
}
let o = sanitizeOptions(options);
let startOfDayWithCDSTime = startOfDay(_.defaults({ now: day }, o));
// The time portion of the Start Date is never visible to or modifiable by the user so we must ignore it.
// Therefore, we must also ignore the time portion of the user's day start (startOfDayWithCDSTime), otherwise the date comparison will be wrong for some times.
// NB: The user's day start date has already been converted to the PREVIOUS day's date if the time portion was before CDS.
let taskStartDate = moment(dailyTask.startDate).zone(o.timezoneOffset);
taskStartDate = moment(taskStartDate).startOf('day');
if (taskStartDate > startOfDayWithCDSTime.startOf('day')) {
return false; // Daily starts in the future
}
if (dailyTask.frequency === 'daily') { // "Every X Days"
if (!dailyTask.everyX) {
return false; // error condition
}
let daysSinceTaskStart = startOfDayWithCDSTime.startOf('day').diff(taskStartDate, 'days');
return daysSinceTaskStart % dailyTask.everyX === 0;
} else if (dailyTask.frequency === 'weekly') { // "On Certain Days of the Week"
if (!dailyTask.repeat) {
return false; // error condition
}
let dayOfWeekNum = startOfDayWithCDSTime.day(); // e.g., 0 for Sunday
return dailyTask.repeat[DAY_MAPPING[dayOfWeekNum]];
} else {
return false; // error condition - unexpected frequency string
}
}

View File

@@ -1,4 +1,10 @@
var $w, _, api, content, i18n, moment, preenHistory, sanitizeOptions, sortOrder,
import {
daysSince,
shouldDo,
} from '../../common/script/cron';
import * as statHelpers from './statHelpers';
var $w, _, api, content, i18n, moment, preenHistory, sortOrder,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
moment = require('moment');
@@ -12,6 +18,13 @@ i18n = require('./i18n');
api = module.exports = {};
api.i18n = i18n;
api.shouldDo = shouldDo;
api.maxLevel = statHelpers.MAX_LEVEL;
api.capByLevel = statHelpers.capByLevel;
api.maxHealth = statHelpers.MAX_HEALTH;
api.tnl = statHelpers.toNextLevel;
api.diminishingReturns = statHelpers.diminishingReturns;
$w = api.$w = function(s) {
return s.split(' ');
@@ -61,184 +74,6 @@ api.planGemLimits = {
convCap: 25
};
/*
------------------------------------------------------
Time / Day
------------------------------------------------------
*/
/*
Each time we're performing date math (cron, task-due-days, etc), we need to take user preferences into consideration.
Specifically {dayStart} (custom day start) and {timezoneOffset}. This function sanitizes / defaults those values.
{now} is also passed in for various purposes, one example being the test scripts scripts testing different "now" times
*/
sanitizeOptions = function(o) {
var dayStart, now, ref, timezoneOffset;
dayStart = !_.isNaN(+o.dayStart) && (0 <= (ref = +o.dayStart) && ref <= 24) ? +o.dayStart : 0;
timezoneOffset = o.timezoneOffset ? +o.timezoneOffset : +moment().zone();
now = o.now ? moment(o.now).zone(timezoneOffset) : moment(+(new Date)).zone(timezoneOffset);
return {
dayStart: dayStart,
timezoneOffset: timezoneOffset,
now: now
};
};
api.startOfWeek = api.startOfWeek = function(options) {
var o;
if (options == null) {
options = {};
}
o = sanitizeOptions(options);
return moment(o.now).startOf('week');
};
api.startOfDay = function(options) {
var dayStart, o;
if (options == null) {
options = {};
}
o = sanitizeOptions(options);
dayStart = moment(o.now).startOf('day').add({
hours: o.dayStart
});
if (moment(o.now).hour() < o.dayStart) {
dayStart.subtract({
days: 1
});
}
return dayStart;
};
api.dayMapping = {
0: 'su',
1: 'm',
2: 't',
3: 'w',
4: 'th',
5: 'f',
6: 's'
};
/*
Absolute diff from "yesterday" till now
*/
api.daysSince = function(yesterday, options) {
var o;
if (options == null) {
options = {};
}
o = sanitizeOptions(options);
return api.startOfDay(_.defaults({
now: o.now
}, o)).diff(api.startOfDay(_.defaults({
now: yesterday
}, o)), 'days');
};
/*
Should the user do this task on this date, given the task's repeat options and user.preferences.dayStart?
*/
api.shouldDo = function(day, dailyTask, options) {
var dayOfWeekCheck, dayOfWeekNum, daysSinceTaskStart, everyXCheck, o, startOfDayWithCDSTime, taskStartDate;
if (options == null) {
options = {};
}
if (dailyTask.type !== 'daily') {
return false;
}
o = sanitizeOptions(options);
startOfDayWithCDSTime = api.startOfDay(_.defaults({
now: day
}, o));
taskStartDate = moment(dailyTask.startDate).zone(o.timezoneOffset);
taskStartDate = moment(taskStartDate).startOf('day');
if (taskStartDate > startOfDayWithCDSTime.startOf('day')) {
return false;
}
if (dailyTask.frequency === 'daily') {
if (!dailyTask.everyX) {
return false;
}
daysSinceTaskStart = startOfDayWithCDSTime.startOf('day').diff(taskStartDate, 'days');
everyXCheck = daysSinceTaskStart % dailyTask.everyX === 0;
return everyXCheck;
} else if (dailyTask.frequency === 'weekly') {
if (!dailyTask.repeat) {
return false;
}
dayOfWeekNum = startOfDayWithCDSTime.day();
dayOfWeekCheck = dailyTask.repeat[api.dayMapping[dayOfWeekNum]];
return dayOfWeekCheck;
} else {
return false;
}
};
/*
------------------------------------------------------
Level cap
------------------------------------------------------
*/
api.maxLevel = 100;
api.capByLevel = function(lvl) {
if (lvl > api.maxLevel) {
return api.maxLevel;
} else {
return lvl;
}
};
/*
------------------------------------------------------
Health cap
------------------------------------------------------
*/
api.maxHealth = 50;
/*
------------------------------------------------------
Scoring
------------------------------------------------------
*/
api.tnl = function(lvl) {
return Math.round(((Math.pow(lvl, 2) * 0.25) + (10 * lvl) + 139.75) / 10) * 10;
};
/*
A hyperbola function that creates diminishing returns, so you can't go to infinite (eg, with Exp gain).
{max} The asymptote
{bonus} All the numbers combined for your point bonus (eg, task.value * user.stats.int * critChance, etc)
{halfway} (optional) the point at which the graph starts bending
*/
api.diminishingReturns = function(bonus, max, halfway) {
if (halfway == null) {
halfway = max / 2;
}
return max * (bonus / (bonus + halfway));
};
api.monod = function(bonus, rateOfIncrease, max) {
return rateOfIncrease * max * bonus / (rateOfIncrease * bonus + max);
};
/*
Preen history for users with > 7 history entries
This takes an infinite array of single day entries [day day day day day...], and turns it into a condensed array
@@ -293,7 +128,6 @@ api.preenTodos = function(tasks) {
});
};
/*
Update the in-browser store with new gear. FIXME this was in user.fns, but it was causing strange issues there
*/
@@ -535,7 +369,7 @@ api.taskClasses = function(task, filters, dayStart, lastCron, showCompleted, mai
classes += " beingEdited";
}
if (type === 'todo' || type === 'daily') {
if (completed || (type === 'daily' && !api.shouldDo(+(new Date), task, {
if (completed || (type === 'daily' && !shouldDo(+(new Date), task, {
dayStart: dayStart
}))) {
classes += " completed";
@@ -861,6 +695,7 @@ api.wrap = function(user, main) {
_.each(['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp'], function(value) {
return stats[value] = 0;
});
// TODO during refactoring: move all gear code from rebirth() to its own function and then call it in reset() as well
gear = user.items.gear;
_.each(['equipped', 'costume'], function(type) {
gear[type] = {};
@@ -1558,9 +1393,9 @@ api.wrap = function(user, main) {
} else {
if (user.preferences.autoEquip) {
user.items.gear.equipped[item.type] = item.key;
message = user.fns.handleTwoHanded(item, null, req);
}
user.items.gear.owned[item.key] = true;
message = user.fns.handleTwoHanded(item, null, req);
if (message == null) {
message = i18n.t('messageBought', {
itemText: item.text(req.language)
@@ -2326,7 +2161,7 @@ api.wrap = function(user, main) {
}
}
dropMultiplier = ((ref1 = user.purchased) != null ? (ref2 = ref1.plan) != null ? ref2.customerId : void 0 : void 0) ? 2 : 1;
if ((api.daysSince(user.items.lastDrop.date, user.preferences) === 0) && (user.items.lastDrop.count >= dropMultiplier * (5 + Math.floor(user._statsComputed.per / 25) + (user.contributor.level || 0)))) {
if ((daysSince(user.items.lastDrop.date, user.preferences) === 0) && (user.items.lastDrop.count >= dropMultiplier * (5 + Math.floor(user._statsComputed.per / 25) + (user.contributor.level || 0)))) {
return;
}
if (((ref3 = user.flags) != null ? ref3.dropsEnabled : void 0) && user.fns.predictableRandom(user.stats.exp) < chance) {
@@ -2533,7 +2368,7 @@ api.wrap = function(user, main) {
options = {};
}
now = +options.now || +(new Date);
daysMissed = api.daysSince(user.lastCron, _.defaults({
daysMissed = daysSince(user.lastCron, _.defaults({
now: now
}, user.preferences));
if (!(daysMissed > 0)) {
@@ -2599,7 +2434,7 @@ api.wrap = function(user, main) {
thatDay = moment(now).subtract({
days: 1
});
if (api.shouldDo(thatDay.toDate(), daily, user.preferences) || completed) {
if (shouldDo(thatDay.toDate(), daily, user.preferences) || completed) {
_.each(daily.checklist, (function(box) {
box.completed = false;
return true;
@@ -2653,7 +2488,7 @@ api.wrap = function(user, main) {
thatDay = moment(now).subtract({
days: n + 1
});
if (api.shouldDo(thatDay.toDate(), task, user.preferences)) {
if (shouldDo(thatDay.toDate(), task, user.preferences)) {
scheduleMisses++;
if (user.stats.buffs.stealth) {
user.stats.buffs.stealth--;

View File

@@ -0,0 +1,44 @@
/*
------------------------------------------------------
Level cap
------------------------------------------------------
*/
export const MAX_LEVEL = 100;
export function capByLevel (lvl) {
if (lvl > MAX_LEVEL) {
return MAX_LEVEL;
} else {
return lvl;
}
}
/*
------------------------------------------------------
Health cap
------------------------------------------------------
*/
export const MAX_HEALTH = 50;
/*
------------------------------------------------------
Scoring
------------------------------------------------------
*/
export function toNextLevel (lvl) {
return Math.round((Math.pow(lvl, 2) * 0.25 + 10 * lvl + 139.75) / 10) * 10;
}
/*
A hyperbola function that creates diminishing returns, so you can't go to infinite (eg, with Exp gain).
{max} The asymptote
{bonus} All the numbers combined for your point bonus (eg, task.value * user.stats.int * critChance, etc)
{halfway} (optional) the point at which the graph starts bending
*/
export function diminishingReturns (bonus, max, halfway = max / 2) {
return max * (bonus / (bonus + halfway));
}

View File

@@ -0,0 +1,71 @@
var migrationName = '20151125_turkey_ladder.js';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Award Gilded Turkey pet to Turkey mount owners, Turkey Mount if they only have Turkey Pet,
* and Turkey Pet otherwise
*/
var dbserver = 'localhost:27017'; // FOR TEST DATABASE
// var dbserver = 'username:password@ds031379-a0.mongolab.com:31379'; // FOR PRODUCTION DATABASE
var dbname = 'habitrpg';
var mongo = require('mongoskin');
var _ = require('lodash');
var dbUsers = mongo.db(dbserver + '/' + dbname + '?auto_reconnect').collection('users');
// specify a query to limit the affected users (empty for all users):
var query = {
};
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
'items.pets.Turkey-Base': 1,
'items.mounts.Turkey-Base': 1
};
console.warn('Updating users...');
var progressCount = 1000;
var count = 0;
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
console.warn('All appropriate users found and modified.');
return displayData();
}
count++;
// specify user data to change:
var set = {};
if (user.items.mounts['Turkey-Base']) {
set = {'migration':migrationName, 'items.pets.Turkey-Gilded':5};
} else if (user.items.pets['Turkey-Base']) {
set = {'migration':migrationName, 'items.mounts.Turkey-Base':true};
} else {
set = {'migration':migrationName, 'items.pets.Turkey-Base':5};
}
dbUsers.update({_id:user._id}, {$set:set});
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
});
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}

View File

@@ -2,7 +2,7 @@ var _id = '';
var update = {
$addToSet: {
'purchased.plan.mysteryItems':{
$each:['headAccessory_mystery_201510','back_mystery_201510']
$each:['head_mystery_201511','armor_mystery_201511']
}
}
};

View File

@@ -1,4 +1,11 @@
/* eslint-disable camelcase, func-names, no-shadow */
import {
DAY_MAPPING,
startOfWeek,
startOfDay,
daysSince,
} from '../../common/script/cron';
let expect = require('expect.js');
let sinon = require('sinon');
let moment = require('moment');
@@ -205,7 +212,7 @@ let repeatWithoutLastWeekday = () => {
s: true,
};
if (shared.startOfWeek(moment().zone(0)).isoWeekday() === 1) {
if (startOfWeek(moment().zone(0)).isoWeekday() === 1) {
repeat.su = false;
} else {
repeat.s = false;
@@ -301,7 +308,7 @@ describe('User', () => {
let yesterday = moment().subtract(1, 'days');
user.dailys[0].repeat[shared.dayMapping[yesterday.day()]] = false;
user.dailys[0].repeat[DAY_MAPPING[yesterday.day()]] = false;
_.each(user.dailys.slice(1), (d) => {
d.completed = true;
});
@@ -383,7 +390,7 @@ describe('User', () => {
it('does not reset checklist on grey incomplete dailies', () => {
let yesterday = moment().subtract(1, 'days');
user.dailys[0].repeat[shared.dayMapping[yesterday.day()]] = false;
user.dailys[0].repeat[DAY_MAPPING[yesterday.day()]] = false;
user.dailys[0].checklist = [
{
text: '1',
@@ -407,7 +414,7 @@ describe('User', () => {
it('resets checklist on complete grey complete dailies', () => {
let yesterday = moment().subtract(1, 'days');
user.dailys[0].repeat[shared.dayMapping[yesterday.day()]] = false;
user.dailys[0].repeat[DAY_MAPPING[yesterday.day()]] = false;
user.dailys[0].checklist = [
{
text: '1',
@@ -1185,7 +1192,7 @@ describe('Cron', () => {
let fstr = 'YYYY-MM-DD HH: mm: ss';
it('startOfDay before dayStart', () => {
let start = shared.startOfDay({
let start = startOfDay({
now: moment('2014-10-09 02: 30: 00'),
dayStart,
});
@@ -1193,7 +1200,7 @@ describe('Cron', () => {
expect(start.format(fstr)).to.eql('2014-10-08 04: 00: 00');
});
it('startOfDay after dayStart', () => {
let start = shared.startOfDay({
let start = startOfDay({
now: moment('2014-10-09 05: 30: 00'),
dayStart,
});
@@ -1202,7 +1209,7 @@ describe('Cron', () => {
});
it('daysSince cron before, now after', () => {
let lastCron = moment('2014-10-09 02: 30: 00');
let days = shared.daysSince(lastCron, {
let days = daysSince(lastCron, {
now: moment('2014-10-09 11: 30: 00'),
dayStart,
});
@@ -1211,7 +1218,7 @@ describe('Cron', () => {
});
it('daysSince cron before, now before', () => {
let lastCron = moment('2014-10-09 02: 30: 00');
let days = shared.daysSince(lastCron, {
let days = daysSince(lastCron, {
now: moment('2014-10-09 03: 30: 00'),
dayStart,
});
@@ -1220,7 +1227,7 @@ describe('Cron', () => {
});
it('daysSince cron after, now after', () => {
let lastCron = moment('2014-10-09 05: 30: 00');
let days = shared.daysSince(lastCron, {
let days = daysSince(lastCron, {
now: moment('2014-10-09 06: 30: 00'),
dayStart,
});
@@ -1229,7 +1236,7 @@ describe('Cron', () => {
});
it('daysSince cron after, now tomorrow before', () => {
let lastCron = moment('2014-10-09 12: 30: 00');
let days = shared.daysSince(lastCron, {
let days = daysSince(lastCron, {
now: moment('2014-10-10 01: 30: 00'),
dayStart,
});
@@ -1238,7 +1245,7 @@ describe('Cron', () => {
});
it('daysSince cron after, now tomorrow after', () => {
let lastCron = moment('2014-10-09 12: 30: 00');
let days = shared.daysSince(lastCron, {
let days = daysSince(lastCron, {
now: moment('2014-10-10 10: 30: 00'),
dayStart,
});
@@ -1247,7 +1254,7 @@ describe('Cron', () => {
});
xit('daysSince, last cron before new dayStart', () => {
let lastCron = moment('2014-10-09 01: 00: 00');
let days = shared.daysSince(lastCron, {
let days = daysSince(lastCron, {
now: moment('2014-10-09 05: 00: 00'),
dayStart,
});
@@ -1266,7 +1273,7 @@ describe('Cron', () => {
function runCron (options) {
_.each([480, 240, 0, -120], function (timezoneOffset) {
let now = shared.startOfWeek({
let now = startOfWeek({
timezoneOffset,
}).add(options.currentHour || 0, 'hours');
@@ -1496,17 +1503,17 @@ describe('Helper', () => {
let today = '2013-01-01 00: 00: 00';
let zone = moment(today).zone();
expect(shared.startOfDay({
expect(startOfDay({
now: new Date(2013, 0, 1, 0),
}, {
timezoneOffset: zone,
}).format(fstr)).to.eql(today);
expect(shared.startOfDay({
expect(startOfDay({
now: new Date(2013, 0, 1, 5),
}, {
timezoneOffset: zone,
}).format(fstr)).to.eql(today);
expect(shared.startOfDay({
expect(startOfDay({
now: new Date(2013, 0, 1, 23, 59, 59),
timezoneOffset: zone,
}).format(fstr)).to.eql(today);

View File

@@ -1,4 +1,7 @@
/* eslint-disable camelcase */
import {
startOfWeek,
} from '../../common/script/cron';
let expect = require('expect.js'); // eslint-disable-line no-shadow
let moment = require('moment');
@@ -17,7 +20,7 @@ let repeatWithoutLastWeekday = () => { // eslint-disable-line no-unused-vars
s: true,
};
if (shared.startOfWeek(moment().zone(0)).isoWeekday() === 1) {
if (startOfWeek(moment().zone(0)).isoWeekday() === 1) {
repeat.su = false;
} else {
repeat.s = false;

View File

@@ -0,0 +1,66 @@
import {
maxHealth,
maxLevel,
capByLevel,
tnl,
diminishingReturns,
} from '../../common/script/index';
describe('helper functions used in stat calculations', () => {
describe('maxHealth', () => {
it('provides a maximum Health value', () => {
const HEALTH_CAP = 50;
expect(maxHealth).to.eql(HEALTH_CAP);
});
});
const LEVEL_CAP = 100;
const LEVEL = 57;
describe('maxLevel', () => {
it('returns a maximum level for attribute gain', () => {
expect(maxLevel).to.eql(LEVEL_CAP);
});
});
describe('capByLevel', () => {
it('returns level given if below cap', () => {
expect(capByLevel(LEVEL)).to.eql(LEVEL);
});
it('returns level given if equal to cap', () => {
expect(capByLevel(LEVEL_CAP)).to.eql(LEVEL_CAP);
});
it('returns level cap if above cap', () => {
expect(capByLevel(LEVEL_CAP + LEVEL)).to.eql(LEVEL_CAP);
});
});
describe('toNextLevel', () => {
it('increases Experience target from one level to the next', () => {
_.times(110, (level) => {
expect(tnl(level + 1)).to.be.greaterThan(tnl(level));
});
});
});
describe('diminishingReturns', () => {
const BONUS = 600;
const MAXIMUM = 200;
const HALFWAY = 75;
it('provides a value under the maximum, given a bonus and maximum', () => {
expect(diminishingReturns(BONUS, MAXIMUM)).to.be.lessThan(MAXIMUM);
});
it('provides a value under the maximum, given a bonus, maximum, and halfway point', () => {
expect(diminishingReturns(BONUS, MAXIMUM, HALFWAY)).to.be.lessThan(MAXIMUM);
});
it('provides a different curve if a halfway point is defined', () => {
expect(diminishingReturns(BONUS, MAXIMUM, HALFWAY)).to.not.eql(diminishingReturns(BONUS, MAXIMUM));
});
});
});

View File

@@ -101,6 +101,34 @@ describe('user.fns.buy', () => {
expect(user.items.gear.equipped).to.not.have.property('armor');
});
it('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', () => {
user.stats.gp = 100;
user.preferences.autoEquip = true;
user.ops.buy({params: {key: 'shield_warrior_1'}});
user.ops.equip({params: {key: 'shield_warrior_1'}});
user.ops.buy({params: {key: 'weapon_warrior_1'}});
user.ops.equip({params: {key: 'weapon_warrior_1'}});
user.ops.buy({params: {key: 'weapon_wizard_1'}});
expect(user.items.gear.equipped).to.have.property('shield', 'shield_base_0');
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_wizard_1');
});
it('buys two-handed equipment but does not automatically remove sword or shield', () => {
user.stats.gp = 100;
user.preferences.autoEquip = false;
user.ops.buy({params: {key: 'shield_warrior_1'}});
user.ops.equip({params: {key: 'shield_warrior_1'}});
user.ops.buy({params: {key: 'weapon_warrior_1'}});
user.ops.equip({params: {key: 'weapon_warrior_1'}});
user.ops.buy({params: {key: 'weapon_wizard_1'}});
expect(user.items.gear.equipped).to.have.property('shield', 'shield_warrior_1');
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_warrior_1');
});
it('does not buy equipment without enough Gold', () => {
user.stats.gp = 20;

View File

@@ -194,7 +194,7 @@ describe('Groups Controller', function() {
expect(group.leave).to.not.be.called;
expect(res.json).to.be.calledOnce;
expect(res.json).to.be.calledWith(403, 'You cannot leave party during an active quest. Please leave the quest first');
expect(res.json).to.be.calledWith(403, 'You cannot leave party during an active quest. Please leave the quest first.');
});
it('prevents quest leader from leaving a party if they have started a quest', function() {

View File

@@ -148,6 +148,19 @@ describe('Challenges Controller', function() {
expect(scope.filterChallenges(notOwnMem)).to.eql(false);
expect(scope.filterChallenges(notOwnNotMem)).to.eql(true);
});
it('it filters challenges to a single group when group id filter is set', inject(function($controller) {
scope.search = { };
scope.groups = {
0: specHelper.newGroup({_id: 'group-one'}),
1: specHelper.newGroup({_id: 'group-two'}),
2: specHelper.newGroup({_id: 'group-three'})
};
scope.groupIdFilter = 'group-one';
scope.filterInitialChallenges();
expect(scope.search.group).to.eql({'group-one': true});
}));
});
describe('selectAll', function() {

View File

@@ -79,6 +79,20 @@ describe('Inventory Controller', function() {
expect(rootScope.openModal).to.have.been.calledWith('hatchPet');
});
it('does not show modal if user tries to hatch a pet they own', function(){
scope.chooseEgg('Cactus');
scope.choosePotion('Base');
expect(user.items.eggs).to.eql({Cactus: 0});
expect(user.items.hatchingPotions).to.eql({Base: 0});
expect(user.items.pets).to.eql({'Cactus-Base': 5});
expect(scope.selectedEgg).to.eql(null);
expect(scope.selectedPotion).to.eql(null);
expect(rootScope.openModal).to.have.been.calledOnce;
scope.chooseEgg('Cactus');
scope.choosePotion('Base');
expect(rootScope.openModal).to.not.have.been.calledTwice;
});
it('does not show pet hatching modal if user has opted out', function(){
user.preferences.suppressModals.hatchPet = true;
scope.chooseEgg('Cactus');
@@ -88,6 +102,82 @@ describe('Inventory Controller', function() {
});
});
describe('Feeding and Raising Pets', function() {
beforeEach(function() {
sandbox.stub(rootScope, 'openModal');
user.items.pets = {'PandaCub-Base':5};
user.items.mounts = {'PandaCub-Base':false};
});
it('feeds a pet', function() {
scope.chooseFood('Meat');
scope.choosePet('PandaCub','Base');
expect(user.items.pets['PandaCub-Base']).to.eql(10);
});
it('gives weaker benefit when feeding inappropriate food', function() {
user.items.food.Honey = 1;
scope.chooseFood('Honey');
scope.choosePet('PandaCub','Base');
expect(user.items.pets['PandaCub-Base']).to.eql(7);
});
it('raises pet to a mount when feeding gauge maxes out', function() {
user.items.pets['PandaCub-Base'] = 45;
scope.chooseFood('Meat');
scope.choosePet('PandaCub','Base');
expect(user.items.pets['PandaCub-Base']).to.eql(-1);
expect(user.items.mounts['PandaCub-Base']).to.exist;
});
it('raises pet to a mount instantly when using a Saddle', function() {
user.items.food.Saddle = 1;
scope.chooseFood('Saddle');
scope.choosePet('PandaCub','Base');
expect(user.items.pets['PandaCub-Base']).to.eql(-1);
expect(user.items.mounts['PandaCub-Base']).to.exist;
});
it('displays mount raising modal for drop pets', function() {
user.items.food.Saddle = 1;
scope.chooseFood('Saddle');
scope.choosePet('PandaCub','Base');
expect(rootScope.openModal).to.have.been.calledOnce;
expect(rootScope.openModal).to.have.been.calledWith('raisePet');
});
it('displays mount raising modal for quest pets', function() {
user.items.food.Saddle = 1;
user.items.pets['Snake-Base'] = 1;
scope.chooseFood('Saddle');
scope.choosePet('Snake','Base');
expect(rootScope.openModal).to.have.been.calledOnce;
expect(rootScope.openModal).to.have.been.calledWith('raisePet');
});
it('displays mount raising modal for premium pets', function() {
user.items.food.Saddle = 1;
user.items.pets['TigerCub-Spooky'] = 1;
scope.chooseFood('Saddle');
scope.choosePet('TigerCub','Spooky');
expect(rootScope.openModal).to.have.been.calledOnce;
expect(rootScope.openModal).to.have.been.calledWith('raisePet');
});
});
it('sells an egg', function(){
scope.chooseEgg('Cactus');
scope.sellInventory();

View File

@@ -148,6 +148,7 @@ window.habitrpg = angular.module('habitrpg',
// Options > Social > Challenges
.state('options.social.challenges', {
url: "/challenges",
params: { groupIdFilter: null },
controller: 'ChallengesCtrl',
templateUrl: "partials/options.social.challenges.html"
})
@@ -156,7 +157,6 @@ window.habitrpg = angular.module('habitrpg',
templateUrl: 'partials/options.social.challenges.detail.html',
controller: ['$scope', 'Challenges', '$stateParams',
function($scope, Challenges, $stateParams){
$scope.obj = $scope.challenge = Challenges.Challenge.get({cid:$stateParams.cid}, function(){
$scope.challenge._locked = true;
});

View File

@@ -5,6 +5,8 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
// challenge
$scope.cid = $state.params.cid;
$scope.groupIdFilter = $stateParams.groupIdFilter;
_getChallenges();
// FIXME $scope.challenges needs to be resolved first (see app.js)
@@ -325,6 +327,21 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
});
};
$scope.filterInitialChallenges = function() {
$scope.groupsFilter = _.uniq(_.pluck($scope.challenges, 'group'), function(g){return g._id});
$scope.search = {
group: _.transform($scope.groups, function(m,g){m[g._id]=true;}),
_isMember: "either",
_isOwner: "either"
};
//If we game from a group, then override the filter to that group
if ($scope.groupIdFilter) {
$scope.search.group = {};
$scope.search.group[$scope.groupIdFilter] = true ;
}
}
function _calculateMaxPrize(gid) {
var userBalance = User.getBalanceInGems() || 0;
@@ -375,12 +392,7 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
} else {
Challenges.Challenge.query(function(challenges){
$scope.challenges = challenges;
$scope.groupsFilter = _.uniq(_.pluck(challenges, 'group'), function(g){return g._id});
$scope.search = {
group: _.transform($scope.groups, function(m,g){m[g._id]=true;}),
_isMember: "either",
_isOwner: "either"
};
$scope.filterInitialChallenges();
});
}
};

View File

@@ -114,8 +114,12 @@ habitrpg.controller("InventoryCtrl",
var eggName = Content.eggs[egg.key].text();
var potName = Content.hatchingPotions[potion.key].text();
if (!$window.confirm(window.env.t('hatchAPot', {potion: potName, egg: eggName}))) return;
var userHasPet = user.items.pets[egg.key + '-' + potion.key];
user.ops.hatch({params:{egg:egg.key, hatchingPotion:potion.key}});
if (!user.preferences.suppressModals.hatchPet) {
if (!user.preferences.suppressModals.hatchPet && !userHasPet) {
$scope.hatchedPet = {
egg: eggName,
potion: potName,
@@ -159,7 +163,7 @@ habitrpg.controller("InventoryCtrl",
// Feeding Pet
if ($scope.selectedFood) {
var food = $scope.selectedFood;
var startingMounts = Stats.totalCount(user.items.mounts);
var startingMounts = $rootScope.countExists(user.items.mounts);
if (food.key === 'Saddle') {
if (!$window.confirm(window.env.t('useSaddle', {pet: petDisplayName}))) return;
} else if (!$window.confirm(window.env.t('feedPet', {name: petDisplayName, article: food.article, text: food.text()}))) {
@@ -169,7 +173,7 @@ habitrpg.controller("InventoryCtrl",
$scope.selectedFood = null;
_updateDropAnimalCount(user.items);
if (Stats.totalCount(user.items.mounts) > startingMounts && !user.preferences.suppressModals.raisePet) {
if ($rootScope.countExists(user.items.mounts) > startingMounts && !user.preferences.suppressModals.raisePet) {
$scope.raisedPet = {
displayName: petDisplayName,
spriteName: pet,

View File

@@ -0,0 +1,15 @@
angular.module('habitrpg')
.filter('timezoneOffsetToUtc', function () {
return function (offset) {
var sign = offset > 0 ? '-' : '+';
offset = Math.abs(offset) / 60;
var hour = Math.floor(offset);
var minutes_int = (offset - hour) * 60;
var minutes = minutes_int < 10 ? '0'+minutes_int : minutes_int;
return 'UTC' + sign + hour + ':' + minutes;
}
});

View File

@@ -58,6 +58,7 @@
"js/filters/money.js",
"js/filters/roundLargeNumbers.js",
"js/filters/taskOrdering.js",
"js/filters/timezoneOffsetToUtc.js",
"js/directives/close-menu.directive.js",
"js/directives/expand-menu.directive.js",

View File

@@ -524,7 +524,7 @@ api.leave = function(req, res, next) {
}
if (group.quest && group.quest.active && group.quest.members && group.quest.members[user._id]) {
return res.json(403, 'You cannot leave party during an active quest. Please leave the quest first');
return res.json(403, 'You cannot leave party during an active quest. Please leave the quest first.');
}
}

View File

@@ -164,12 +164,22 @@ export let schema = new Schema({
inviteParty: {type: Boolean, default: false},
},
ios: {
<<<<<<< HEAD
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},
},
=======
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},
inviteParty: {type: Boolean, 'default': false},
}
>>>>>>> develop
},
dropsEnabled: {type: Boolean, default: false},
itemsEnabled: {type: Boolean, default: false},

View File

@@ -116,6 +116,16 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
ng-disabled='dayStart == user.preferences.dayStart')
=env.t('saveCustomDayStart')
hr
h5=env.t('timezone')
.form-horizontal
.form-group
.col-sm-12
p!=env.t('timezoneUTC', {utc: "{{ user.preferences.timezoneOffset | timezoneOffsetToUtc }}"})
br
p!=env.t('timezoneInfo')
.personal-options.col-md-6
.panel.panel-default
.panel-heading

View File

@@ -10,7 +10,8 @@
table.table.table-striped
tr(ng-repeat='challenge in group.challenges')
td
a(ui-sref='options.social.challenges.detail({cid:challenge._id})') {{challenge.name}}
a(ui-sref='options.social.challenges.detail({cid:challenge._id, groupIdFilter: group._id})')
markdown(text='challenge.name')
div(ng-if='group.challenges.length == 0')
p
|&nbsp;

View File

@@ -140,7 +140,12 @@ script(type='text/ng-template', id='partials/options.social.challenges.html')
ng-disabled='insufficientGemsForTavernChallenge()')
.form-group
input.form-control(type='text', minlength="3", maxlength="16",
textarea.form-control(cols='3', placeholder=env.t('challengeDescr'), ng-model='newChallenge.description'
ng-disabled='insufficientGemsForTavernChallenge()')
.row
.form-group.col-md-6.col-sm-12
input.form-control(type='text', minlength="3",
ng-model='newChallenge.shortName', placeholder=env.t('challengeTag'), required
ng-disabled='insufficientGemsForTavernChallenge()')
|&nbsp;
@@ -148,11 +153,8 @@ script(type='text/ng-template', id='partials/options.social.challenges.html')
popover=env.t('challengeTagPop'), popover-trigger='mouseenter', popover-placement='right')
=env.t('moreInfo')
.form-group
textarea.form-control(cols='3', placeholder=env.t('challengeDescr'), ng-model='newChallenge.description'
ng-disabled='insufficientGemsForTavernChallenge()')
.form-group
.row
.form-group.col-md-6.col-sm-12
.input-group
span.input-group-addon
.Pet_Currency_Gem1x
@@ -163,7 +165,7 @@ script(type='text/ng-template', id='partials/options.social.challenges.html')
a.hint(popover="{{newChallenge.group=='habitrpg' ? env.t('prizePopTavern') : env.t('prizePop')}}",
popover-trigger='mouseenter', popover-placement='right')
=env.t('moreInfo')
.pull-right(ng-show='newChallenge.group=="habitrpg"')
div(ng-show='newChallenge.group=="habitrpg"')
!=env.t('publicChallenges')
.form-group(ng-if='user.contributor.admin')
@@ -203,7 +205,8 @@ script(type='text/ng-template', id='partials/options.social.challenges.html')
a.btn.btn-sm.btn-success(ng-hide='challenge._isMember', ng-click='join(challenge)')
span.glyphicon.glyphicon-ok
=env.t('join')
a.accordion-toggle(id="{{challenge._id}}" ng-click='toggle(challenge._id)') {{challenge.name}}
a.accordion-toggle(id="{{challenge._id}}" ng-click='toggle(challenge._id)')
markdown(text='challenge.name')
.panel-body(ng-class='{collapse: !$stateParams.cid == challenge._id}')
.accordion-inner(ng-if='$stateParams.cid == challenge._id')
div(ui-view)

View File

@@ -1,5 +1,30 @@
h2 11/19/2015 - SMALL iOS UPDATE AND HABITICA HIRING NEWS!
h2 11/25/2015 - HABITICA THANKSGIVING! NOVEMBER SUBSCRIBER ITEM AND TURKEY PETS AND MOUNTS
hr
tr
td
.npc_daniel.pull-right
h3 Happy Thanksgiving!
p It's Thanksgiving in Habitica! On this day Habiticans celebrate by spending time with loved ones, giving thanks, and riding their glorious turkeys into the magnificent sunset. Some of the NPCs are celebrating the occasion!
tr
td
.Pet-Turkey-Gilded.pull-right
h3 Turkey Pet and Mount!
p Those of you who weren't around last Thanksgiving have received an adorable Turkey Pet, and those of you who got a Turkey Pet last year have received a handsome Turkey Mount! Already got a Turkey Mount? You, my friend, have been gifted the rare and glittering Gilded Turkey Pet!
br
p Thank you for using Habitica - we really love you guys <3
tr
td
.promo_mystery_201511.pull-right
h3 November Subscriber Items Revealed
p The November Subscriber Item Set has been revealed: the Wood Warrior Set! All November subscribers will receive the Log Crown and the Wooden Armor. You still have five 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
if menuItem !== 'oldNews'
hr
a(href='/static/old-news', target='_blank') Read older news
mixin oldNews
h2 11/19/2015 - SMALL iOS UPDATE AND HABITICA HIRING NEWS!
tr
td
h3 Habitica Hiring News
@@ -15,12 +40,6 @@ h2 11/19/2015 - SMALL iOS UPDATE AND HABITICA HIRING NEWS!
br
p If you already reviewed the last version of the app, Apple has hidden it for this version, but you can automatically post the same review again by tapping “Write a review” and then just hitting "Send." Thank you very much for taking the time to share your thoughts with us! Posting and reposting reviews really helps us out.
p.small.muted by Viirus and Lemoness
if menuItem !== 'oldNews'
hr
a(href='/static/old-news', target='_blank') Read older news
mixin oldNews
h2 11/16/2015 - HABITICA STICKERS AND COSTUME CONTEST BADGES!
tr
td

View File

@@ -589,8 +589,7 @@ html(ng-app='habitrpg', ng-controller='RootCtrl')
#footercall
.container-fluid
.row
.col-md-10
h3= env.t('joinOthers')
h3= env.t('joinOthers', {userCount:'900,000'})
.row
.col-md-4.col-md-offset-4
button.btn.btn-primary.btn-lg.btn-block(ng-click='playButtonClick()')= env.t('free')