Merge branch 'develop' into api-v3
@@ -1,6 +1,6 @@
|
|||||||
.2014_Fall_HealerPROMO2 {
|
.2014_Fall_HealerPROMO2 {
|
||||||
background-image: url(spritesmith-largeSprites-0.png);
|
background-image: url(spritesmith-largeSprites-0.png);
|
||||||
background-position: -731px -995px;
|
background-position: -822px -995px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
}
|
}
|
||||||
.2014_Fall_Warrior_PROMO {
|
.2014_Fall_Warrior_PROMO {
|
||||||
background-image: url(spritesmith-largeSprites-0.png);
|
background-image: url(spritesmith-largeSprites-0.png);
|
||||||
background-position: -1095px -995px;
|
background-position: -276px -995px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
}
|
}
|
||||||
.promo_dilatoryDistress {
|
.promo_dilatoryDistress {
|
||||||
background-image: url(spritesmith-largeSprites-0.png);
|
background-image: url(spritesmith-largeSprites-0.png);
|
||||||
background-position: -367px -995px;
|
background-position: -458px -995px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
}
|
}
|
||||||
.promo_enchanted_armoire_201509 {
|
.promo_enchanted_armoire_201509 {
|
||||||
background-image: url(spritesmith-largeSprites-0.png);
|
background-image: url(spritesmith-largeSprites-0.png);
|
||||||
background-position: -458px -995px;
|
background-position: -549px -995px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
}
|
}
|
||||||
.promo_mystery_201405 {
|
.promo_mystery_201405 {
|
||||||
background-image: url(spritesmith-largeSprites-0.png);
|
background-image: url(spritesmith-largeSprites-0.png);
|
||||||
background-position: -1004px -995px;
|
background-position: -1095px -995px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
}
|
}
|
||||||
.promo_mystery_201409 {
|
.promo_mystery_201409 {
|
||||||
background-image: url(spritesmith-largeSprites-0.png);
|
background-image: url(spritesmith-largeSprites-0.png);
|
||||||
background-position: -640px -995px;
|
background-position: -731px -995px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
}
|
}
|
||||||
.promo_mystery_201411 {
|
.promo_mystery_201411 {
|
||||||
background-image: url(spritesmith-largeSprites-0.png);
|
background-image: url(spritesmith-largeSprites-0.png);
|
||||||
background-position: -822px -995px;
|
background-position: -913px -995px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
@@ -168,13 +168,13 @@
|
|||||||
}
|
}
|
||||||
.promo_mystery_201502 {
|
.promo_mystery_201502 {
|
||||||
background-image: url(spritesmith-largeSprites-0.png);
|
background-image: url(spritesmith-largeSprites-0.png);
|
||||||
background-position: -276px -995px;
|
background-position: -367px -995px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201503 {
|
.promo_mystery_201503 {
|
||||||
background-image: url(spritesmith-largeSprites-0.png);
|
background-image: url(spritesmith-largeSprites-0.png);
|
||||||
background-position: 0px -1101px;
|
background-position: -91px -1101px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
}
|
}
|
||||||
.promo_mystery_201505 {
|
.promo_mystery_201505 {
|
||||||
background-image: url(spritesmith-largeSprites-0.png);
|
background-image: url(spritesmith-largeSprites-0.png);
|
||||||
background-position: -549px -995px;
|
background-position: -640px -995px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
}
|
}
|
||||||
.promo_mystery_201509 {
|
.promo_mystery_201509 {
|
||||||
background-image: url(spritesmith-largeSprites-0.png);
|
background-image: url(spritesmith-largeSprites-0.png);
|
||||||
background-position: -913px -995px;
|
background-position: -1004px -995px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
@@ -220,6 +220,12 @@
|
|||||||
width: 93px;
|
width: 93px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
|
.promo_mystery_201511 {
|
||||||
|
background-image: url(spritesmith-largeSprites-0.png);
|
||||||
|
background-position: 0px -1101px;
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
}
|
||||||
.promo_mystery_3014 {
|
.promo_mystery_3014 {
|
||||||
background-image: url(spritesmith-largeSprites-0.png);
|
background-image: url(spritesmith-largeSprites-0.png);
|
||||||
background-position: -943px -764px;
|
background-position: -943px -764px;
|
||||||
|
|||||||
BIN
common/dist/sprites/spritesmith-largeSprites-0.png
vendored
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 194 KiB |
1742
common/dist/sprites/spritesmith-main-4.css
vendored
BIN
common/dist/sprites/spritesmith-main-4.png
vendored
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
794
common/dist/sprites/spritesmith-main-5.css
vendored
BIN
common/dist/sprites/spritesmith-main-5.png
vendored
|
Before Width: | Height: | Size: 396 KiB After Width: | Height: | Size: 387 KiB |
544
common/dist/sprites/spritesmith-main-6.css
vendored
BIN
common/dist/sprites/spritesmith-main-6.png
vendored
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 153 KiB |
472
common/dist/sprites/spritesmith-main-7.css
vendored
BIN
common/dist/sprites/spritesmith-main-7.png
vendored
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 143 KiB |
558
common/dist/sprites/spritesmith-main-8.css
vendored
BIN
common/dist/sprites/spritesmith-main-8.png
vendored
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 142 KiB |
2068
common/dist/sprites/spritesmith-main-9.css
vendored
BIN
common/dist/sprites/spritesmith-main-9.png
vendored
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
BIN
common/img/sprites/spritesmith/stable/pets/Pet-Turkey-Gilded.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
@@ -342,6 +342,8 @@
|
|||||||
"armorMystery201508Notes": "Run fast as a flash in the fluffy Cheetah Costume! Confers no benefit. August 2015 Subscriber Item.",
|
"armorMystery201508Notes": "Run fast as a flash in the fluffy Cheetah Costume! Confers no benefit. August 2015 Subscriber Item.",
|
||||||
"armorMystery201509Text": "Werewolf Costume",
|
"armorMystery201509Text": "Werewolf Costume",
|
||||||
"armorMystery201509Notes": "This IS a costume, right? Confers no benefit. September 2015 Subscriber Item.",
|
"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",
|
"armorMystery301404Text": "Steampunk Suit",
|
||||||
"armorMystery301404Notes": "Dapper and dashing, wot! Confers no benefit. February 3015 Subscriber Item.",
|
"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.",
|
"headMystery201508Notes": "This cozy cheetah hat is very fuzzy! Confers no benefit. August 2015 Subscriber Item.",
|
||||||
"headMystery201509Text": "Werewolf Mask",
|
"headMystery201509Text": "Werewolf Mask",
|
||||||
"headMystery201509Notes": "This IS a mask, right? Confers no benefit. September 2015 Subscriber Item.",
|
"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",
|
"headMystery301404Text": "Fancy Top Hat",
|
||||||
"headMystery301404Notes": "A fancy top hat for the finest of gentlefolk! January 3015 Subscriber Item. Confers no benefit.",
|
"headMystery301404Notes": "A fancy top hat for the finest of gentlefolk! January 3015 Subscriber Item. Confers no benefit.",
|
||||||
"headMystery301405Text": "Basic Top Hat",
|
"headMystery301405Text": "Basic Top Hat",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"valentineCardAchievementText": "Aww, you and your friend must really care about each other! Sent or received <%= cards %> Valentine's Day cards.",
|
"valentineCardAchievementText": "Aww, you and your friend must really care about each other! Sent or received <%= cards %> Valentine's Day cards.",
|
||||||
"polarBear": "Polar Bear",
|
"polarBear": "Polar Bear",
|
||||||
"turkey": "Turkey",
|
"turkey": "Turkey",
|
||||||
|
"gildedTurkey": "Gilded Turkey",
|
||||||
"polarBearPup": "Polar Bear Cub",
|
"polarBearPup": "Polar Bear Cub",
|
||||||
"jackolantern": "Jack-O-Lantern",
|
"jackolantern": "Jack-O-Lantern",
|
||||||
"seasonalShop": "Seasonal Shop",
|
"seasonalShop": "Seasonal Shop",
|
||||||
|
|||||||
@@ -144,5 +144,8 @@
|
|||||||
"gemCapExtra": "Gem Cap Extra:",
|
"gemCapExtra": "Gem Cap Extra:",
|
||||||
"mysticHourglasses": "Mystic Hourglasses:",
|
"mysticHourglasses": "Mystic Hourglasses:",
|
||||||
"paypal": "PayPal",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,12 @@ let armor = {
|
|||||||
mystery: '201509',
|
mystery: '201509',
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
|
201511: {
|
||||||
|
text: t('armorMystery201511Text'),
|
||||||
|
notes: t('armorMystery201511Notes'),
|
||||||
|
mystery: '201511',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
301404: {
|
301404: {
|
||||||
text: t('armorMystery301404Text'),
|
text: t('armorMystery301404Text'),
|
||||||
notes: t('armorMystery301404Notes'),
|
notes: t('armorMystery301404Notes'),
|
||||||
@@ -238,6 +244,12 @@ let head = {
|
|||||||
mystery: '201509',
|
mystery: '201509',
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
|
201511: {
|
||||||
|
text: t('headMystery201511Text'),
|
||||||
|
notes: t('headMystery201511Notes'),
|
||||||
|
mystery: '201511',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
301404: {
|
301404: {
|
||||||
text: t('headMystery301404Text'),
|
text: t('headMystery301404Text'),
|
||||||
notes: t('headMystery301404Notes'),
|
notes: t('headMystery301404Notes'),
|
||||||
|
|||||||
@@ -952,7 +952,8 @@ api.specialPets = {
|
|||||||
'JackOLantern-Base': 'jackolantern',
|
'JackOLantern-Base': 'jackolantern',
|
||||||
'Mammoth-Base': 'mammoth',
|
'Mammoth-Base': 'mammoth',
|
||||||
'Tiger-Veteran': 'veteranTiger',
|
'Tiger-Veteran': 'veteranTiger',
|
||||||
'Phoenix-Base': 'phoenix'
|
'Phoenix-Base': 'phoenix',
|
||||||
|
'Turkey-Gilded': 'gildedTurkey',
|
||||||
};
|
};
|
||||||
|
|
||||||
api.specialMounts = {
|
api.specialMounts = {
|
||||||
|
|||||||
@@ -106,6 +106,11 @@ let mysterySets = {
|
|||||||
end: '2015-11-02',
|
end: '2015-11-02',
|
||||||
text: 'Horned Goblin Set',
|
text: 'Horned Goblin Set',
|
||||||
},
|
},
|
||||||
|
201511: {
|
||||||
|
start: '2015-11-25',
|
||||||
|
end: '2015-12-02',
|
||||||
|
text: 'Wood Warrior Set',
|
||||||
|
},
|
||||||
301404: {
|
301404: {
|
||||||
start: '3014-03-24',
|
start: '3014-03-24',
|
||||||
end: '3014-04-02',
|
end: '3014-04-02',
|
||||||
|
|||||||
110
common/script/cron.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; };
|
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');
|
moment = require('moment');
|
||||||
@@ -12,6 +18,13 @@ i18n = require('./i18n');
|
|||||||
api = module.exports = {};
|
api = module.exports = {};
|
||||||
|
|
||||||
api.i18n = i18n;
|
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) {
|
$w = api.$w = function(s) {
|
||||||
return s.split(' ');
|
return s.split(' ');
|
||||||
@@ -61,184 +74,6 @@ api.planGemLimits = {
|
|||||||
convCap: 25
|
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
|
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
|
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
|
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";
|
classes += " beingEdited";
|
||||||
}
|
}
|
||||||
if (type === 'todo' || type === 'daily') {
|
if (type === 'todo' || type === 'daily') {
|
||||||
if (completed || (type === 'daily' && !api.shouldDo(+(new Date), task, {
|
if (completed || (type === 'daily' && !shouldDo(+(new Date), task, {
|
||||||
dayStart: dayStart
|
dayStart: dayStart
|
||||||
}))) {
|
}))) {
|
||||||
classes += " completed";
|
classes += " completed";
|
||||||
@@ -861,6 +695,7 @@ api.wrap = function(user, main) {
|
|||||||
_.each(['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp'], function(value) {
|
_.each(['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp'], function(value) {
|
||||||
return stats[value] = 0;
|
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;
|
gear = user.items.gear;
|
||||||
_.each(['equipped', 'costume'], function(type) {
|
_.each(['equipped', 'costume'], function(type) {
|
||||||
gear[type] = {};
|
gear[type] = {};
|
||||||
@@ -1558,9 +1393,9 @@ api.wrap = function(user, main) {
|
|||||||
} else {
|
} else {
|
||||||
if (user.preferences.autoEquip) {
|
if (user.preferences.autoEquip) {
|
||||||
user.items.gear.equipped[item.type] = item.key;
|
user.items.gear.equipped[item.type] = item.key;
|
||||||
|
message = user.fns.handleTwoHanded(item, null, req);
|
||||||
}
|
}
|
||||||
user.items.gear.owned[item.key] = true;
|
user.items.gear.owned[item.key] = true;
|
||||||
message = user.fns.handleTwoHanded(item, null, req);
|
|
||||||
if (message == null) {
|
if (message == null) {
|
||||||
message = i18n.t('messageBought', {
|
message = i18n.t('messageBought', {
|
||||||
itemText: item.text(req.language)
|
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;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (((ref3 = user.flags) != null ? ref3.dropsEnabled : void 0) && user.fns.predictableRandom(user.stats.exp) < chance) {
|
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 = {};
|
options = {};
|
||||||
}
|
}
|
||||||
now = +options.now || +(new Date);
|
now = +options.now || +(new Date);
|
||||||
daysMissed = api.daysSince(user.lastCron, _.defaults({
|
daysMissed = daysSince(user.lastCron, _.defaults({
|
||||||
now: now
|
now: now
|
||||||
}, user.preferences));
|
}, user.preferences));
|
||||||
if (!(daysMissed > 0)) {
|
if (!(daysMissed > 0)) {
|
||||||
@@ -2599,7 +2434,7 @@ api.wrap = function(user, main) {
|
|||||||
thatDay = moment(now).subtract({
|
thatDay = moment(now).subtract({
|
||||||
days: 1
|
days: 1
|
||||||
});
|
});
|
||||||
if (api.shouldDo(thatDay.toDate(), daily, user.preferences) || completed) {
|
if (shouldDo(thatDay.toDate(), daily, user.preferences) || completed) {
|
||||||
_.each(daily.checklist, (function(box) {
|
_.each(daily.checklist, (function(box) {
|
||||||
box.completed = false;
|
box.completed = false;
|
||||||
return true;
|
return true;
|
||||||
@@ -2653,7 +2488,7 @@ api.wrap = function(user, main) {
|
|||||||
thatDay = moment(now).subtract({
|
thatDay = moment(now).subtract({
|
||||||
days: n + 1
|
days: n + 1
|
||||||
});
|
});
|
||||||
if (api.shouldDo(thatDay.toDate(), task, user.preferences)) {
|
if (shouldDo(thatDay.toDate(), task, user.preferences)) {
|
||||||
scheduleMisses++;
|
scheduleMisses++;
|
||||||
if (user.stats.buffs.stealth) {
|
if (user.stats.buffs.stealth) {
|
||||||
user.stats.buffs.stealth--;
|
user.stats.buffs.stealth--;
|
||||||
|
|||||||
44
common/script/statHelpers.js
Normal 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));
|
||||||
|
}
|
||||||
71
migrations/20151125_turkey_ladder.js
Normal 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);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ var _id = '';
|
|||||||
var update = {
|
var update = {
|
||||||
$addToSet: {
|
$addToSet: {
|
||||||
'purchased.plan.mysteryItems':{
|
'purchased.plan.mysteryItems':{
|
||||||
$each:['headAccessory_mystery_201510','back_mystery_201510']
|
$each:['head_mystery_201511','armor_mystery_201511']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
/* eslint-disable camelcase, func-names, no-shadow */
|
/* eslint-disable camelcase, func-names, no-shadow */
|
||||||
|
import {
|
||||||
|
DAY_MAPPING,
|
||||||
|
startOfWeek,
|
||||||
|
startOfDay,
|
||||||
|
daysSince,
|
||||||
|
} from '../../common/script/cron';
|
||||||
|
|
||||||
let expect = require('expect.js');
|
let expect = require('expect.js');
|
||||||
let sinon = require('sinon');
|
let sinon = require('sinon');
|
||||||
let moment = require('moment');
|
let moment = require('moment');
|
||||||
@@ -205,7 +212,7 @@ let repeatWithoutLastWeekday = () => {
|
|||||||
s: true,
|
s: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (shared.startOfWeek(moment().zone(0)).isoWeekday() === 1) {
|
if (startOfWeek(moment().zone(0)).isoWeekday() === 1) {
|
||||||
repeat.su = false;
|
repeat.su = false;
|
||||||
} else {
|
} else {
|
||||||
repeat.s = false;
|
repeat.s = false;
|
||||||
@@ -301,7 +308,7 @@ describe('User', () => {
|
|||||||
|
|
||||||
let yesterday = moment().subtract(1, 'days');
|
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) => {
|
_.each(user.dailys.slice(1), (d) => {
|
||||||
d.completed = true;
|
d.completed = true;
|
||||||
});
|
});
|
||||||
@@ -383,7 +390,7 @@ describe('User', () => {
|
|||||||
it('does not reset checklist on grey incomplete dailies', () => {
|
it('does not reset checklist on grey incomplete dailies', () => {
|
||||||
let yesterday = moment().subtract(1, 'days');
|
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 = [
|
user.dailys[0].checklist = [
|
||||||
{
|
{
|
||||||
text: '1',
|
text: '1',
|
||||||
@@ -407,7 +414,7 @@ describe('User', () => {
|
|||||||
it('resets checklist on complete grey complete dailies', () => {
|
it('resets checklist on complete grey complete dailies', () => {
|
||||||
let yesterday = moment().subtract(1, 'days');
|
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 = [
|
user.dailys[0].checklist = [
|
||||||
{
|
{
|
||||||
text: '1',
|
text: '1',
|
||||||
@@ -1185,7 +1192,7 @@ describe('Cron', () => {
|
|||||||
let fstr = 'YYYY-MM-DD HH: mm: ss';
|
let fstr = 'YYYY-MM-DD HH: mm: ss';
|
||||||
|
|
||||||
it('startOfDay before dayStart', () => {
|
it('startOfDay before dayStart', () => {
|
||||||
let start = shared.startOfDay({
|
let start = startOfDay({
|
||||||
now: moment('2014-10-09 02: 30: 00'),
|
now: moment('2014-10-09 02: 30: 00'),
|
||||||
dayStart,
|
dayStart,
|
||||||
});
|
});
|
||||||
@@ -1193,7 +1200,7 @@ describe('Cron', () => {
|
|||||||
expect(start.format(fstr)).to.eql('2014-10-08 04: 00: 00');
|
expect(start.format(fstr)).to.eql('2014-10-08 04: 00: 00');
|
||||||
});
|
});
|
||||||
it('startOfDay after dayStart', () => {
|
it('startOfDay after dayStart', () => {
|
||||||
let start = shared.startOfDay({
|
let start = startOfDay({
|
||||||
now: moment('2014-10-09 05: 30: 00'),
|
now: moment('2014-10-09 05: 30: 00'),
|
||||||
dayStart,
|
dayStart,
|
||||||
});
|
});
|
||||||
@@ -1202,7 +1209,7 @@ describe('Cron', () => {
|
|||||||
});
|
});
|
||||||
it('daysSince cron before, now after', () => {
|
it('daysSince cron before, now after', () => {
|
||||||
let lastCron = moment('2014-10-09 02: 30: 00');
|
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'),
|
now: moment('2014-10-09 11: 30: 00'),
|
||||||
dayStart,
|
dayStart,
|
||||||
});
|
});
|
||||||
@@ -1211,7 +1218,7 @@ describe('Cron', () => {
|
|||||||
});
|
});
|
||||||
it('daysSince cron before, now before', () => {
|
it('daysSince cron before, now before', () => {
|
||||||
let lastCron = moment('2014-10-09 02: 30: 00');
|
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'),
|
now: moment('2014-10-09 03: 30: 00'),
|
||||||
dayStart,
|
dayStart,
|
||||||
});
|
});
|
||||||
@@ -1220,7 +1227,7 @@ describe('Cron', () => {
|
|||||||
});
|
});
|
||||||
it('daysSince cron after, now after', () => {
|
it('daysSince cron after, now after', () => {
|
||||||
let lastCron = moment('2014-10-09 05: 30: 00');
|
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'),
|
now: moment('2014-10-09 06: 30: 00'),
|
||||||
dayStart,
|
dayStart,
|
||||||
});
|
});
|
||||||
@@ -1229,7 +1236,7 @@ describe('Cron', () => {
|
|||||||
});
|
});
|
||||||
it('daysSince cron after, now tomorrow before', () => {
|
it('daysSince cron after, now tomorrow before', () => {
|
||||||
let lastCron = moment('2014-10-09 12: 30: 00');
|
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'),
|
now: moment('2014-10-10 01: 30: 00'),
|
||||||
dayStart,
|
dayStart,
|
||||||
});
|
});
|
||||||
@@ -1238,7 +1245,7 @@ describe('Cron', () => {
|
|||||||
});
|
});
|
||||||
it('daysSince cron after, now tomorrow after', () => {
|
it('daysSince cron after, now tomorrow after', () => {
|
||||||
let lastCron = moment('2014-10-09 12: 30: 00');
|
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'),
|
now: moment('2014-10-10 10: 30: 00'),
|
||||||
dayStart,
|
dayStart,
|
||||||
});
|
});
|
||||||
@@ -1247,7 +1254,7 @@ describe('Cron', () => {
|
|||||||
});
|
});
|
||||||
xit('daysSince, last cron before new dayStart', () => {
|
xit('daysSince, last cron before new dayStart', () => {
|
||||||
let lastCron = moment('2014-10-09 01: 00: 00');
|
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'),
|
now: moment('2014-10-09 05: 00: 00'),
|
||||||
dayStart,
|
dayStart,
|
||||||
});
|
});
|
||||||
@@ -1266,7 +1273,7 @@ describe('Cron', () => {
|
|||||||
|
|
||||||
function runCron (options) {
|
function runCron (options) {
|
||||||
_.each([480, 240, 0, -120], function (timezoneOffset) {
|
_.each([480, 240, 0, -120], function (timezoneOffset) {
|
||||||
let now = shared.startOfWeek({
|
let now = startOfWeek({
|
||||||
timezoneOffset,
|
timezoneOffset,
|
||||||
}).add(options.currentHour || 0, 'hours');
|
}).add(options.currentHour || 0, 'hours');
|
||||||
|
|
||||||
@@ -1496,17 +1503,17 @@ describe('Helper', () => {
|
|||||||
let today = '2013-01-01 00: 00: 00';
|
let today = '2013-01-01 00: 00: 00';
|
||||||
let zone = moment(today).zone();
|
let zone = moment(today).zone();
|
||||||
|
|
||||||
expect(shared.startOfDay({
|
expect(startOfDay({
|
||||||
now: new Date(2013, 0, 1, 0),
|
now: new Date(2013, 0, 1, 0),
|
||||||
}, {
|
}, {
|
||||||
timezoneOffset: zone,
|
timezoneOffset: zone,
|
||||||
}).format(fstr)).to.eql(today);
|
}).format(fstr)).to.eql(today);
|
||||||
expect(shared.startOfDay({
|
expect(startOfDay({
|
||||||
now: new Date(2013, 0, 1, 5),
|
now: new Date(2013, 0, 1, 5),
|
||||||
}, {
|
}, {
|
||||||
timezoneOffset: zone,
|
timezoneOffset: zone,
|
||||||
}).format(fstr)).to.eql(today);
|
}).format(fstr)).to.eql(today);
|
||||||
expect(shared.startOfDay({
|
expect(startOfDay({
|
||||||
now: new Date(2013, 0, 1, 23, 59, 59),
|
now: new Date(2013, 0, 1, 23, 59, 59),
|
||||||
timezoneOffset: zone,
|
timezoneOffset: zone,
|
||||||
}).format(fstr)).to.eql(today);
|
}).format(fstr)).to.eql(today);
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
import {
|
||||||
|
startOfWeek,
|
||||||
|
} from '../../common/script/cron';
|
||||||
|
|
||||||
let expect = require('expect.js'); // eslint-disable-line no-shadow
|
let expect = require('expect.js'); // eslint-disable-line no-shadow
|
||||||
let moment = require('moment');
|
let moment = require('moment');
|
||||||
@@ -17,7 +20,7 @@ let repeatWithoutLastWeekday = () => { // eslint-disable-line no-unused-vars
|
|||||||
s: true,
|
s: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (shared.startOfWeek(moment().zone(0)).isoWeekday() === 1) {
|
if (startOfWeek(moment().zone(0)).isoWeekday() === 1) {
|
||||||
repeat.su = false;
|
repeat.su = false;
|
||||||
} else {
|
} else {
|
||||||
repeat.s = false;
|
repeat.s = false;
|
||||||
|
|||||||
66
test/common/statHelpers.test.js
Normal 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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -101,6 +101,34 @@ describe('user.fns.buy', () => {
|
|||||||
expect(user.items.gear.equipped).to.not.have.property('armor');
|
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', () => {
|
it('does not buy equipment without enough Gold', () => {
|
||||||
user.stats.gp = 20;
|
user.stats.gp = 20;
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ describe('Groups Controller', function() {
|
|||||||
|
|
||||||
expect(group.leave).to.not.be.called;
|
expect(group.leave).to.not.be.called;
|
||||||
expect(res.json).to.be.calledOnce;
|
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() {
|
it('prevents quest leader from leaving a party if they have started a quest', function() {
|
||||||
|
|||||||
@@ -148,6 +148,19 @@ describe('Challenges Controller', function() {
|
|||||||
expect(scope.filterChallenges(notOwnMem)).to.eql(false);
|
expect(scope.filterChallenges(notOwnMem)).to.eql(false);
|
||||||
expect(scope.filterChallenges(notOwnNotMem)).to.eql(true);
|
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() {
|
describe('selectAll', function() {
|
||||||
|
|||||||
@@ -78,7 +78,21 @@ describe('Inventory Controller', function() {
|
|||||||
expect(rootScope.openModal).to.have.been.calledOnce;
|
expect(rootScope.openModal).to.have.been.calledOnce;
|
||||||
expect(rootScope.openModal).to.have.been.calledWith('hatchPet');
|
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(){
|
it('does not show pet hatching modal if user has opted out', function(){
|
||||||
user.preferences.suppressModals.hatchPet = true;
|
user.preferences.suppressModals.hatchPet = true;
|
||||||
scope.chooseEgg('Cactus');
|
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(){
|
it('sells an egg', function(){
|
||||||
scope.chooseEgg('Cactus');
|
scope.chooseEgg('Cactus');
|
||||||
scope.sellInventory();
|
scope.sellInventory();
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ window.habitrpg = angular.module('habitrpg',
|
|||||||
// Options > Social > Challenges
|
// Options > Social > Challenges
|
||||||
.state('options.social.challenges', {
|
.state('options.social.challenges', {
|
||||||
url: "/challenges",
|
url: "/challenges",
|
||||||
|
params: { groupIdFilter: null },
|
||||||
controller: 'ChallengesCtrl',
|
controller: 'ChallengesCtrl',
|
||||||
templateUrl: "partials/options.social.challenges.html"
|
templateUrl: "partials/options.social.challenges.html"
|
||||||
})
|
})
|
||||||
@@ -156,7 +157,6 @@ window.habitrpg = angular.module('habitrpg',
|
|||||||
templateUrl: 'partials/options.social.challenges.detail.html',
|
templateUrl: 'partials/options.social.challenges.detail.html',
|
||||||
controller: ['$scope', 'Challenges', '$stateParams',
|
controller: ['$scope', 'Challenges', '$stateParams',
|
||||||
function($scope, Challenges, $stateParams){
|
function($scope, Challenges, $stateParams){
|
||||||
|
|
||||||
$scope.obj = $scope.challenge = Challenges.Challenge.get({cid:$stateParams.cid}, function(){
|
$scope.obj = $scope.challenge = Challenges.Challenge.get({cid:$stateParams.cid}, function(){
|
||||||
$scope.challenge._locked = true;
|
$scope.challenge._locked = true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
|
|||||||
// challenge
|
// challenge
|
||||||
$scope.cid = $state.params.cid;
|
$scope.cid = $state.params.cid;
|
||||||
|
|
||||||
|
$scope.groupIdFilter = $stateParams.groupIdFilter;
|
||||||
|
|
||||||
_getChallenges();
|
_getChallenges();
|
||||||
|
|
||||||
// FIXME $scope.challenges needs to be resolved first (see app.js)
|
// 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) {
|
function _calculateMaxPrize(gid) {
|
||||||
|
|
||||||
var userBalance = User.getBalanceInGems() || 0;
|
var userBalance = User.getBalanceInGems() || 0;
|
||||||
@@ -375,12 +392,7 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
|
|||||||
} else {
|
} else {
|
||||||
Challenges.Challenge.query(function(challenges){
|
Challenges.Challenge.query(function(challenges){
|
||||||
$scope.challenges = challenges;
|
$scope.challenges = challenges;
|
||||||
$scope.groupsFilter = _.uniq(_.pluck(challenges, 'group'), function(g){return g._id});
|
$scope.filterInitialChallenges();
|
||||||
$scope.search = {
|
|
||||||
group: _.transform($scope.groups, function(m,g){m[g._id]=true;}),
|
|
||||||
_isMember: "either",
|
|
||||||
_isOwner: "either"
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -114,8 +114,12 @@ habitrpg.controller("InventoryCtrl",
|
|||||||
var eggName = Content.eggs[egg.key].text();
|
var eggName = Content.eggs[egg.key].text();
|
||||||
var potName = Content.hatchingPotions[potion.key].text();
|
var potName = Content.hatchingPotions[potion.key].text();
|
||||||
if (!$window.confirm(window.env.t('hatchAPot', {potion: potName, egg: eggName}))) return;
|
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}});
|
user.ops.hatch({params:{egg:egg.key, hatchingPotion:potion.key}});
|
||||||
if (!user.preferences.suppressModals.hatchPet) {
|
|
||||||
|
if (!user.preferences.suppressModals.hatchPet && !userHasPet) {
|
||||||
$scope.hatchedPet = {
|
$scope.hatchedPet = {
|
||||||
egg: eggName,
|
egg: eggName,
|
||||||
potion: potName,
|
potion: potName,
|
||||||
@@ -159,7 +163,7 @@ habitrpg.controller("InventoryCtrl",
|
|||||||
// Feeding Pet
|
// Feeding Pet
|
||||||
if ($scope.selectedFood) {
|
if ($scope.selectedFood) {
|
||||||
var food = $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 (food.key === 'Saddle') {
|
||||||
if (!$window.confirm(window.env.t('useSaddle', {pet: petDisplayName}))) return;
|
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()}))) {
|
} 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;
|
$scope.selectedFood = null;
|
||||||
|
|
||||||
_updateDropAnimalCount(user.items);
|
_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 = {
|
$scope.raisedPet = {
|
||||||
displayName: petDisplayName,
|
displayName: petDisplayName,
|
||||||
spriteName: pet,
|
spriteName: pet,
|
||||||
|
|||||||
15
website/public/js/filters/timezoneOffsetToUtc.js
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -58,6 +58,7 @@
|
|||||||
"js/filters/money.js",
|
"js/filters/money.js",
|
||||||
"js/filters/roundLargeNumbers.js",
|
"js/filters/roundLargeNumbers.js",
|
||||||
"js/filters/taskOrdering.js",
|
"js/filters/taskOrdering.js",
|
||||||
|
"js/filters/timezoneOffsetToUtc.js",
|
||||||
|
|
||||||
"js/directives/close-menu.directive.js",
|
"js/directives/close-menu.directive.js",
|
||||||
"js/directives/expand-menu.directive.js",
|
"js/directives/expand-menu.directive.js",
|
||||||
|
|||||||
@@ -524,7 +524,7 @@ api.leave = function(req, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (group.quest && group.quest.active && group.quest.members && group.quest.members[user._id]) {
|
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.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,12 +164,22 @@ export let schema = new Schema({
|
|||||||
inviteParty: {type: Boolean, default: false},
|
inviteParty: {type: Boolean, default: false},
|
||||||
},
|
},
|
||||||
ios: {
|
ios: {
|
||||||
|
<<<<<<< HEAD
|
||||||
addTask: {type: Boolean, default: false},
|
addTask: {type: Boolean, default: false},
|
||||||
editTask: {type: Boolean, default: false},
|
editTask: {type: Boolean, default: false},
|
||||||
deleteTask: {type: Boolean, default: false},
|
deleteTask: {type: Boolean, default: false},
|
||||||
filterTask: {type: Boolean, default: false},
|
filterTask: {type: Boolean, default: false},
|
||||||
groupPets: {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},
|
dropsEnabled: {type: Boolean, default: false},
|
||||||
itemsEnabled: {type: Boolean, default: false},
|
itemsEnabled: {type: Boolean, default: false},
|
||||||
|
|||||||
@@ -116,6 +116,16 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
|
|||||||
ng-disabled='dayStart == user.preferences.dayStart')
|
ng-disabled='dayStart == user.preferences.dayStart')
|
||||||
=env.t('saveCustomDayStart')
|
=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
|
.personal-options.col-md-6
|
||||||
.panel.panel-default
|
.panel.panel-default
|
||||||
.panel-heading
|
.panel-heading
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
table.table.table-striped
|
table.table.table-striped
|
||||||
tr(ng-repeat='challenge in group.challenges')
|
tr(ng-repeat='challenge in group.challenges')
|
||||||
td
|
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')
|
div(ng-if='group.challenges.length == 0')
|
||||||
p
|
p
|
||||||
|
|
|
|
||||||
|
|||||||
@@ -139,32 +139,34 @@ script(type='text/ng-template', id='partials/options.social.challenges.html')
|
|||||||
placeholder=env.t('challengeTitle'), required='required',
|
placeholder=env.t('challengeTitle'), required='required',
|
||||||
ng-disabled='insufficientGemsForTavernChallenge()')
|
ng-disabled='insufficientGemsForTavernChallenge()')
|
||||||
|
|
||||||
.form-group
|
|
||||||
input.form-control(type='text', minlength="3", maxlength="16",
|
|
||||||
ng-model='newChallenge.shortName', placeholder=env.t('challengeTag'), required
|
|
||||||
ng-disabled='insufficientGemsForTavernChallenge()')
|
|
||||||
|
|
|
||||||
a.hint.vertical-20(target='_blank', href='http://habitica.wikia.com/wiki/Tags',
|
|
||||||
popover=env.t('challengeTagPop'), popover-trigger='mouseenter', popover-placement='right')
|
|
||||||
=env.t('moreInfo')
|
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
textarea.form-control(cols='3', placeholder=env.t('challengeDescr'), ng-model='newChallenge.description'
|
textarea.form-control(cols='3', placeholder=env.t('challengeDescr'), ng-model='newChallenge.description'
|
||||||
ng-disabled='insufficientGemsForTavernChallenge()')
|
ng-disabled='insufficientGemsForTavernChallenge()')
|
||||||
|
|
||||||
.form-group
|
.row
|
||||||
.input-group
|
.form-group.col-md-6.col-sm-12
|
||||||
span.input-group-addon
|
input.form-control(type='text', minlength="3",
|
||||||
.Pet_Currency_Gem1x
|
ng-model='newChallenge.shortName', placeholder=env.t('challengeTag'), required
|
||||||
input.form-control(type='number', placeholder=env.t('prize'),
|
ng-disabled='insufficientGemsForTavernChallenge()')
|
||||||
ng-disabled='insufficientGemsForTavernChallenge()'
|
|
|
||||||
min="{{newChallenge.group=='habitrpg' ? 1 : 0}}",
|
a.hint.vertical-20(target='_blank', href='http://habitica.wikia.com/wiki/Tags',
|
||||||
max="{{maxPrize}}", ng-model='newChallenge.prize')
|
popover=env.t('challengeTagPop'), popover-trigger='mouseenter', popover-placement='right')
|
||||||
a.hint(popover="{{newChallenge.group=='habitrpg' ? env.t('prizePopTavern') : env.t('prizePop')}}",
|
=env.t('moreInfo')
|
||||||
popover-trigger='mouseenter', popover-placement='right')
|
|
||||||
=env.t('moreInfo')
|
.row
|
||||||
.pull-right(ng-show='newChallenge.group=="habitrpg"')
|
.form-group.col-md-6.col-sm-12
|
||||||
!=env.t('publicChallenges')
|
.input-group
|
||||||
|
span.input-group-addon
|
||||||
|
.Pet_Currency_Gem1x
|
||||||
|
input.form-control(type='number', placeholder=env.t('prize'),
|
||||||
|
ng-disabled='insufficientGemsForTavernChallenge()'
|
||||||
|
min="{{newChallenge.group=='habitrpg' ? 1 : 0}}",
|
||||||
|
max="{{maxPrize}}", ng-model='newChallenge.prize')
|
||||||
|
a.hint(popover="{{newChallenge.group=='habitrpg' ? env.t('prizePopTavern') : env.t('prizePop')}}",
|
||||||
|
popover-trigger='mouseenter', popover-placement='right')
|
||||||
|
=env.t('moreInfo')
|
||||||
|
div(ng-show='newChallenge.group=="habitrpg"')
|
||||||
|
!=env.t('publicChallenges')
|
||||||
|
|
||||||
.form-group(ng-if='user.contributor.admin')
|
.form-group(ng-if='user.contributor.admin')
|
||||||
.checkbox
|
.checkbox
|
||||||
@@ -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)')
|
a.btn.btn-sm.btn-success(ng-hide='challenge._isMember', ng-click='join(challenge)')
|
||||||
span.glyphicon.glyphicon-ok
|
span.glyphicon.glyphicon-ok
|
||||||
=env.t('join')
|
=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}')
|
.panel-body(ng-class='{collapse: !$stateParams.cid == challenge._id}')
|
||||||
.accordion-inner(ng-if='$stateParams.cid == challenge._id')
|
.accordion-inner(ng-if='$stateParams.cid == challenge._id')
|
||||||
div(ui-view)
|
div(ui-view)
|
||||||
|
|||||||
@@ -1,26 +1,45 @@
|
|||||||
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
|
hr
|
||||||
tr
|
tr
|
||||||
td
|
td
|
||||||
h3 Habitica Hiring News
|
.npc_daniel.pull-right
|
||||||
p Exciting news! Right now, Habitica is looking to add a senior full stack developer to our team, and what better place to look than our awesome community?
|
h3 Happy Thanksgiving!
|
||||||
br
|
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!
|
||||||
p If you’d like to apply, you should have experience as a lead developer, and be a JavaScript whiz who is familiar with MongoDB and Angular. Bonus points for familiarity with <a href='http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths#Technology_Stack' target='_blank'>our tech stack</a>! Passion for open source is, naturally, a must ;)
|
|
||||||
br
|
|
||||||
p Send your resume to <a href='mailto:jobs@habitica.com'>jobs@habitica.com</a> with your GitHub handle, Habitica username, and list of favorite online hangouts! Please also let us know whether or not you would be able to move to Los Angeles. We’re looking forward to hearing from you!
|
|
||||||
tr
|
tr
|
||||||
td
|
td
|
||||||
h3 Small iOS App Update
|
.Pet-Turkey-Gilded.pull-right
|
||||||
p We've released a small <a href='https://itunes.apple.com/us/app/habitica/id994882113?ls=1&mt=8' target='_blank'>iOS update</a>, just to fix some bothersome bugs (including crashes), add a nice intro slide for new users, and make it more obvious how to invite your friends to your party.
|
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
|
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 Thank you for using Habitica - we really love you guys <3
|
||||||
p.small.muted by Viirus and Lemoness
|
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'
|
if menuItem !== 'oldNews'
|
||||||
hr
|
hr
|
||||||
a(href='/static/old-news', target='_blank') Read older news
|
a(href='/static/old-news', target='_blank') Read older news
|
||||||
|
|
||||||
mixin oldNews
|
mixin oldNews
|
||||||
|
h2 11/19/2015 - SMALL iOS UPDATE AND HABITICA HIRING NEWS!
|
||||||
|
tr
|
||||||
|
td
|
||||||
|
h3 Habitica Hiring News
|
||||||
|
p Exciting news! Right now, Habitica is looking to add a senior full stack developer to our team, and what better place to look than our awesome community?
|
||||||
|
br
|
||||||
|
p If you’d like to apply, you should have experience as a lead developer, and be a JavaScript whiz who is familiar with MongoDB and Angular. Bonus points for familiarity with <a href='http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths#Technology_Stack' target='_blank'>our tech stack</a>! Passion for open source is, naturally, a must ;)
|
||||||
|
br
|
||||||
|
p Send your resume to <a href='mailto:jobs@habitica.com'>jobs@habitica.com</a> with your GitHub handle, Habitica username, and list of favorite online hangouts! Please also let us know whether or not you would be able to move to Los Angeles. We’re looking forward to hearing from you!
|
||||||
|
tr
|
||||||
|
td
|
||||||
|
h3 Small iOS App Update
|
||||||
|
p We've released a small <a href='https://itunes.apple.com/us/app/habitica/id994882113?ls=1&mt=8' target='_blank'>iOS update</a>, just to fix some bothersome bugs (including crashes), add a nice intro slide for new users, and make it more obvious how to invite your friends to your party.
|
||||||
|
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
|
||||||
h2 11/16/2015 - HABITICA STICKERS AND COSTUME CONTEST BADGES!
|
h2 11/16/2015 - HABITICA STICKERS AND COSTUME CONTEST BADGES!
|
||||||
tr
|
tr
|
||||||
td
|
td
|
||||||
|
|||||||
@@ -589,8 +589,7 @@ html(ng-app='habitrpg', ng-controller='RootCtrl')
|
|||||||
#footercall
|
#footercall
|
||||||
.container-fluid
|
.container-fluid
|
||||||
.row
|
.row
|
||||||
.col-md-10
|
h3= env.t('joinOthers', {userCount:'900,000'})
|
||||||
h3= env.t('joinOthers')
|
|
||||||
.row
|
.row
|
||||||
.col-md-4.col-md-offset-4
|
.col-md-4.col-md-offset-4
|
||||||
button.btn.btn-primary.btn-lg.btn-block(ng-click='playButtonClick()')= env.t('free')
|
button.btn.btn-primary.btn-lg.btn-block(ng-click='playButtonClick()')= env.t('free')
|
||||||
|
|||||||