Merge branch 'develop' into Ozzystrasza-tavern_default_prize
@@ -1,6 +1,6 @@
|
||||
.2014_Fall_HealerPROMO2 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -637px -1378px;
|
||||
background-position: -1001px -1378px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
@@ -12,19 +12,19 @@
|
||||
}
|
||||
.2014_Fall_RoguePROMO3 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -452px -865px;
|
||||
background-position: -306px -220px;
|
||||
width: 105px;
|
||||
height: 90px;
|
||||
}
|
||||
.2014_Fall_Warrior_PROMO {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -1013px -1275px;
|
||||
background-position: -1295px -1275px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_android {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -1278px -199px;
|
||||
background-position: -813px -151px;
|
||||
width: 175px;
|
||||
height: 175px;
|
||||
}
|
||||
@@ -64,6 +64,12 @@
|
||||
width: 139px;
|
||||
height: 588px;
|
||||
}
|
||||
.promo_backgrounds_armoire_201608 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -452px -442px;
|
||||
width: 140px;
|
||||
height: 439px;
|
||||
}
|
||||
.promo_backtoschool {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -1493px -151px;
|
||||
@@ -78,7 +84,7 @@
|
||||
}
|
||||
.promo_chairs_glasses {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -452px -442px;
|
||||
background-position: -452px -882px;
|
||||
width: 51px;
|
||||
height: 210px;
|
||||
}
|
||||
@@ -96,7 +102,7 @@
|
||||
}
|
||||
.promo_dilatoryDistress {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -831px -1275px;
|
||||
background-position: -1113px -1275px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
@@ -126,7 +132,7 @@
|
||||
}
|
||||
.promo_enchanted_armoire_201509 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -1286px -1275px;
|
||||
background-position: -182px -1378px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
@@ -138,7 +144,7 @@
|
||||
}
|
||||
.promo_enchanted_armoire_201601 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -1378px;
|
||||
background-position: -364px -1378px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
@@ -150,7 +156,7 @@
|
||||
}
|
||||
.promo_habitica {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -813px -151px;
|
||||
background-position: -1278px -199px;
|
||||
width: 175px;
|
||||
height: 175px;
|
||||
}
|
||||
@@ -174,13 +180,13 @@
|
||||
}
|
||||
.promo_mystery_201405 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -273px -1378px;
|
||||
background-position: -637px -1378px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201406 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -452px -956px;
|
||||
background-position: 0px -1378px;
|
||||
width: 90px;
|
||||
height: 96px;
|
||||
}
|
||||
@@ -198,7 +204,7 @@
|
||||
}
|
||||
.promo_mystery_201409 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -455px -1378px;
|
||||
background-position: -819px -1378px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
@@ -210,7 +216,7 @@
|
||||
}
|
||||
.promo_mystery_201411 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -546px -1378px;
|
||||
background-position: -910px -1378px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
@@ -228,13 +234,13 @@
|
||||
}
|
||||
.promo_mystery_201502 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -649px -1275px;
|
||||
background-position: -931px -1275px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201503 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -91px -1378px;
|
||||
background-position: -455px -1378px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
@@ -246,7 +252,7 @@
|
||||
}
|
||||
.promo_mystery_201505 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -1377px -1275px;
|
||||
background-position: -273px -1378px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
@@ -258,31 +264,31 @@
|
||||
}
|
||||
.promo_mystery_201507 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -452px -653px;
|
||||
background-position: -306px -417px;
|
||||
width: 90px;
|
||||
height: 105px;
|
||||
}
|
||||
.promo_mystery_201508 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -306px -402px;
|
||||
background-position: -837px -1275px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201509 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -1104px -1275px;
|
||||
background-position: -1386px -1275px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201510 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -306px -311px;
|
||||
background-position: -743px -1275px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201511 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -922px -1275px;
|
||||
background-position: -1204px -1275px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
@@ -300,37 +306,37 @@
|
||||
}
|
||||
.promo_mystery_201602 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -740px -1275px;
|
||||
background-position: -1022px -1275px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201603 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -364px -1378px;
|
||||
background-position: -728px -1378px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201604 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -306px -220px;
|
||||
background-position: -649px -1275px;
|
||||
width: 93px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201605 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -182px -1378px;
|
||||
background-position: -546px -1378px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_mystery_201606 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -452px -759px;
|
||||
background-position: -306px -311px;
|
||||
width: 90px;
|
||||
height: 105px;
|
||||
}
|
||||
.promo_mystery_201607 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -1195px -1275px;
|
||||
background-position: -91px -1378px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
@@ -408,13 +414,13 @@
|
||||
}
|
||||
.promo_springclasses2014 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -699px -785px;
|
||||
background-position: -699px -876px;
|
||||
width: 288px;
|
||||
height: 90px;
|
||||
}
|
||||
.promo_springclasses2015 {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -699px -876px;
|
||||
background-position: -699px -785px;
|
||||
width: 288px;
|
||||
height: 90px;
|
||||
}
|
||||
@@ -522,7 +528,7 @@
|
||||
}
|
||||
.welcome_basic_avatars {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -994px -642px;
|
||||
background-position: -1241px -642px;
|
||||
width: 246px;
|
||||
height: 165px;
|
||||
}
|
||||
@@ -534,7 +540,7 @@
|
||||
}
|
||||
.welcome_sample_tasks {
|
||||
background-image: url(spritesmith-largeSprites-0.png);
|
||||
background-position: -1241px -642px;
|
||||
background-position: -994px -642px;
|
||||
width: 246px;
|
||||
height: 165px;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 385 KiB After Width: | Height: | Size: 397 KiB |
|
Before Width: | Height: | Size: 335 KiB After Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 377 KiB After Width: | Height: | Size: 400 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 19 KiB |
@@ -207,5 +207,13 @@
|
||||
"backgroundDeepSeaText": "Deep Sea",
|
||||
"backgroundDeepSeaNotes": "Dive to the Deep Sea.",
|
||||
"backgroundDilatoryCastleText": "Castle of Dilatory",
|
||||
"backgroundDilatoryCastleNotes": "Swim past the Castle of Dilatory."
|
||||
"backgroundDilatoryCastleNotes": "Swim past the Castle of Dilatory.",
|
||||
|
||||
"backgrounds082016": "SET 27: Released August 2016",
|
||||
"backgroundIdyllicCabinText": "Idyllic Cabin",
|
||||
"backgroundIdyllicCabinNotes": "Retreat to an Idyllic Cabin.",
|
||||
"backgroundMountainPyramidText": "Mountain Pyramid",
|
||||
"backgroundMountainPyramidNotes": "Climb the many steps of a Mountain Pyramid.",
|
||||
"backgroundStormyShipText": "Stormy Ship",
|
||||
"backgroundStormyShipNotes": "Hold steady against wind and wave aboard a Stormy Ship."
|
||||
}
|
||||
|
||||
@@ -481,6 +481,8 @@
|
||||
"armorArmoireStripedSwimsuitNotes": "What could be more fun than battling sea monsters on the beach? Increases Constitution by <%= con %>. Enchanted Armoire: Seaside Set (Item 2 of 3).",
|
||||
"armorArmoireCannoneerRagsText": "Cannoneer Rags",
|
||||
"armorArmoireCannoneerRagsNotes": "These rags be tougher than they look. Increases Constitution by <%= con %>. Enchanted Armoire: Cannoneer Set (Item 2 of 3).",
|
||||
"armorArmoireFalconerArmorText": "Falconer Armor",
|
||||
"armorArmoireFalconerArmorNotes": "Keep away talon attacks with this sturdy armor! Increases Constitution by <%= con %>. Enchanted Armoire: Falconer Set (Item 1 of 3).",
|
||||
|
||||
"headgear": "headgear",
|
||||
"headgearCapitalized": "Headgear",
|
||||
@@ -746,6 +748,8 @@
|
||||
"headArmoireGreenFloppyHatNotes": "Many spells have been sewn into this simple hat, giving it a gorgeous green color. Increases Constitution, Intelligence, and Perception by <%= attrs %> each. Enchanted Armoire: Independent Item.",
|
||||
"headArmoireCannoneerBandannaText": "Cannoneer Bandanna",
|
||||
"headArmoireCannoneerBandannaNotes": "'Tis a cannoneer's life for me! Increases Intelligence and Perception by <%= attrs %> each. Enchanted Armoire: Cannoneer Set (Item 3 of 3).",
|
||||
"headArmoireFalconerCapText": "Falconer Cap",
|
||||
"headArmoireFalconerCapNotes": "This jaunty cap helps you better understand birds of prey. Increases Intelligence by <%= int %>. Enchanted Armoire: Falconer Set (Item 2 of 3).",
|
||||
|
||||
"offhand": "shield-hand item",
|
||||
"offhandCapitalized": "Shield-Hand Item",
|
||||
@@ -881,6 +885,8 @@
|
||||
"shieldArmoireFloralBouquetNotes": "Not much help in battle, but aren't they beautiful? Increases Constitution by <%= con %>. Enchanted Armoire: Independent Item.",
|
||||
"shieldArmoireSandyBucketText": "Sandy Bucket",
|
||||
"shieldArmoireSandyBucketNotes": "Good for storing all that Gold that you'll earn from completing tasks! Increases Perception by <%= per %>. Enchanted Armoire: Seaside Set (Item 3 of 3).",
|
||||
"shieldArmoirePerchingFalconText": "Perching Falcon",
|
||||
"shieldArmoirePerchingFalconNotes": "A falcon friend perches on your arm, prepared to swoop at your enemies. Increases Strength by <%= str %>. Enchanted Armoire: Falconer Set (Item 3 of 3).",
|
||||
|
||||
"back": "Back Accessory",
|
||||
"backBase0Text": "No Back Accessory",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"audioTheme_gokulTheme": "Gokul Theme",
|
||||
"audioTheme_luneFoxTheme": "LuneFox's Theme",
|
||||
"audioTheme_rosstavoTheme": "Rosstavo's Theme",
|
||||
"audioTheme_dewinTheme": "Dewin's Theme",
|
||||
"askQuestion": "Ask a Question",
|
||||
"reportBug": "Report a Bug",
|
||||
"HabiticaWiki": "The Habitica Wiki",
|
||||
|
||||
@@ -367,6 +367,20 @@ let backgrounds = {
|
||||
notes: t('backgroundDeepSeaNotes'),
|
||||
},
|
||||
},
|
||||
backgrounds082016: {
|
||||
idyllic_cabin: {
|
||||
text: t('backgroundIdyllicCabinText'),
|
||||
notes: t('backgroundIdyllicCabinNotes'),
|
||||
},
|
||||
mountain_pyramid: {
|
||||
text: t('backgroundMountainPyramidText'),
|
||||
notes: t('backgroundMountainPyramidNotes'),
|
||||
},
|
||||
stormy_ship: {
|
||||
text: t('backgroundStormyShipText'),
|
||||
notes: t('backgroundStormyShipNotes'),
|
||||
},
|
||||
},
|
||||
};
|
||||
/* eslint-enable quote-props */
|
||||
|
||||
|
||||
@@ -150,6 +150,14 @@ let armor = {
|
||||
set: 'cannoneer',
|
||||
canOwn: ownsItem('armor_armoire_cannoneerRags'),
|
||||
},
|
||||
falconerArmor: {
|
||||
text: t('armorArmoireFalconerArmorText'),
|
||||
notes: t('armorArmoireFalconerArmorNotes', { con: 10 }),
|
||||
value: 100,
|
||||
con: 10,
|
||||
set: 'falconer',
|
||||
canOwn: ownsItem('armor_armoire_falconerArmor'),
|
||||
},
|
||||
};
|
||||
|
||||
let eyewear = {
|
||||
@@ -380,6 +388,14 @@ let head = {
|
||||
set: 'cannoneer',
|
||||
canOwn: ownsItem('head_armoire_cannoneerBandanna'),
|
||||
},
|
||||
falconerCap: {
|
||||
text: t('headArmoireFalconerCapText'),
|
||||
notes: t('headArmoireFalconerCapNotes', { int: 10 }),
|
||||
value: 100,
|
||||
int: 10,
|
||||
set: 'falconer',
|
||||
canOwn: ownsItem('head_armoire_falconerCap'),
|
||||
},
|
||||
};
|
||||
|
||||
let shield = {
|
||||
@@ -440,6 +456,14 @@ let shield = {
|
||||
set: 'seaside',
|
||||
canOwn: ownsItem('shield_armoire_sandyBucket'),
|
||||
},
|
||||
perchingFalcon: {
|
||||
text: t('shieldArmoirePerchingFalconText'),
|
||||
notes: t('shieldArmoirePerchingFalconNotes', { str: 16 }),
|
||||
value: 100,
|
||||
str: 16,
|
||||
set: 'falconer',
|
||||
canOwn: ownsItem('shield_armoire_perchingFalcon'),
|
||||
},
|
||||
};
|
||||
|
||||
let headAccessory = {
|
||||
|
||||
@@ -53,7 +53,7 @@ shops.getMarketCategories = function getMarket (user, language) {
|
||||
value: hatchingPotion.value,
|
||||
locked: false,
|
||||
currency: 'gems',
|
||||
purchaseType: 'hatchingpotions',
|
||||
purchaseType: 'hatchingPotions',
|
||||
};
|
||||
}).sortBy('key').value();
|
||||
categories.push(hatchingPotionsCategory);
|
||||
@@ -192,14 +192,6 @@ shops.getTimeTravelersCategories = function getTimeTravelersCategories (user, la
|
||||
// };
|
||||
shops.getSeasonalShopCategories = function getSeasonalShopCategories (user, language) {
|
||||
let availableSets = {
|
||||
summerWarrior: i18n.t('daringSwashbucklerSet', language),
|
||||
summerMage: i18n.t('emeraldMermageSet', language),
|
||||
summerHealer: i18n.t('reefSeahealerSet', language),
|
||||
summerRogue: i18n.t('roguishPirateSet', language),
|
||||
summer2015Warrior: i18n.t('sunfishWarriorSet', language),
|
||||
summer2015Mage: i18n.t('shipSoothsayerSet', language),
|
||||
summer2015Healer: i18n.t('strappingSailorSet', language),
|
||||
summer2015Rogue: i18n.t('reefRenegadeSet', language),
|
||||
};
|
||||
|
||||
let categories = [];
|
||||
|
||||
@@ -65,7 +65,8 @@
|
||||
"LOGGLY_ACCOUNT": "account",
|
||||
"PUSH_CONFIGS": {
|
||||
"GCM_SERVER_API_KEY": "",
|
||||
"APN_ENABLED": "true"
|
||||
"APN_ENABLED": "true",
|
||||
"FCM_SERVER_API_KEY": ""
|
||||
},
|
||||
"PUSHER": {
|
||||
"ENABLED": "false",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"dependencies": {
|
||||
"abbrev": {
|
||||
"version": "1.0.9",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"accepts": "^1.3.2",
|
||||
@@ -68,6 +68,7 @@
|
||||
"nconf": "~0.8.2",
|
||||
"newrelic": "^1.27.2",
|
||||
"nib": "^1.1.0",
|
||||
"node-gcm": "^0.14.4",
|
||||
"nodemailer": "^2.3.2",
|
||||
"object-path": "^0.9.2",
|
||||
"pageres": "^4.1.1",
|
||||
|
||||
@@ -4,12 +4,21 @@ import {
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import common from '../../../../../common';
|
||||
|
||||
describe('GET /groups/:groupId/members', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
user = await generateUser({
|
||||
balance: 10,
|
||||
contributor: {level: 1},
|
||||
backer: {tier: 3},
|
||||
preferences: {
|
||||
costume: false,
|
||||
background: 'volcano',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('validates optional req.query.lastId to be an UUID', async () => {
|
||||
@@ -57,6 +66,30 @@ describe('GET /groups/:groupId/members', () => {
|
||||
expect(res[0].profile).to.have.all.keys(['name']);
|
||||
});
|
||||
|
||||
it('req.query.includeAllPublicFields === true only works with parties', async () => {
|
||||
let group = await generateGroup(user, {type: 'guild', name: generateUUID()});
|
||||
let res = await user.get(`/groups/${group._id}/members?includeAllPublicFields=true`);
|
||||
expect(res[0]).to.have.all.keys(['_id', 'id', 'profile']);
|
||||
expect(res[0].profile).to.have.all.keys(['name']);
|
||||
});
|
||||
|
||||
it('populates all public fields if req.query.includeAllPublicFields === true and it is a party', async () => {
|
||||
await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let [memberRes] = await user.get('/groups/party/members?includeAllPublicFields=true');
|
||||
|
||||
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||
'backer', 'contributor', 'auth', 'items',
|
||||
]);
|
||||
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql(['size', 'hair', 'skin', 'shirt',
|
||||
'chair', 'costume', 'sleep', 'background'].sort());
|
||||
|
||||
expect(memberRes.stats.maxMP).to.exists;
|
||||
expect(memberRes.stats.maxHealth).to.equal(common.maxHealth);
|
||||
expect(memberRes.stats.toNextLevel).to.equal(common.tnl(memberRes.stats.lvl));
|
||||
});
|
||||
|
||||
it('returns only first 30 members', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import {
|
||||
generateUser,
|
||||
resetHabiticaDB,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('GET /hall/heroes', () => {
|
||||
it('returns all heroes sorted by -contributor.level and with correct fields', async () => {
|
||||
await resetHabiticaDB();
|
||||
|
||||
let nonHero = await generateUser();
|
||||
let hero1 = await generateUser({
|
||||
contributor: {level: 1},
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import common from '../../../../../common';
|
||||
|
||||
describe('GET /members/:memberId', () => {
|
||||
let user;
|
||||
@@ -36,6 +37,10 @@ describe('GET /members/:memberId', () => {
|
||||
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql(['size', 'hair', 'skin', 'shirt',
|
||||
'chair', 'costume', 'sleep', 'background'].sort());
|
||||
|
||||
expect(memberRes.stats.maxMP).to.exists;
|
||||
expect(memberRes.stats.maxHealth).to.equal(common.maxHealth);
|
||||
expect(memberRes.stats.toNextLevel).to.equal(common.tnl(memberRes.stats.lvl));
|
||||
});
|
||||
|
||||
it('handles non-existing members', async () => {
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('GET /shops/seasonal', () => {
|
||||
|
||||
expect(shop.identifier).to.equal('seasonalShop');
|
||||
expect(shop.text).to.eql(t('seasonalShop'));
|
||||
expect(shop.notes).to.eql(t('seasonalShopSummerText'));
|
||||
expect(shop.notes).to.eql(t('seasonalShopClosedText'));
|
||||
expect(shop.imageName).to.be.a('string');
|
||||
expect(shop.categories).to.be.an('array');
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import common from '../../../../../common';
|
||||
|
||||
describe('GET /user', () => {
|
||||
let user;
|
||||
@@ -9,9 +10,13 @@ describe('GET /user', () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('returns the authenticated user', async () => {
|
||||
it('returns the authenticated user with computed stats', async () => {
|
||||
let returnedUser = await user.get('/user');
|
||||
expect(returnedUser._id).to.equal(user._id);
|
||||
|
||||
expect(returnedUser.stats.maxMP).to.exists;
|
||||
expect(returnedUser.stats.maxHealth).to.equal(common.maxHealth);
|
||||
expect(returnedUser.stats.toNextLevel).to.equal(common.tnl(returnedUser.stats.lvl));
|
||||
});
|
||||
|
||||
it('does not return private paths (and apiToken)', async () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ describe('POST /user/auth/local/login', () => {
|
||||
api = requester();
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('success with username', async () => {
|
||||
let response = await api.post(endpoint, {
|
||||
username: user.auth.local.username,
|
||||
@@ -20,6 +21,7 @@ describe('POST /user/auth/local/login', () => {
|
||||
});
|
||||
expect(response.apiToken).to.eql(user.apiToken);
|
||||
});
|
||||
|
||||
it('success with email', async () => {
|
||||
let response = await api.post(endpoint, {
|
||||
username: user.auth.local.email,
|
||||
@@ -27,6 +29,7 @@ describe('POST /user/auth/local/login', () => {
|
||||
});
|
||||
expect(response.apiToken).to.eql(user.apiToken);
|
||||
});
|
||||
|
||||
it('user is blocked', async () => {
|
||||
await user.update({ 'auth.blocked': 1 });
|
||||
await expect(api.post(endpoint, {
|
||||
@@ -38,6 +41,7 @@ describe('POST /user/auth/local/login', () => {
|
||||
message: t('accountSuspended', { userId: user._id }),
|
||||
});
|
||||
});
|
||||
|
||||
it('wrong password', async () => {
|
||||
await expect(api.post(endpoint, {
|
||||
username: user.auth.local.username,
|
||||
@@ -48,6 +52,7 @@ describe('POST /user/auth/local/login', () => {
|
||||
message: t('invalidLoginCredentialsLong'),
|
||||
});
|
||||
});
|
||||
|
||||
it('missing username', async () => {
|
||||
await expect(api.post(endpoint, {
|
||||
password: 'wrong-password',
|
||||
@@ -57,6 +62,7 @@ describe('POST /user/auth/local/login', () => {
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('missing password', async () => {
|
||||
await expect(api.post(endpoint, {
|
||||
username: user.auth.local.username,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import passport from 'passport';
|
||||
|
||||
describe('POST /user/auth/social', () => {
|
||||
let api;
|
||||
let user;
|
||||
let endpoint = '/user/auth/social';
|
||||
let randomAccessToken = '123456';
|
||||
let facebookId = 'facebookId';
|
||||
let network = 'facebook';
|
||||
|
||||
before(async () => {
|
||||
api = requester();
|
||||
user = await generateUser();
|
||||
|
||||
let expectedResult = {id: facebookId};
|
||||
let passportFacebookProfile = sinon.stub(passport._strategies.facebook, 'userProfile');
|
||||
passportFacebookProfile.yields(null, expectedResult);
|
||||
});
|
||||
|
||||
it('fails if network is not facebook', async () => {
|
||||
await expect(api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network: 'NotFacebook',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyFbSupported'),
|
||||
});
|
||||
});
|
||||
|
||||
it('registers a new user', async () => {
|
||||
let response = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.true;
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
await user.update({ 'auth.facebook.id': facebookId });
|
||||
|
||||
let response = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(user.apiToken);
|
||||
expect(response.id).to.eql(user._id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
});
|
||||
@@ -2,12 +2,13 @@ import { model as User } from '../../../../../website/server/models/user';
|
||||
import requireAgain from 'require-again';
|
||||
import pushNotify from 'push-notify';
|
||||
import nconf from 'nconf';
|
||||
import gcmLib from 'node-gcm'; // works with FCM notifications too
|
||||
|
||||
describe('pushNotifications', () => {
|
||||
let user;
|
||||
let sendPushNotification;
|
||||
let pathToPushNotifications = '../../../../../website/server/libs/pushNotifications';
|
||||
let gcmSendSpy;
|
||||
let fcmSendSpy;
|
||||
let apnSendSpy;
|
||||
|
||||
let identifier = 'identifier';
|
||||
@@ -16,15 +17,12 @@ describe('pushNotifications', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
gcmSendSpy = sinon.spy();
|
||||
fcmSendSpy = sinon.spy();
|
||||
apnSendSpy = sinon.spy();
|
||||
|
||||
sandbox.stub(nconf, 'get').returns('true');
|
||||
sandbox.stub(nconf, 'get').returns('true-key');
|
||||
|
||||
sandbox.stub(pushNotify, 'gcm').returns({
|
||||
on: () => null,
|
||||
send: gcmSendSpy,
|
||||
});
|
||||
sandbox.stub(gcmLib.Sender.prototype, 'send', fcmSendSpy);
|
||||
|
||||
sandbox.stub(pushNotify, 'apn').returns({
|
||||
on: () => null,
|
||||
@@ -40,14 +38,14 @@ describe('pushNotifications', () => {
|
||||
|
||||
it('throws if user is not supplied', () => {
|
||||
expect(sendPushNotification).to.throw;
|
||||
expect(gcmSendSpy).to.not.have.been.called;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('throws if user.preferences.pushNotifications.unsubscribeFromAll is true', () => {
|
||||
user.preferences.pushNotifications.unsubscribeFromAll = true;
|
||||
expect(() => sendPushNotification(user)).to.throw;
|
||||
expect(gcmSendSpy).to.not.have.been.called;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
@@ -56,7 +54,7 @@ describe('pushNotifications', () => {
|
||||
title,
|
||||
message,
|
||||
})).to.throw;
|
||||
expect(gcmSendSpy).to.not.have.been.called;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
@@ -65,7 +63,7 @@ describe('pushNotifications', () => {
|
||||
identifier,
|
||||
message,
|
||||
})).to.throw;
|
||||
expect(gcmSendSpy).to.not.have.been.called;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
@@ -74,7 +72,7 @@ describe('pushNotifications', () => {
|
||||
identifier,
|
||||
title,
|
||||
})).to.throw;
|
||||
expect(gcmSendSpy).to.not.have.been.called;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
@@ -84,68 +82,7 @@ describe('pushNotifications', () => {
|
||||
title,
|
||||
message,
|
||||
});
|
||||
expect(gcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('uses GCM for Android devices', () => {
|
||||
user.pushDevices.push({
|
||||
type: 'android',
|
||||
regId: '123',
|
||||
});
|
||||
|
||||
let details = {
|
||||
identifier,
|
||||
title,
|
||||
message,
|
||||
payload: {
|
||||
a: true,
|
||||
b: true,
|
||||
},
|
||||
timeToLive: 23,
|
||||
};
|
||||
|
||||
sendPushNotification(user, details);
|
||||
expect(gcmSendSpy).to.have.been.calledOnce;
|
||||
expect(gcmSendSpy).to.have.been.calledWithMatch({
|
||||
registrationId: '123',
|
||||
delayWhileIdle: true,
|
||||
timeToLive: 23,
|
||||
data: {
|
||||
identifier,
|
||||
title,
|
||||
message,
|
||||
a: true,
|
||||
b: true,
|
||||
},
|
||||
});
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('defaults timeToLive to 15', () => {
|
||||
user.pushDevices.push({
|
||||
type: 'android',
|
||||
regId: '123',
|
||||
});
|
||||
|
||||
let details = {
|
||||
identifier,
|
||||
title,
|
||||
message,
|
||||
};
|
||||
|
||||
sendPushNotification(user, details);
|
||||
expect(gcmSendSpy).to.have.been.calledOnce;
|
||||
expect(gcmSendSpy).to.have.been.calledWithMatch({
|
||||
registrationId: '123',
|
||||
delayWhileIdle: true,
|
||||
timeToLive: 15,
|
||||
data: {
|
||||
identifier,
|
||||
title,
|
||||
message,
|
||||
},
|
||||
});
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
@@ -180,6 +117,6 @@ describe('pushNotifications', () => {
|
||||
b: true,
|
||||
},
|
||||
});
|
||||
expect(gcmSendSpy).to.not.have.been.called;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import common from '../../../../../common';
|
||||
|
||||
describe('User Model', () => {
|
||||
it('keeps user._tmp when calling .toJSON', () => {
|
||||
@@ -31,6 +32,21 @@ describe('User Model', () => {
|
||||
expect(toJSON).to.not.have.keys('_nonTmp');
|
||||
});
|
||||
|
||||
it('can add computed stats to a JSONified user object', () => {
|
||||
let user = new User();
|
||||
let userToJSON = user.toJSON();
|
||||
|
||||
expect(userToJSON.stats.maxMP).to.not.exists;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exists;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exists;
|
||||
|
||||
user.addComputedStatsToJSONObj(userToJSON);
|
||||
|
||||
expect(userToJSON.stats.maxMP).to.exists;
|
||||
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);
|
||||
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
|
||||
});
|
||||
|
||||
context('notifications', () => {
|
||||
it('can add notifications with data', () => {
|
||||
let user = new User();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
display: inline-block
|
||||
|
||||
.wide-popover
|
||||
max-width: -moz-max-content
|
||||
max-width: max-content
|
||||
|
||||
// Small alerts
|
||||
|
||||
@@ -163,7 +163,7 @@ api.registerLocal = {
|
||||
|
||||
function _loginRes (user, req, res) {
|
||||
if (user.auth.blocked) throw new NotAuthorized(res.t('accountSuspended', {userId: user._id}));
|
||||
return res.respond(200, {id: user._id, apiToken: user.apiToken});
|
||||
return res.respond(200, {id: user._id, apiToken: user.apiToken, newUser: user.newUser || false});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,6 +178,7 @@ function _loginRes (user, req, res) {
|
||||
*
|
||||
* @apiSuccess {String} data._id The user's unique identifier
|
||||
* @apiSuccess {String} data.apiToken The user's api token that must be used to authenticate requests.
|
||||
* @apiSuccess {Boolean} data.newUser Returns true if the user was just created (always false for local login).
|
||||
*/
|
||||
api.loginLocal = {
|
||||
method: 'POST',
|
||||
@@ -260,6 +261,7 @@ api.loginSocial = {
|
||||
|
||||
let savedUser = await user.save();
|
||||
|
||||
user.newUser = true;
|
||||
_loginRes(user, ...arguments);
|
||||
|
||||
// Clean previous email preferences
|
||||
|
||||
@@ -50,7 +50,10 @@ api.getMember = {
|
||||
if (!member) throw new NotFound(res.t('userWithIDNotFound', {userId: memberId}));
|
||||
|
||||
// manually call toJSON with minimize: true so empty paths aren't returned
|
||||
res.respond(200, member.toJSON({minimize: true}));
|
||||
let memberToJSON = member.toJSON({minimize: true});
|
||||
member.addComputedStatsToJSONObj(memberToJSON);
|
||||
|
||||
res.respond(200, memberToJSON);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -100,6 +103,7 @@ function _getMembersForItem (type) {
|
||||
|
||||
let query = {};
|
||||
let fields = nameFields;
|
||||
let addComputedStats = false; // add computes stats to the member info when items and stats are available
|
||||
|
||||
if (type === 'challenge-members') {
|
||||
query.challenges = challenge._id;
|
||||
@@ -111,6 +115,7 @@ function _getMembersForItem (type) {
|
||||
|
||||
if (req.query.includeAllPublicFields === 'true') {
|
||||
fields = memberFields;
|
||||
addComputedStats = true;
|
||||
}
|
||||
}
|
||||
} else if (type === 'group-invites') {
|
||||
@@ -138,7 +143,13 @@ function _getMembersForItem (type) {
|
||||
.exec();
|
||||
|
||||
// manually call toJSON with minimize: true so empty paths aren't returned
|
||||
res.respond(200, members.map(member => member.toJSON({minimize: true})));
|
||||
let membersToJSON = members.map(member => {
|
||||
let memberToJSON = member.toJSON({minimize: true});
|
||||
if (addComputedStats) member.addComputedStatsToJSONObj(memberToJSON);
|
||||
|
||||
return memberToJSON;
|
||||
});
|
||||
res.respond(200, membersToJSON);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -111,8 +111,8 @@ api.getSeasonalShopItems = {
|
||||
let resObject = {
|
||||
identifier: 'seasonalShop',
|
||||
text: res.t('seasonalShop'),
|
||||
notes: res.t('seasonalShopSummerText'),
|
||||
imageName: 'seasonalshop_open',
|
||||
notes: res.t('seasonalShopClosedText'),
|
||||
imageName: 'seasonalshop_closed',
|
||||
categories: shops.getSeasonalShopCategories(user, req.language),
|
||||
};
|
||||
|
||||
|
||||
@@ -30,19 +30,14 @@ api.getUser = {
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user.toJSON();
|
||||
let user = res.locals.user;
|
||||
let userToJSON = user.toJSON();
|
||||
|
||||
// Remove apiToken from response TODO make it private at the user level? returned in signup/login
|
||||
delete user.apiToken;
|
||||
delete userToJSON.apiToken;
|
||||
|
||||
// TODO move to model? (maybe virtuals, maybe in toJSON)
|
||||
// NOTE: if an item is manually added to user.stats common/fns/predictableRandom must be tweaked
|
||||
// so it's not considered. Otherwise the client will have it while the server won't and the results will be different.
|
||||
user.stats.toNextLevel = common.tnl(user.stats.lvl);
|
||||
user.stats.maxHealth = common.maxHealth;
|
||||
user.stats.maxMP = common.statsComputed(user).maxMP;
|
||||
|
||||
return res.respond(200, user);
|
||||
user.addComputedStatsToJSONObj(userToJSON);
|
||||
return res.respond(200, userToJSON);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const md = markdownIt({
|
||||
|
||||
let api = {};
|
||||
|
||||
const TOTAL_USER_COUNT = '1,100,000';
|
||||
const TOTAL_USER_COUNT = '1,500,000';
|
||||
const LOADING_SCREEN_TIPS = 32;
|
||||
|
||||
api.getFrontPage = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import nconf from 'nconf';
|
||||
// TODO remove this lib and use directly the apn module
|
||||
import pushNotify from 'push-notify';
|
||||
import apnLib from 'apn';
|
||||
import logger from './logger';
|
||||
@@ -7,19 +8,11 @@ import Bluebird from 'bluebird';
|
||||
import {
|
||||
S3,
|
||||
} from './aws';
|
||||
import gcmLib from 'node-gcm'; // works with FCM notifications too
|
||||
|
||||
const GCM_API_KEY = nconf.get('PUSH_CONFIGS:GCM_SERVER_API_KEY');
|
||||
const FCM_API_KEY = nconf.get('PUSH_CONFIGS:FCM_SERVER_API_KEY');
|
||||
|
||||
let gcm = GCM_API_KEY ? pushNotify.gcm({
|
||||
apiKey: GCM_API_KEY,
|
||||
retries: 3,
|
||||
}) : undefined;
|
||||
|
||||
if (gcm) {
|
||||
gcm.on('transmissionError', (err, message, registrationId) => {
|
||||
logger.error('GCM Error', err, message, registrationId);
|
||||
});
|
||||
}
|
||||
const fcmSender = FCM_API_KEY ? new gcmLib.Sender(FCM_API_KEY) : undefined;
|
||||
|
||||
let apn;
|
||||
|
||||
@@ -82,15 +75,18 @@ module.exports = function sendNotification (user, details = {}) {
|
||||
_.each(pushDevices, pushDevice => {
|
||||
switch (pushDevice.type) {
|
||||
case 'android':
|
||||
if (gcm) {
|
||||
payload.title = details.title;
|
||||
payload.message = details.message;
|
||||
gcm.send({
|
||||
registrationId: pushDevice.regId,
|
||||
delayWhileIdle: true,
|
||||
timeToLive: details.timeToLive ? details.timeToLive : 15,
|
||||
// Required for fcm to be received in background
|
||||
payload.title = details.title;
|
||||
payload.body = details.message;
|
||||
|
||||
if (fcmSender) {
|
||||
let message = new gcmLib.Message({
|
||||
data: payload,
|
||||
});
|
||||
|
||||
fcmSender.send(message, {
|
||||
registrationTokens: [pushDevice.regId],
|
||||
}, 10, (err) => logger.error('FCM Error', err));
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import shared from '../../../../common';
|
||||
import common from '../../../../common';
|
||||
import Bluebird from 'bluebird';
|
||||
import {
|
||||
chatDefaults,
|
||||
@@ -23,12 +23,12 @@ schema.methods.getGroups = function getUserGroups () {
|
||||
schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, message) {
|
||||
let sender = this;
|
||||
|
||||
shared.refPush(userToReceiveMessage.inbox.messages, chatDefaults(message, sender));
|
||||
common.refPush(userToReceiveMessage.inbox.messages, chatDefaults(message, sender));
|
||||
userToReceiveMessage.inbox.newMessages++;
|
||||
userToReceiveMessage._v++;
|
||||
userToReceiveMessage.markModified('inbox.messages');
|
||||
|
||||
shared.refPush(sender.inbox.messages, defaults({sent: true}, chatDefaults(message, userToReceiveMessage)));
|
||||
common.refPush(sender.inbox.messages, defaults({sent: true}, chatDefaults(message, userToReceiveMessage)));
|
||||
sender.markModified('inbox.messages');
|
||||
|
||||
let promises = [userToReceiveMessage.save(), sender.save()];
|
||||
@@ -40,4 +40,15 @@ schema.methods.addNotification = function addUserNotification (type, data = {})
|
||||
type,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
// Add stats.toNextLevel, stats.maxMP and stats.maxHealth
|
||||
// to a JSONified User object
|
||||
schema.methods.addComputedStatsToJSONObj = function addComputedStatsToUserJSONObj (obj) {
|
||||
// NOTE: if an item is manually added to user.stats then
|
||||
// common/fns/predictableRandom must be tweaked so the new item is not considered.
|
||||
// Otherwise the client will have it while the server won't and the results will be different.
|
||||
obj.stats.toNextLevel = common.tnl(this.stats.lvl);
|
||||
obj.stats.maxHealth = common.maxHealth;
|
||||
obj.stats.maxMP = common.statsComputed(this).maxMP;
|
||||
};
|
||||
@@ -391,7 +391,7 @@ let schema = new Schema({
|
||||
skin: {type: String, default: '915533'},
|
||||
shirt: {type: String, default: 'blue'},
|
||||
timezoneOffset: {type: Number, default: 0},
|
||||
sound: {type: String, default: 'off', enum: ['off', 'danielTheBard', 'gokulTheme', 'luneFoxTheme', 'wattsTheme', 'rosstavoTheme']},
|
||||
sound: {type: String, default: 'off', enum: ['off', 'danielTheBard', 'gokulTheme', 'luneFoxTheme', 'wattsTheme', 'rosstavoTheme', 'dewinTheme']},
|
||||
chair: {type: String, default: 'none'},
|
||||
timezoneOffsetAtLastCron: Number,
|
||||
language: String,
|
||||
|
||||
@@ -223,9 +223,9 @@ nav.toolbar(ng-controller='MenuCtrl')
|
||||
div
|
||||
ul.toolbar-submenu
|
||||
// Using [{k,v}] instead of {k:v,k:v} to maintain order ('off' at top)
|
||||
for theme in ['off', 'danielTheBard', 'gokulTheme', 'luneFoxTheme', 'wattsTheme', 'rosstavoTheme']
|
||||
for theme in ['off', 'danielTheBard', 'gokulTheme', 'luneFoxTheme', 'wattsTheme', 'rosstavoTheme', 'dewinTheme']
|
||||
li
|
||||
a(ng-class="{'bg-primary':user.preferences.sound === '# {theme}'}", ng-click="set({'preferences.sound':'#{theme}'})")=env.t('audioTheme_'+theme)
|
||||
a(ng-class="{'bg-info':user.preferences.sound === '#{theme}'}", ng-click="set({'preferences.sound':'#{theme}'})")=env.t('audioTheme_'+theme)
|
||||
ul.toolbar-controls
|
||||
li.toolbar-controls-button
|
||||
a(data-close-menu)=env.t('close')
|
||||
|
||||
@@ -1,42 +1,59 @@
|
||||
h2 8/1/2016 - TAKE THIS CHALLENGE AND GUILD SPOTLIGHT
|
||||
h3 8/3/2016 - AUGUST BACKGROUNDS AND ARMOIRE ITEMS; NEW AUDIO THEME
|
||||
.promo_backgrounds_armoire_201608.pull-right
|
||||
hr
|
||||
tr
|
||||
td
|
||||
.promo_takethis_armor.pull-right
|
||||
h3 New Take This Challenge
|
||||
p The next Take This Challenge has launched, <a href='/#/options/groups/challenges/ee2b3c87-13f0-422a-af3c-309102d4f7e6'>Cast of Characters</a>, with a focus on picturing your feelings as characters in a story. Be sure to check it out to earn additional pieces of the Take This armor set!
|
||||
br
|
||||
p <a href='http://www.takethis.org/' target='_blank'>Take This</a> is a nonprofit that seeks to inform the gamer community about mental health issues, to provide education about mental disorders and mental illness prevention, and to reduce the stigma of mental illness.
|
||||
br
|
||||
p Congratulations to the winners of the last Take This Challenge, "Check Your HP": grand prize winner bookguinea, and runners-up abbey, SeanXiaaa, pushkin89, fightorflight, and ProfDzul. Plus, all participants in that Challenge have received a piece of the <a href='http://habitica.wikia.com/wiki/Event_Item_Sequences#Take_This_Armor_Set' target='_blank'>Take This item set</a>. Enjoy!
|
||||
p.small.muted by Lemoness, SabreCat, Doctor B, and the Take This team
|
||||
h3 August Backgrounds
|
||||
p We’ve added three new backgrounds: Stormy Ship, Idyllic Cabin, and Mountain Pyramid. Get them now from the <a href='/#/options/profile/backgrounds'>Background Shop</a> and show off your avatars in the Tavern!
|
||||
p.small.muted by JInjooHat, KusSv, and Beffymaroo
|
||||
tr
|
||||
td
|
||||
h3 GUILD SPOTLIGHT
|
||||
p There's a new <a href='https://habitica.wordpress.com/category/guild-spotlight/' target='_blank'>Guild Spotlight on the blog</a>! Check it out now to learn about some awesome Habitica Guilds that highlight fun fandoms.
|
||||
p.small.muted by Lemoness
|
||||
h3 August Armoire Items: Falconer Set
|
||||
p There’s new Gold-purchasable equipment in the Enchanted Armoire, including the Falconer set. The Enchanted Armoire unlocks when you achieve Ultimate Gear. Better work hard on your real-life tasks to earn all the pieces!
|
||||
p.small.muted by Mako413
|
||||
tr
|
||||
td
|
||||
h3 New Audio Theme
|
||||
p There's a new audio theme available: Dewin's Theme! Select this theme from the 🔈 drop-down to level up to sounds with an 8-bit flavor.
|
||||
p.small.muted by Dewin
|
||||
|
||||
if menuItem !== 'oldNews'
|
||||
hr
|
||||
a(href='/static/old-news', target='_blank') Read older news
|
||||
|
||||
mixin oldNews
|
||||
h2 7/31/2016 - HABITICA NAMING DAY AND GRYPHON PETS/MOUNTS!
|
||||
h2 8/1/2016 - TAKE THIS CHALLENGE AND GUILD SPOTLIGHT
|
||||
tr
|
||||
td
|
||||
span.Mount_Body_Gryphon-RoyalPurple.pull-right
|
||||
span.Mount_Head_Gryphon-RoyalPurple.pull-right(style='margin:0')
|
||||
h3 Habitica Naming Day
|
||||
p Happy Habitica Naming day! In honor of the day when we changed the name of the app from HabitRPG to Habitica, we've given everyone an achievement, as well as some delicious cake for your pets and mounts.
|
||||
p.small.muted by Lemoness and SabreCat
|
||||
tr
|
||||
td
|
||||
.Pet-Gryphon-RoyalPurple.pull-right
|
||||
h3 Habitica Gryphon Mount and Pet
|
||||
p Speaking of pets and mounts, we've given all new users our Purple Gryphon Mount, Melior! If you already received Melior last year, we've given you his little sister Meliora, a Purple Gryphon Pet. You can find them in the Rare Pet/Rare Mount sections of the stable!
|
||||
.promo_takethis_armor.pull-right
|
||||
h3 New Take This Challenge
|
||||
p The next Take This Challenge has launched, <a href='/#/options/groups/challenges/ee2b3c87-13f0-422a-af3c-309102d4f7e6'>Cast of Characters</a>, with a focus on picturing your feelings as characters in a story. Be sure to check it out to earn additional pieces of the Take This armor set!
|
||||
br
|
||||
p Thanks for being a Habitica user -- you all mean so much to us. We hope that you enjoy your presents!
|
||||
p.small.muted by Lemoness and Baconsaur
|
||||
p <a href='http://www.takethis.org/' target='_blank'>Take This</a> is a nonprofit that seeks to inform the gamer community about mental health issues, to provide education about mental disorders and mental illness prevention, and to reduce the stigma of mental illness.
|
||||
br
|
||||
p Congratulations to the winners of the last Take This Challenge, "Check Your HP": grand prize winner bookguinea, and runners-up abbey, SeanXiaaa, pushkin89, fightorflight, and ProfDzul. Plus, all participants in that Challenge have received a piece of the <a href='http://habitica.wikia.com/wiki/Event_Item_Sequences#Take_This_Armor_Set' target='_blank'>Take This item set</a>. Enjoy!
|
||||
p.small.muted by Lemoness, SabreCat, Doctor B, and the Take This team
|
||||
tr
|
||||
td
|
||||
h3 Guild Spotlight
|
||||
p There's a new <a href='https://habitica.wordpress.com/category/guild-spotlight/' target='_blank'>Guild Spotlight on the blog</a>! Check it out now to learn about some awesome Habitica Guilds that highlight fun fandoms.
|
||||
p.small.muted by Lemoness
|
||||
h2 7/31/2016 - HABITICA NAMING DAY AND GRYPHON PETS/MOUNTS!
|
||||
tr
|
||||
td
|
||||
span.Mount_Body_Gryphon-RoyalPurple.pull-right
|
||||
span.Mount_Head_Gryphon-RoyalPurple.pull-right(style='margin:0')
|
||||
h3 Habitica Naming Day
|
||||
p Happy Habitica Naming day! In honor of the day when we changed the name of the app from HabitRPG to Habitica, we've given everyone an achievement, as well as some delicious cake for your pets and mounts.
|
||||
p.small.muted by Lemoness and SabreCat
|
||||
tr
|
||||
td
|
||||
.Pet-Gryphon-RoyalPurple.pull-right
|
||||
h3 Habitica Gryphon Mount and Pet
|
||||
p Speaking of pets and mounts, we've given all new users our Purple Gryphon Mount, Melior! If you already received Melior last year, we've given you his little sister Meliora, a Purple Gryphon Pet. You can find them in the Rare Pet/Rare Mount sections of the stable!
|
||||
br
|
||||
p Thanks for being a Habitica user -- you all mean so much to us. We hope that you enjoy your presents!
|
||||
p.small.muted by Lemoness and Baconsaur
|
||||
h2 7/29/2016 - LAST CHANCE FOR SUMMER SPLASH; MOBILE UPDATES
|
||||
tr
|
||||
td
|
||||
|
||||
@@ -37,9 +37,8 @@ block content
|
||||
a.gallery(href='/marketing/guild.png', title=env.t('marketing2Header'))
|
||||
img.img-rendering-auto(src='/marketing/guild.png')
|
||||
p.lead=env.t('marketing2Lead1')
|
||||
|
||||
a.gallery(href='/common/img/sprites/spritesmith/quests/quest_vice3.png', title=env.t('marketing2Lead2Title'))
|
||||
img(src='/common/img/sprites/spritesmith/quests/quest_vice3.png')
|
||||
a.gallery(href='/common/img/sprites/spritesmith/quests/bosses/quest_vice3.png', title=env.t('marketing2Lead2Title'))
|
||||
img(src='/common/img/sprites/spritesmith/quests/bosses/quest_vice3.png')
|
||||
p.lead!=env.t('marketing2Lead2')
|
||||
.col-md-6
|
||||
a.gallery(href='/marketing/challenge.png', title=env.t('challenges'))
|
||||
|
||||