Fixing edit task issue according to blade's comments

This commit is contained in:
Husman 2016-06-22 17:13:58 -07:00
commit e167e41731
99 changed files with 6514 additions and 5665 deletions

View file

@ -1,9 +1,9 @@
/* Comment out for holiday events */
.npc_ian {
/* .npc_ian {
background: url("/common/img/sprites/npc_ian.gif") no-repeat;
width: 78px;
height: 135px;
}
} */
.quest_burnout {
background: url("/common/img/sprites/quest_burnout.gif") no-repeat;

View file

@ -1,42 +1,42 @@
.2014_Fall_HealerPROMO2 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1333px -1252px;
background-position: -855px -1310px;
width: 90px;
height: 90px;
}
.2014_Fall_Mage_PROMO9 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -306px -220px;
background-position: -1201px -775px;
width: 120px;
height: 90px;
}
.2014_Fall_RoguePROMO3 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -308px -1252px;
background-position: -109px -1310px;
width: 105px;
height: 90px;
}
.2014_Fall_Warrior_PROMO {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -787px -1252px;
background-position: -309px -1310px;
width: 90px;
height: 90px;
}
.promo_android {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -176px -1076px;
background-position: -1095px -508px;
width: 175px;
height: 175px;
}
.promo_backgrounds_armoire_201602 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1017px -103px;
background-position: -306px -220px;
width: 141px;
height: 294px;
}
.promo_backgrounds_armoire_201603 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -875px -103px;
background-position: -875px -241px;
width: 141px;
height: 294px;
}
@ -60,91 +60,91 @@
}
.promo_backtoschool {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1305px -584px;
background-position: -1342px -553px;
width: 150px;
height: 150px;
}
.promo_burnout {
background-image: url(spritesmith-largeSprites-0.png);
background-position: 0px -525px;
background-position: -875px 0px;
width: 219px;
height: 240px;
}
.promo_chairs_glasses {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -306px -311px;
background-position: -1017px -241px;
width: 51px;
height: 210px;
}
.promo_classes_fall_2014 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -363px -862px;
background-position: -363px -1005px;
width: 321px;
height: 100px;
}
.promo_classes_fall_2015 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -875px -712px;
background-position: -281px -857px;
width: 377px;
height: 99px;
}
.promo_dilatoryDistress {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -455px -1344px;
background-position: -1492px -1310px;
width: 90px;
height: 90px;
}
.promo_egg_mounts {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -875px -398px;
background-position: 0px -857px;
width: 280px;
height: 147px;
}
.promo_enchanted_armoire {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -685px -862px;
background-position: -271px -772px;
width: 374px;
height: 76px;
}
.promo_enchanted_armoire_201507 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1305px -1034px;
background-position: -1342px -1091px;
width: 217px;
height: 90px;
}
.promo_enchanted_armoire_201508 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1122px -546px;
background-position: -875px -536px;
width: 180px;
height: 90px;
}
.promo_enchanted_armoire_201509 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -696px -1252px;
background-position: 0px -1401px;
width: 90px;
height: 90px;
}
.promo_enchanted_armoire_201511 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1456px -584px;
background-position: -1201px -684px;
width: 122px;
height: 90px;
}
.promo_enchanted_armoire_201601 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -969px -1252px;
background-position: -491px -1310px;
width: 90px;
height: 90px;
}
.promo_floral_potions {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1159px -103px;
background-position: -1095px -684px;
width: 105px;
height: 273px;
}
.promo_habitica {
background-image: url(spritesmith-largeSprites-0.png);
background-position: 0px -1076px;
background-position: -1095px -332px;
width: 175px;
height: 175px;
}
@ -156,307 +156,313 @@
}
.promo_haunted_hair {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1462px -285px;
background-position: -1483px -704px;
width: 100px;
height: 137px;
}
.promo_item_notif {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1305px -182px;
background-position: -1342px 0px;
width: 249px;
height: 102px;
}
.promo_mystery_201405 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1424px -1252px;
background-position: -946px -1310px;
width: 90px;
height: 90px;
}
.promo_mystery_201406 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -358px -417px;
background-position: -1493px -251px;
width: 90px;
height: 96px;
}
.promo_mystery_201407 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1253px -712px;
background-position: -777px -442px;
width: 42px;
height: 62px;
}
.promo_mystery_201408 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1523px -1125px;
background-position: -1271px -414px;
width: 60px;
height: 71px;
}
.promo_mystery_201409 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -878px -1252px;
background-position: -400px -1310px;
width: 90px;
height: 90px;
}
.promo_mystery_201410 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1156px -474px;
background-position: -1017px -452px;
width: 72px;
height: 63px;
}
.promo_mystery_201411 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1151px -1252px;
background-position: -673px -1310px;
width: 90px;
height: 90px;
}
.promo_mystery_201412 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1226px -637px;
background-position: -734px -442px;
width: 42px;
height: 66px;
}
.promo_mystery_201501 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1229px -474px;
background-position: -1541px -1182px;
width: 48px;
height: 63px;
}
.promo_mystery_201502 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -182px -1344px;
background-position: -1219px -1310px;
width: 90px;
height: 90px;
}
.promo_mystery_201503 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -364px -1344px;
background-position: -1401px -1310px;
width: 90px;
height: 90px;
}
.promo_mystery_201504 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1122px -637px;
background-position: -1271px -508px;
width: 60px;
height: 69px;
}
.promo_mystery_201505 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -546px -1344px;
background-position: -91px -1401px;
width: 90px;
height: 90px;
}
.promo_mystery_201506 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1183px -637px;
background-position: -1271px -578px;
width: 42px;
height: 69px;
}
.promo_mystery_201507 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -358px -311px;
background-position: -1499px -103px;
width: 90px;
height: 105px;
}
.promo_mystery_201508 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -602px -1252px;
background-position: -215px -1310px;
width: 93px;
height: 90px;
}
.promo_mystery_201509 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1060px -1252px;
background-position: -582px -1310px;
width: 90px;
height: 90px;
}
.promo_mystery_201510 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -414px -1252px;
background-position: -1493px -553px;
width: 93px;
height: 90px;
}
.promo_mystery_201511 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1242px -1252px;
background-position: -764px -1310px;
width: 90px;
height: 90px;
}
.promo_mystery_201512 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1523px -1034px;
background-position: -1271px -332px;
width: 60px;
height: 81px;
}
.promo_mystery_201601 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1456px -735px;
background-position: -1201px -866px;
width: 120px;
height: 90px;
}
.promo_mystery_201602 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: 0px -1344px;
background-position: -1037px -1310px;
width: 90px;
height: 90px;
}
.promo_mystery_201603 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -91px -1344px;
background-position: -1128px -1310px;
width: 90px;
height: 90px;
}
.promo_mystery_201604 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -508px -1252px;
background-position: -1493px -402px;
width: 93px;
height: 90px;
}
.promo_mystery_201605 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -273px -1344px;
background-position: -1310px -1310px;
width: 90px;
height: 90px;
}
.promo_mystery_3014 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1305px -1125px;
background-position: -1342px -1000px;
width: 217px;
height: 90px;
}
.promo_orca {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1456px -433px;
background-position: -1483px -852px;
width: 105px;
height: 105px;
}
.promo_partyhats {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1456px -675px;
background-position: -875px -627px;
width: 115px;
height: 47px;
}
.promo_pastel_skin {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -657px -965px;
background-position: -685px -1005px;
width: 330px;
height: 83px;
}
.customize-option.promo_pastel_skin {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -682px -980px;
background-position: -710px -1020px;
width: 60px;
height: 60px;
}
.promo_peppermint_flame {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1446px -886px;
background-position: -1342px -852px;
width: 140px;
height: 147px;
}
.promo_pet_skins {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1305px -886px;
background-position: -1342px -704px;
width: 140px;
height: 147px;
}
.customize-option.promo_pet_skins {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1330px -901px;
background-position: -1367px -719px;
width: 60px;
height: 60px;
}
.promo_shimmer_hair {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -326px -965px;
background-position: -326px -1108px;
width: 330px;
height: 83px;
}
.promo_splashyskins {
background-image: url(spritesmith-largeSprites-0.png);
background-position: 0px -1252px;
background-position: -1342px -1182px;
width: 198px;
height: 91px;
}
.customize-option.promo_splashyskins {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -25px -1267px;
background-position: -1367px -1197px;
width: 60px;
height: 60px;
}
.promo_spring_classes_2016 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: 0px -862px;
background-position: 0px -1005px;
width: 362px;
height: 102px;
}
.promo_springclasses2014 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1305px 0px;
background-position: -289px -1219px;
width: 288px;
height: 90px;
}
.promo_springclasses2015 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1305px -91px;
background-position: 0px -1219px;
width: 288px;
height: 90px;
}
.promo_summer_classes_2014 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -875px 0px;
background-position: -401px -525px;
width: 429px;
height: 102px;
}
.promo_summer_classes_2015 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -352px -1076px;
background-position: -770px -676px;
width: 300px;
height: 88px;
}
.promo_summer_classes_2016 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: 0px -525px;
width: 400px;
height: 150px;
}
.promo_takethis_armor {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -738px -525px;
background-position: -578px -1219px;
width: 114px;
height: 87px;
}
.promo_updos {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1305px -285px;
background-position: -1342px -103px;
width: 156px;
height: 147px;
}
.promo_veteran_pets {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1156px -398px;
background-position: -799px -772px;
width: 146px;
height: 75px;
}
.promo_winter_classes_2016 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -499px -766px;
background-position: -659px -857px;
width: 360px;
height: 90px;
}
.promo_winterclasses2015 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: 0px -965px;
background-position: 0px -1108px;
width: 325px;
height: 110px;
}
.promo_winteryhair {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1060px -862px;
background-position: -646px -772px;
width: 152px;
height: 75px;
}
.avatar_variety {
background-image: url(spritesmith-largeSprites-0.png);
background-position: 0px -766px;
background-position: -271px -676px;
width: 498px;
height: 95px;
}
.npc_viirus {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -199px -1252px;
background-position: 0px -1310px;
width: 108px;
height: 90px;
}
@ -468,31 +474,31 @@
}
.scene_coding {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1305px -433px;
background-position: -1342px -402px;
width: 150px;
height: 150px;
}
.scene_phone_peek {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1305px -735px;
background-position: -1342px -251px;
width: 150px;
height: 150px;
}
.welcome_basic_avatars {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -491px -525px;
background-position: -1095px 0px;
width: 246px;
height: 165px;
}
.welcome_promo_party {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -220px -525px;
background-position: 0px -676px;
width: 270px;
height: 180px;
}
.welcome_sample_tasks {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -875px -546px;
background-position: -1095px -166px;
width: 246px;
height: 165px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 344 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 144 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 149 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB

View file

@ -0,0 +1,186 @@
.Pet-Whale-Shade {
background-image: url(spritesmith-main-13.png);
background-position: -82px 0px;
width: 81px;
height: 99px;
}
.Pet-Whale-Skeleton {
background-image: url(spritesmith-main-13.png);
background-position: 0px -300px;
width: 81px;
height: 99px;
}
.Pet-Whale-White {
background-image: url(spritesmith-main-13.png);
background-position: -164px 0px;
width: 81px;
height: 99px;
}
.Pet-Whale-Zombie {
background-image: url(spritesmith-main-13.png);
background-position: 0px -100px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Base {
background-image: url(spritesmith-main-13.png);
background-position: -82px -100px;
width: 81px;
height: 99px;
}
.Pet-Wolf-CottonCandyBlue {
background-image: url(spritesmith-main-13.png);
background-position: -164px -100px;
width: 81px;
height: 99px;
}
.Pet-Wolf-CottonCandyPink {
background-image: url(spritesmith-main-13.png);
background-position: -246px 0px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Desert {
background-image: url(spritesmith-main-13.png);
background-position: -246px -100px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Floral {
background-image: url(spritesmith-main-13.png);
background-position: 0px -200px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Golden {
background-image: url(spritesmith-main-13.png);
background-position: -82px -200px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Peppermint {
background-image: url(spritesmith-main-13.png);
background-position: -164px -200px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Red {
background-image: url(spritesmith-main-13.png);
background-position: -246px -200px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Shade {
background-image: url(spritesmith-main-13.png);
background-position: -328px 0px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Skeleton {
background-image: url(spritesmith-main-13.png);
background-position: -328px -100px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Spooky {
background-image: url(spritesmith-main-13.png);
background-position: -328px -200px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Veteran {
background-image: url(spritesmith-main-13.png);
background-position: 0px 0px;
width: 81px;
height: 99px;
}
.Pet-Wolf-White {
background-image: url(spritesmith-main-13.png);
background-position: -82px -300px;
width: 81px;
height: 99px;
}
.Pet-Wolf-Zombie {
background-image: url(spritesmith-main-13.png);
background-position: -164px -300px;
width: 81px;
height: 99px;
}
.Pet_HatchingPotion_Base {
background-image: url(spritesmith-main-13.png);
background-position: -295px -300px;
width: 48px;
height: 51px;
}
.Pet_HatchingPotion_CottonCandyBlue {
background-image: url(spritesmith-main-13.png);
background-position: -410px -156px;
width: 48px;
height: 51px;
}
.Pet_HatchingPotion_CottonCandyPink {
background-image: url(spritesmith-main-13.png);
background-position: -344px -300px;
width: 48px;
height: 51px;
}
.Pet_HatchingPotion_Desert {
background-image: url(spritesmith-main-13.png);
background-position: -410px 0px;
width: 48px;
height: 51px;
}
.Pet_HatchingPotion_Floral {
background-image: url(spritesmith-main-13.png);
background-position: -410px -52px;
width: 48px;
height: 51px;
}
.Pet_HatchingPotion_Golden {
background-image: url(spritesmith-main-13.png);
background-position: -410px -104px;
width: 48px;
height: 51px;
}
.Pet_HatchingPotion_Peppermint {
background-image: url(spritesmith-main-13.png);
background-position: -246px -300px;
width: 48px;
height: 51px;
}
.Pet_HatchingPotion_Red {
background-image: url(spritesmith-main-13.png);
background-position: -410px -208px;
width: 48px;
height: 51px;
}
.Pet_HatchingPotion_Shade {
background-image: url(spritesmith-main-13.png);
background-position: -410px -260px;
width: 48px;
height: 51px;
}
.Pet_HatchingPotion_Skeleton {
background-image: url(spritesmith-main-13.png);
background-position: -410px -312px;
width: 48px;
height: 51px;
}
.Pet_HatchingPotion_Spooky {
background-image: url(spritesmith-main-13.png);
background-position: 0px -400px;
width: 48px;
height: 51px;
}
.Pet_HatchingPotion_White {
background-image: url(spritesmith-main-13.png);
background-position: -49px -400px;
width: 48px;
height: 51px;
}
.Pet_HatchingPotion_Zombie {
background-image: url(spritesmith-main-13.png);
background-position: -98px -400px;
width: 48px;
height: 51px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 136 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 178 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 376 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 158 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 136 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

View file

@ -173,6 +173,15 @@
"weaponSpecialSpring2016HealerText": "Spring Flower Wand",
"weaponSpecialSpring2016HealerNotes": "With a wave and a wink, you bring the fields and forests into bloom! Or bop troublesome mice on the head. Increases Intelligence by <%= int %>. Limited Edition 2016 Spring Gear.",
"weaponSpecialSummer2016RogueText": "Electric Rod",
"weaponSpecialSummer2016RogueNotes": "Anyone who battles you is in for a shocking surprise... Increases Strength by <%= str %>. Limited Edition 2016 Summer Gear.",
"weaponSpecialSummer2016WarriorText": "Hooked Sword",
"weaponSpecialSummer2016WarriorNotes": "Bite those tough tasks with this hooked sword! Increases Strength by <%= str %>. Limited Edition 2016 Summer Gear.",
"weaponSpecialSummer2016MageText": "Seafoam Staff",
"weaponSpecialSummer2016MageNotes": "All the power of the seas filters through this staff. Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition 2016 Summer Gear.",
"weaponSpecialSummer2016HealerText": "Healing Trident",
"weaponSpecialSummer2016HealerNotes": "One spike harms, the other heals. Increases Intelligence by <%= int %>. Limited Edition 2016 Summer Gear.",
"weaponMystery201411Text": "Pitchfork of Feasting",
"weaponMystery201411Notes": "Stab your enemies or dig in to your favorite foods - this versatile pitchfork does it all! Confers no benefit. November 2014 Subscriber Item.",
"weaponMystery201502Text": "Shimmery Winged Staff of Love and Also Truth",
@ -375,6 +384,15 @@
"armorSpecialSpring2016HealerText": "Fluffy Bunny Breeches",
"armorSpecialSpring2016HealerNotes": "Hippity hop! Bound from hill to hill, healing those in need. Increases Constitution by <%= con %>. Limited Edition 2016 Spring Gear.",
"armorSpecialSummer2016RogueText": "Eel Tail",
"armorSpecialSummer2016RogueNotes": "This electrifying garment transforms its wearer into a real Eel Rogue! Increases Perception by <%= per %>. Limited Edition 2016 Summer Gear.",
"armorSpecialSummer2016WarriorText": "Shark Tail",
"armorSpecialSummer2016WarriorNotes": "This rough garment transforms its wearer into a real Shark Warrior! Increases Constitution by <%= con %>. Limited Edition 2016 Summer Gear.",
"armorSpecialSummer2016MageText": "Dolphin Tail",
"armorSpecialSummer2016MageNotes": "This slippery garment transforms its wearer into a real Dolphin Mage! Increases Intelligence by <%= int %>. Limited Edition 2016 Summer Gear.",
"armorSpecialSummer2016HealerText": "Seahorse Tail",
"armorSpecialSummer2016HealerNotes": "This spiky garment transforms its wearer into a real Seahorse Healer! Increases Constitution by <%= con %>. Limited Edition 2016 Summer Gear.",
"armorMystery201402Text": "Messenger Robes",
"armorMystery201402Notes": "Shimmering and strong, these robes have many pockets to carry letters. Confers no benefit. February 2014 Subscriber Item.",
"armorMystery201403Text": "Forest Walker Armor",
@ -606,6 +624,15 @@
"headSpecialSpring2016HealerText": "Blossom Diadem",
"headSpecialSpring2016HealerNotes": "It glints with the potential of new life ready to burst forth. Increases Intelligence by <%= int %>. Limited Edition 2016 Spring Gear.",
"headSpecialSummer2016RogueText": "Eel Helm",
"headSpecialSummer2016RogueNotes": "Peek out from rocky crevices while wearing this stealthy helm. Increases Perception by <%= per %>. Limited Edition 2016 Summer Gear.",
"headSpecialSummer2016WarriorText": "Shark Helmet",
"headSpecialSummer2016WarriorNotes": "Bite those tough tasks with this fearsome helm! Increases Strength by <%= str %>. Limited Edition 2016 Summer Gear.",
"headSpecialSummer2016MageText": "Blowspout Hat",
"headSpecialSummer2016MageNotes": "Magical water constantly sprays from this hat. Increases Perception by <%= per %>. Limited Edition 2016 Summer Gear.",
"headSpecialSummer2016HealerText": "Seahorse Helm",
"headSpecialSummer2016HealerNotes": "This helm indicates that the wearer was trained by the magical healing seahorses of Dilatory. Increases Intelligence by <%= int %>. Limited Edition 2016 Summer Gear.",
"headSpecialGaymerxText": "Rainbow Warrior Helm",
"headSpecialGaymerxNotes": "In celebration of the GaymerX Conference, this special helmet is decorated with a radiant, colorful rainbow pattern! GaymerX is a game convention celebrating LGTBQ and gaming and is open to everyone.",
@ -805,6 +832,13 @@
"shieldSpecialSpring2016HealerText": "Floral Buckler",
"shieldSpecialSpring2016HealerNotes": "The April Fool claims this little shield will block Shiny Seeds. Don't believe him. Increases Constitution by <%= con %>. Limited Edition 2016 Spring Gear.",
"shieldSpecialSummer2016RogueText": "Electric Rod",
"shieldSpecialSummer2016RogueNotes": "Anyone who battles you is in for a shocking surprise... Increases Strength by <%= str %>. Limited Edition 2016 Summer Gear.",
"shieldSpecialSummer2016WarriorText": "Shark Tooth",
"shieldSpecialSummer2016WarriorNotes": "Bite those tough tasks with this toothy shield! Increases Constitution by <%= con %>. Limited Edition 2016 Summer Gear.",
"shieldSpecialSummer2016HealerText": "Sea Star Shield",
"shieldSpecialSummer2016HealerNotes": "Sometimes mistakenly called a Starfish Shield. Increases Constitution by <%= con %>. Limited Edition 2016 Summer Gear.",
"shieldMystery201601Text": "Resolution Slayer",
"shieldMystery201601Notes": "This blade can be used to parry away all distractions. Confers no benefit. January 2016 Subscriber Item.",
"shieldMystery301405Text": "Clock Shield",

View file

@ -71,6 +71,10 @@
"magicianBunnySet": "Magician's Bunny (Mage)",
"comfortingKittySet": "Comforting Kitty (Healer)",
"sneakySqueakerSet": "Sneaky Squeaker (Rogue)",
"sunfishWarriorSet": "Sunfish Warrior (Warrior)",
"shipSoothsayerSet": "Ship Soothsayer (Mage)",
"strappingSailorSet": "Strapping Sailor (Healer)",
"reefRenegadeSet": "Reef Renegade (Rogue)",
"fallEventAvailability": "Available until October 31",
"winterEventAvailability": "Available until December 31",
"springEventAvailability": "Available until May 31"

View file

@ -108,10 +108,12 @@
"emailNotifications": "Email Notifications",
"wonChallenge": "You won a Challenge!",
"newPM": "Received Private Message",
"newPMInfo": "New Message from <%= name %>: <%= message %>",
"sentGems": "Sent gems!",
"giftedGems": "Gifted Gems",
"giftedGemsInfo": "<%= amount %> Gems - by <%= name %>",
"giftedGemsInfo": "<%= name %> gifted you <%= amount %> Gems",
"giftedSubscription": "Gifted Subscription",
"giftedSubscriptionInfo": "<%= name %> gifted you a <%= months %> Subscription",
"invitedParty": "Invited To Party",
"invitedGuild": "Invited To Guild",
"importantAnnouncements": "Your account is inactive",
@ -126,6 +128,7 @@
"unsubscribedTextOthers": "You won't receive any other email from Habitica.",
"unsubscribeAllEmails": "Check to Unsubscribe from Emails",
"unsubscribeAllEmailsText": "By checking this box, I certify that I understand that by unsubscribing from all emails, Habitica will never be able to notify me via email about important changes to the site or my account.",
"unsubscribeAllPush": "Check to Unsubscribe from all Push Notifications",
"correctlyUnsubscribedEmailType": "Correctly unsubscribed from \"<%= emailType %>\" emails.",
"subscriptionRateText": "Recurring $<%= price %> USD every <%= months %> months",
"recurringText": "recurring",
@ -138,8 +141,8 @@
"promoCode": "Promo Code",
"promoCodeApplied": "Promo Code Applied! Check your inventory",
"promoPlaceholder": "Enter Promotion Code",
"couponText": "We sometimes have events and give out coupon codes for special gear. (eg, those who stop by our Wondercon booth)",
"displayInviteToPartyWhenPartyIs1": "Display Invite To Party button when party has 1 member.",
"couponText": "We sometimes have events and give out coupon codes for special gear. (eg, those who stop by our Wondercon booth)",
"saveCustomDayStart": "Save Custom Day Start",
"registration": "Registration",
"addLocalAuth": "Add local authentication:",
@ -152,8 +155,11 @@
"invalidUrl": "invalid url",
"invalidEnabled": "the \"enabled\" parameter should be a boolean",
"regIdRequired": "RegId is required",
"invalidPushClient": "Invalid client. Only Official Habitica clients can receive push notifications.",
"pushDeviceAdded": "Push device added successfully",
"pushDeviceAlreadyAdded": "The user already has the push device",
"pushDeviceNotFound": "The user has no push device with this id.",
"pushDeviceRemoved": "Push device removed successfully.",
"add": "Add",
"buyGemsGoldCap": "Cap raised to <%= amount %>",
"mysticHourglass": "<%= amount %> Mystic Hourglass",
@ -168,5 +174,6 @@
"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 resetting at the wrong time, repeat this check on all other PCs and on a browser on your mobile devices."
"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 resetting at the wrong time, repeat this check on all other PCs and on a browser on your mobile devices.",
"push": "Push"
}

View file

@ -24,6 +24,7 @@ export const EVENTS = {
gaymerx: { start: '2015-12-01', end: '2015-12-14' },
winter2016: { start: '2015-12-18', end: '2016-02-02' },
spring2016: { start: '2016-03-18', end: '2016-05-02' },
summer2016: { start: '2016-06-21', end: '2016-08-02' },
};
export const GEAR_TYPES = [

View file

@ -94,6 +94,9 @@ let armor = {
notes: t('armorSpecialSummerRogueNotes', { per: 15 }),
value: 90,
per: 15,
canBuy: () => {
return true;
},
},
summerWarrior: {
event: EVENTS.summer,
@ -102,6 +105,9 @@ let armor = {
notes: t('armorSpecialSummerWarriorNotes', { con: 9 }),
value: 90,
con: 9,
canBuy: () => {
return true;
},
},
summerMage: {
event: EVENTS.summer,
@ -110,6 +116,9 @@ let armor = {
notes: t('armorSpecialSummerMageNotes', { int: 9 }),
value: 90,
int: 9,
canBuy: () => {
return true;
},
},
summerHealer: {
event: EVENTS.summer,
@ -118,6 +127,9 @@ let armor = {
notes: t('armorSpecialSummerHealerNotes', { con: 15 }),
value: 90,
con: 15,
canBuy: () => {
return true;
},
},
fallRogue: {
event: EVENTS.fall,
@ -228,6 +240,9 @@ let armor = {
notes: t('armorSpecialSummer2015RogueNotes', { per: 15 }),
value: 90,
per: 15,
canBuy: () => {
return true;
},
},
summer2015Warrior: {
event: EVENTS.summer2015,
@ -236,6 +251,9 @@ let armor = {
notes: t('armorSpecialSummer2015WarriorNotes', { con: 9 }),
value: 90,
con: 9,
canBuy: () => {
return true;
},
},
summer2015Mage: {
event: EVENTS.summer2015,
@ -244,6 +262,9 @@ let armor = {
notes: t('armorSpecialSummer2015MageNotes', { int: 9 }),
value: 90,
int: 9,
canBuy: () => {
return true;
},
},
summer2015Healer: {
event: EVENTS.summer2015,
@ -252,6 +273,9 @@ let armor = {
notes: t('armorSpecialSummer2015HealerNotes', { con: 15 }),
value: 90,
con: 15,
canBuy: () => {
return true;
},
},
fall2015Rogue: {
event: EVENTS.fall2015,
@ -361,6 +385,38 @@ let armor = {
value: 90,
con: 15,
},
summer2016Rogue: {
event: EVENTS.summer2016,
specialClass: 'rogue',
text: t('armorSpecialSummer2016RogueText'),
notes: t('armorSpecialSummer2016RogueNotes', { per: 15 }),
value: 90,
per: 15,
},
summer2016Warrior: {
event: EVENTS.summer2016,
specialClass: 'warrior',
text: t('armorSpecialSummer2016WarriorText'),
notes: t('armorSpecialSummer2016WarriorNotes', { con: 9 }),
value: 90,
con: 9,
},
summer2016Mage: {
event: EVENTS.summer2016,
specialClass: 'wizard',
text: t('armorSpecialSummer2016MageText'),
notes: t('armorSpecialSummer2016MageNotes', { int: 9 }),
value: 90,
int: 9,
},
summer2016Healer: {
event: EVENTS.summer2016,
specialClass: 'healer',
text: t('armorSpecialSummer2016HealerText'),
notes: t('armorSpecialSummer2016HealerNotes', { con: 15 }),
value: 90,
con: 15,
},
};
let back = {
@ -378,6 +434,9 @@ let body = {
text: t('bodySpecialSummerHealerText'),
notes: t('bodySpecialSummerHealerNotes'),
value: 20,
canBuy: () => {
return true;
},
},
summerMage: {
event: EVENTS.summer,
@ -385,6 +444,9 @@ let body = {
text: t('bodySpecialSummerMageText'),
notes: t('bodySpecialSummerMageNotes'),
value: 20,
canBuy: () => {
return true;
},
},
summer2015Healer: {
event: EVENTS.summer2015,
@ -392,6 +454,9 @@ let body = {
text: t('bodySpecialSummer2015HealerText'),
notes: t('bodySpecialSummer2015HealerNotes'),
value: 20,
canBuy: () => {
return true;
},
},
summer2015Mage: {
event: EVENTS.summer2015,
@ -399,6 +464,9 @@ let body = {
text: t('bodySpecialSummer2015MageText'),
notes: t('bodySpecialSummer2015MageNotes'),
value: 20,
canBuy: () => {
return true;
},
},
summer2015Rogue: {
event: EVENTS.summer2015,
@ -406,6 +474,9 @@ let body = {
text: t('bodySpecialSummer2015RogueText'),
notes: t('bodySpecialSummer2015RogueNotes'),
value: 20,
canBuy: () => {
return true;
},
},
summer2015Warrior: {
event: EVENTS.summer2015,
@ -413,6 +484,9 @@ let body = {
text: t('bodySpecialSummer2015WarriorText'),
notes: t('bodySpecialSummer2015WarriorNotes'),
value: 20,
canBuy: () => {
return true;
},
},
};
@ -425,6 +499,9 @@ let eyewear = {
text: t('eyewearSpecialSummerRogueText'),
notes: t('eyewearSpecialSummerRogueNotes'),
value: 20,
canBuy: () => {
return true;
},
},
summerWarrior: {
event: EVENTS.summer,
@ -432,6 +509,9 @@ let eyewear = {
text: t('eyewearSpecialSummerWarriorText'),
notes: t('eyewearSpecialSummerWarriorNotes'),
value: 20,
canBuy: () => {
return true;
},
},
blackTopFrame: {
gearSet: 'glasses',
@ -565,6 +645,9 @@ let head = {
notes: t('headSpecialSummerRogueNotes', { per: 9 }),
value: 60,
per: 9,
canBuy: () => {
return true;
},
},
summerWarrior: {
event: EVENTS.summer,
@ -573,6 +656,9 @@ let head = {
notes: t('headSpecialSummerWarriorNotes', { str: 9 }),
value: 60,
str: 9,
canBuy: () => {
return true;
},
},
summerMage: {
event: EVENTS.summer,
@ -581,6 +667,9 @@ let head = {
notes: t('headSpecialSummerMageNotes', { per: 7 }),
value: 60,
per: 7,
canBuy: () => {
return true;
},
},
summerHealer: {
event: EVENTS.summer,
@ -589,6 +678,9 @@ let head = {
notes: t('headSpecialSummerHealerNotes', { int: 7 }),
value: 60,
int: 7,
canBuy: () => {
return true;
},
},
fallRogue: {
event: EVENTS.fall,
@ -699,6 +791,9 @@ let head = {
notes: t('headSpecialSummer2015RogueNotes', { per: 9 }),
value: 60,
per: 9,
canBuy: () => {
return true;
},
},
summer2015Warrior: {
event: EVENTS.summer2015,
@ -707,6 +802,9 @@ let head = {
notes: t('headSpecialSummer2015WarriorNotes', { str: 9 }),
value: 60,
str: 9,
canBuy: () => {
return true;
},
},
summer2015Mage: {
event: EVENTS.summer2015,
@ -715,6 +813,9 @@ let head = {
notes: t('headSpecialSummer2015MageNotes', { per: 7 }),
value: 60,
per: 7,
canBuy: () => {
return true;
},
},
summer2015Healer: {
event: EVENTS.summer2015,
@ -723,6 +824,9 @@ let head = {
notes: t('headSpecialSummer2015HealerNotes', { int: 7 }),
value: 60,
int: 7,
canBuy: () => {
return true;
},
},
fall2015Rogue: {
event: EVENTS.fall2015,
@ -832,6 +936,38 @@ let head = {
value: 60,
int: 7,
},
summer2016Rogue: {
event: EVENTS.summer2016,
specialClass: 'rogue',
text: t('headSpecialSummer2016RogueText'),
notes: t('headSpecialSummer2016RogueNotes', { per: 9 }),
value: 60,
per: 9,
},
summer2016Warrior: {
event: EVENTS.summer2016,
specialClass: 'warrior',
text: t('headSpecialSummer2016WarriorText'),
notes: t('headSpecialSummer2016WarriorNotes', { str: 9 }),
value: 60,
str: 9,
},
summer2016Mage: {
event: EVENTS.summer2016,
specialClass: 'wizard',
text: t('headSpecialSummer2016MageText'),
notes: t('headSpecialSummer2016MageNotes', { per: 7 }),
value: 60,
per: 7,
},
summer2016Healer: {
event: EVENTS.summer2016,
specialClass: 'healer',
text: t('headSpecialSummer2016HealerText'),
notes: t('headSpecialSummer2016HealerNotes', { int: 7 }),
value: 60,
int: 7,
},
};
let headAccessory = {
@ -1074,6 +1210,9 @@ let shield = {
notes: t('shieldSpecialSummerRogueNotes', { str: 8 }),
value: 80,
str: 8,
canBuy: () => {
return true;
},
},
summerWarrior: {
event: EVENTS.summer,
@ -1082,6 +1221,9 @@ let shield = {
notes: t('shieldSpecialSummerWarriorNotes', { con: 7 }),
value: 70,
con: 7,
canBuy: () => {
return true;
},
},
summerHealer: {
event: EVENTS.summer,
@ -1090,6 +1232,9 @@ let shield = {
notes: t('shieldSpecialSummerHealerNotes', { con: 9 }),
value: 70,
con: 9,
canBuy: () => {
return true;
},
},
fallRogue: {
event: EVENTS.fall,
@ -1170,6 +1315,9 @@ let shield = {
notes: t('shieldSpecialSummer2015RogueNotes', { str: 8 }),
value: 80,
str: 8,
canBuy: () => {
return true;
},
},
summer2015Warrior: {
event: EVENTS.summer2015,
@ -1178,6 +1326,9 @@ let shield = {
notes: t('shieldSpecialSummer2015WarriorNotes', { con: 7 }),
value: 70,
con: 7,
canBuy: () => {
return true;
},
},
summer2015Healer: {
event: EVENTS.summer2015,
@ -1186,6 +1337,9 @@ let shield = {
notes: t('shieldSpecialSummer2015HealerNotes', { con: 9 }),
value: 70,
con: 9,
canBuy: () => {
return true;
},
},
fall2015Rogue: {
event: EVENTS.fall2015,
@ -1259,6 +1413,30 @@ let shield = {
value: 70,
con: 9,
},
summer2016Rogue: {
event: EVENTS.summer2016,
specialClass: 'rogue',
text: t('shieldSpecialSummer2016RogueText'),
notes: t('shieldSpecialSummer2016RogueNotes', { str: 8 }),
value: 80,
str: 8,
},
summer2016Warrior: {
event: EVENTS.summer2016,
specialClass: 'warrior',
text: t('shieldSpecialSummer2016WarriorText'),
notes: t('shieldSpecialSummer2016WarriorNotes', { con: 7 }),
value: 70,
con: 7,
},
summer2016Healer: {
event: EVENTS.summer2016,
specialClass: 'healer',
text: t('shieldSpecialSummer2016HealerText'),
notes: t('shieldSpecialSummer2016HealerNotes', { con: 9 }),
value: 70,
con: 9,
},
};
let weapon = {
@ -1349,6 +1527,9 @@ let weapon = {
notes: t('weaponSpecialSummerRogueNotes', { str: 8 }),
value: 80,
str: 8,
canBuy: () => {
return true;
},
},
summerWarrior: {
event: EVENTS.summer,
@ -1357,6 +1538,9 @@ let weapon = {
notes: t('weaponSpecialSummerWarriorNotes', { str: 15 }),
value: 90,
str: 15,
canBuy: () => {
return true;
},
},
summerMage: {
event: EVENTS.summer,
@ -1367,6 +1551,9 @@ let weapon = {
value: 160,
int: 15,
per: 7,
canBuy: () => {
return true;
},
},
summerHealer: {
event: EVENTS.summer,
@ -1375,6 +1562,9 @@ let weapon = {
notes: t('weaponSpecialSummerHealerNotes', { int: 9 }),
value: 90,
int: 9,
canBuy: () => {
return true;
},
},
fallRogue: {
event: EVENTS.fall,
@ -1485,6 +1675,9 @@ let weapon = {
notes: t('weaponSpecialSummer2015RogueNotes', { str: 8 }),
value: 80,
str: 8,
canBuy: () => {
return true;
},
},
summer2015Warrior: {
event: EVENTS.summer2015,
@ -1493,6 +1686,9 @@ let weapon = {
notes: t('weaponSpecialSummer2015WarriorNotes', { str: 15 }),
value: 90,
str: 15,
canBuy: () => {
return true;
},
},
summer2015Mage: {
event: EVENTS.summer2015,
@ -1503,6 +1699,9 @@ let weapon = {
value: 160,
int: 15,
per: 7,
canBuy: () => {
return true;
},
},
summer2015Healer: {
event: EVENTS.summer2015,
@ -1511,6 +1710,9 @@ let weapon = {
notes: t('weaponSpecialSummer2015HealerNotes', { int: 9 }),
value: 90,
int: 9,
canBuy: () => {
return true;
},
},
fall2015Rogue: {
event: EVENTS.fall2015,
@ -1614,6 +1816,40 @@ let weapon = {
value: 90,
int: 9,
},
summer2016Rogue: {
event: EVENTS.summer2016,
specialClass: 'rogue',
text: t('weaponSpecialSummer2016RogueText'),
notes: t('weaponSpecialSummer2016RogueNotes', { str: 8 }),
value: 80,
str: 8,
},
summer2016Warrior: {
event: EVENTS.summer2016,
specialClass: 'warrior',
text: t('weaponSpecialSummer2016WarriorText'),
notes: t('weaponSpecialSummer2016WarriorNotes', { str: 15 }),
value: 90,
str: 15,
},
summer2016Mage: {
event: EVENTS.summer2016,
specialClass: 'wizard',
twoHanded: true,
text: t('weaponSpecialSummer2016MageText'),
notes: t('weaponSpecialSummer2016MageNotes', { int: 15, per: 7 }),
value: 160,
int: 15,
per: 7,
},
summer2016Healer: {
event: EVENTS.summer2016,
specialClass: 'healer',
text: t('weaponSpecialSummer2016HealerText'),
notes: t('weaponSpecialSummer2016HealerNotes', { int: 9 }),
value: 90,
int: 9,
},
};
let specialSet = {

View file

@ -151,7 +151,6 @@ import blockUser from './ops/blockUser';
import clearPMs from './ops/clearPMs';
import deletePM from './ops/deletePM';
import reroll from './ops/reroll';
import addPushDevice from './ops/addPushDevice';
import reset from './ops/reset';
import markPmsRead from './ops/markPMSRead';
@ -191,7 +190,6 @@ api.ops = {
clearPMs,
deletePM,
reroll,
addPushDevice,
reset,
markPmsRead,
};
@ -272,7 +270,6 @@ api.wrap = function wrapUser (user, main = true) {
addWebhook: _.partial(importedOps.addWebhook, user),
updateWebhook: _.partial(importedOps.updateWebhook, user),
deleteWebhook: _.partial(importedOps.deleteWebhook, user),
addPushDevice: _.partial(importedOps.addPushDevice, user),
clearPMs: _.partial(importedOps.clearPMs, user),
deletePM: _.partial(importedOps.deletePM, user),
blockUser: _.partial(importedOps.blockUser, user),

View file

@ -1,41 +0,0 @@
import _ from 'lodash';
import i18n from '../i18n';
import {
BadRequest,
NotAuthorized,
} from '../libs/errors';
// TODO move to server code
module.exports = function addPushDevice (user, req = {}) {
let regId = _.get(req, 'body.regId');
if (!regId) throw new BadRequest(i18n.t('regIdRequired', req.language));
let type = _.get(req, 'body.type');
if (!type) throw new BadRequest(i18n.t('typeRequired', req.language));
if (!user.pushDevices) {
user.pushDevices = [];
}
let pushDevices = user.pushDevices;
let item = {
regId,
type,
};
let indexOfPushDevice = _.findIndex(pushDevices, {
regId: item.regId,
});
if (indexOfPushDevice !== -1) {
throw new NotAuthorized(i18n.t('pushDeviceAlreadyAdded', req.language));
}
pushDevices.push(item);
return [
user.pushDevices,
i18n.t('pushDeviceAdded', req.language),
];
};

View file

@ -18,7 +18,6 @@ import deleteTag from './deleteTag';
import addWebhook from './addWebhook';
import updateWebhook from './updateWebhook';
import deleteWebhook from './deleteWebhook';
import addPushDevice from './addPushDevice';
import clearPMs from './clearPMs';
import deletePM from './deletePM';
import blockUser from './blockUser';
@ -68,7 +67,6 @@ module.exports = {
addWebhook,
updateWebhook,
deleteWebhook,
addPushDevice,
clearPMs,
deletePM,
blockUser,

View file

@ -65,10 +65,7 @@
"LOGGLY_ACCOUNT": "account",
"PUSH_CONFIGS": {
"GCM_SERVER_API_KEY": "",
"APN_PEM_FILES": {
"KEY": "key.pem",
"CERT": "cert.pem"
}
"APN_ENABLED": "true"
},
"FIREBASE": {
"APP": "app-name",

View file

@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "3.14.0",
"version": "3.15.0",
"main": "./website/server/index.js",
"dependencies": {
"accepts": "^1.3.2",
@ -78,7 +78,7 @@
"paypal-rest-sdk": "^1.2.1",
"pretty-data": "^0.40.0",
"ps-tree": "^1.0.0",
"push-notify": "^1.1.1",
"push-notify": "habitrpg/push-notify#v1.2.0",
"request": "~2.72.0",
"rimraf": "^2.4.3",
"run-sequence": "^1.1.4",

View file

@ -0,0 +1,32 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('DELETE /user/push-devices', () => {
let user;
let regId = '10';
let type = 'ios';
beforeEach(async () => {
user = await generateUser();
});
it('returns an error if user does not have the push device', async () => {
await expect(user.del(`/user/push-devices/${regId}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('pushDeviceNotFound'),
});
});
it('removes a push device from the user', async () => {
await user.post('/user/push-devices', {type, regId});
let response = await user.del(`/user/push-devices/${regId}`);
await user.sync();
expect(response.message).to.equal(t('pushDeviceRemoved'));
expect(user.pushDevices.length).to.equal(0);
});
});

View file

@ -1,35 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('POST /user/addPushDevice', () => {
let user;
let regId = '10';
let type = 'someRandomType';
beforeEach(async () => {
user = await generateUser();
});
it('returns an error if user already has the push device', async () => {
await user.post('/user/addPushDevice', {type, regId});
await expect(user.post('/user/addPushDevice', {type, regId}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('pushDeviceAlreadyAdded'),
});
});
// More tests in common code unit tests
it('adds a push device to the user', async () => {
let response = await user.post('/user/addPushDevice', {type, regId});
await user.sync();
expect(response.message).to.equal(t('pushDeviceAdded'));
expect(user.pushDevices[0].type).to.equal(type);
expect(user.pushDevices[0].regId).to.equal(regId);
});
});

View file

@ -0,0 +1,60 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('POST /user/push-devices', () => {
let user;
let regId = '10';
let type = 'ios';
beforeEach(async () => {
user = await generateUser();
});
it('returns an error when regId is not provided', async () => {
await expect(user.post('/user/push-devices'), {type})
.to.eventually.be.rejected.and.to.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('returns an error when type is not provided', async () => {
await expect(user.post('/user/push-devices', {regId}))
.to.eventually.be.rejected.and.to.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('returns an error when type is not valid', async () => {
await expect(user.post('/user/push-devices', {regId, type: 'invalid'}))
.to.eventually.be.rejected.and.to.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('returns an error if user already has the push device', async () => {
await user.post('/user/push-devices', {type, regId});
await expect(user.post('/user/push-devices', {type, regId}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('pushDeviceAlreadyAdded'),
});
});
it('adds a push device to the user', async () => {
let response = await user.post('/user/push-devices', {type, regId});
await user.sync();
expect(response.message).to.equal(t('pushDeviceAdded'));
expect(user.pushDevices[0].type).to.equal(type);
expect(user.pushDevices[0].regId).to.equal(regId);
});
});

View file

@ -0,0 +1,185 @@
import { model as User } from '../../../../../website/server/models/user';
import requireAgain from 'require-again';
import pushNotify from 'push-notify';
import nconf from 'nconf';
describe('pushNotifications', () => {
let user;
let sendPushNotification;
let pathToPushNotifications = '../../../../../website/server/libs/api-v3/pushNotifications';
let gcmSendSpy;
let apnSendSpy;
let identifier = 'identifier';
let title = 'title';
let message = 'message';
beforeEach(() => {
user = new User();
gcmSendSpy = sinon.spy();
apnSendSpy = sinon.spy();
sandbox.stub(nconf, 'get').returns('true');
sandbox.stub(pushNotify, 'gcm').returns({
on: () => null,
send: gcmSendSpy,
});
sandbox.stub(pushNotify, 'apn').returns({
on: () => null,
send: apnSendSpy,
});
sendPushNotification = requireAgain(pathToPushNotifications);
});
afterEach(() => {
sandbox.restore();
});
it('throws if user is not supplied', () => {
expect(sendPushNotification).to.throw;
expect(gcmSendSpy).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(apnSendSpy).to.not.have.been.called;
});
it('throws if details.identifier is not supplied', () => {
expect(() => sendPushNotification(user, {
title,
message,
})).to.throw;
expect(gcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.title is not supplied', () => {
expect(() => sendPushNotification(user, {
identifier,
message,
})).to.throw;
expect(gcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.message is not supplied', () => {
expect(() => sendPushNotification(user, {
identifier,
title,
})).to.throw;
expect(gcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('returns if no device is registered', () => {
sendPushNotification(user, {
identifier,
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(apnSendSpy).to.not.have.been.called;
});
// TODO disabled because APN relies on a Promise
xit('uses APN for iOS devices', () => {
user.pushDevices.push({
type: 'ios',
regId: '123',
});
let details = {
identifier,
title,
message,
category: 'fun',
payload: {
a: true,
b: true,
},
};
sendPushNotification(user, details);
expect(apnSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.have.been.calledWithMatch({
token: '123',
alert: message,
sound: 'default',
category: 'fun',
payload: {
identifier,
a: true,
b: true,
},
});
expect(gcmSendSpy).to.not.have.been.called;
});
});

View file

@ -1,59 +0,0 @@
import addPushDevice from '../../../common/script/ops/addPushDevice';
import i18n from '../../../common/script/i18n';
import {
generateUser,
} from '../../helpers/common.helper';
import {
NotAuthorized,
BadRequest,
} from '../../../common/script/libs/errors';
describe('shared.ops.addPushDevice', () => {
let user;
let regId = '10';
let type = 'someRandomType';
beforeEach(() => {
user = generateUser();
user.stats.hp = 0;
});
it('returns an error when regId is not provided', (done) => {
try {
addPushDevice(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('regIdRequired'));
done();
}
});
it('returns an error when type is not provided', (done) => {
try {
addPushDevice(user, {body: {regId}});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('typeRequired'));
done();
}
});
it('adds a push device', () => {
let [, message] = addPushDevice(user, {body: {regId, type}});
expect(message).to.equal(i18n.t('pushDeviceAdded'));
expect(user.pushDevices[0].type).to.equal(type);
expect(user.pushDevices[0].regId).to.equal(regId);
});
it('does not a push device twice', (done) => {
try {
addPushDevice(user, {body: {regId, type}});
addPushDevice(user, {body: {regId, type}});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('pushDeviceAlreadyAdded'));
done();
}
});
});

View file

@ -1,58 +1,6 @@
// TODO move to /api-v2
var api = module.exports;
var _ = require('lodash');
var nconf = require('nconf');
var pushNotify = require('push-notify');
var gcmApiKey = nconf.get("PUSH_CONFIGS:GCM_SERVER_API_KEY");
var gcm = gcmApiKey ? pushNotify.gcm({
apiKey: gcmApiKey,
retries: 3
}) : undefined;
if(gcm){
gcm.on('transmitted', function (result, message, registrationId) {
//console.info("transmitted", result, message, registrationId);
});
gcm.on('transmissionError', function (error, message, registrationId) {
//console.info("transmissionError", error, message, registrationId);
});
gcm.on('updated', function (result, registrationId) {
//console.info("updated", result, registrationId);
});
}
api.sendNotify = function(user, title, msg, timeToLive){
timeToLive = timeToLive || 15;
// need investigation:
// https://github.com/HabitRPG/habitrpg/issues/5252
if(!user)
return;
_.forEach(user.pushDevices, function(pushDevice){
switch(pushDevice.type){
case "android":
if(gcm){
gcm.send({
registrationId: pushDevice.regId,
//collapseKey: 'COLLAPSE_KEY',
delayWhileIdle: true,
timeToLive: timeToLive,
data: {
title: title,
message: msg
}
});
}
break;
case "ios":
break;
}
});
// Disabled, see libs/api-v3/pushNotifications
};

View file

@ -410,7 +410,7 @@ api.resetPassword = {
let newPassword = passwordUtils.makeSalt(); // use a salt as the new password too (they'll change it later)
let hashedPassword = passwordUtils.encrypt(newPassword, salt);
let user = await User.findOne({ 'auth.local.email': email }, { 'auth.local': 1 });
let user = await User.findOne({ 'auth.local.email': email });
if (user) {
user.auth.local.salt = salt;
@ -428,8 +428,10 @@ api.resetPassword = {
baseUrl: nconf.get('BASE_URL'),
}),
});
await user.save();
}
res.respond(200, {}, res.t('passwordReset'));
},
};

View file

@ -20,7 +20,6 @@ import { removeFromArray } from '../../libs/api-v3/collectionManipulators';
import * as firebase from '../../libs/api-v3/firebase';
import { sendTxn as sendTxnEmail } from '../../libs/api-v3/email';
import { encrypt } from '../../libs/api-v3/encryption';
import common from '../../../../common';
import sendPushNotification from '../../libs/api-v3/pushNotifications';
let api = {};
@ -537,11 +536,18 @@ async function _inviteByUUID (uuid, group, inviter, req, res) {
sendTxnEmail(userToInvite, `invited-${groupTemplate}`, emailVars);
}
sendPushNotification(
userToInvite,
common.i18n.t(group.type === 'guild' ? 'invitedGuild' : 'invitedParty'),
group.name
);
if (userToInvite.preferences.pushNotifications[`invited${groupLabel}`] !== false) {
let identifier = group.type === 'guild' ? 'invitedGuild' : 'invitedParty';
sendPushNotification(
userToInvite,
{
title: group.name,
message: res.t(identifier),
identifier,
payload: {groupID: group._id},
}
);
}
let userInvited = await userToInvite.save();
if (group.type === 'guild') {

View file

@ -302,6 +302,18 @@ api.sendPrivateMessage = {
{name: 'PMS_INBOX_URL', content: '/#/options/groups/inbox'},
]);
}
if (receiver.preferences.pushNotifications.newPM !== false) {
sendPushNotification(
receiver,
{
title: res.t('newPM'),
message: res.t('newPMInfo', {name: getUserInfo(sender, ['name']).name, message}),
identifier: 'newPM',
category: 'newPM',
payload: {replyTo: sender._id},
}
);
}
res.respond(200, {});
},
@ -372,8 +384,15 @@ api.transferGems = {
{name: 'X_GEMS_GIFTED', content: gemAmount},
]);
}
sendPushNotification(sender, res.t('giftedGems'), res.t('giftedGemsInfo', { amount: gemAmount, name: byUsername }));
if (receiver.preferences.pushNotifications.giftedGems !== false) {
sendPushNotification(receiver,
{
title: res.t('giftedGems'),
message: res.t('giftedGemsInfo', {amount: gemAmount, name: byUsername}),
identifier: 'giftedGems',
payload: {replyTo: sender._id},
});
}
res.respond(200, {});
},

View file

@ -0,0 +1,95 @@
import { authWithHeaders } from '../../middlewares/api-v3/auth';
import {
NotAuthorized,
NotFound,
} from '../../libs/api-v3/errors';
let api = {};
/**
* @apiIgnore
* @api {post} /api/v3/user/push-devices Add a push device to a user
* @apiVersion 3.0.0
* @apiName UserAddPushDevice
* @apiGroup User
*
* @apiParam {string} regId The id of the push device
* @apiParam {string} type The type of push device
*
* @apiSuccess {Object} data List of push devices
* @apiSuccess {string} message Success message
*/
api.addPushDevice = {
method: 'POST',
url: '/user/push-devices',
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
req.checkBody('regId', res.t('regIdRequired')).notEmpty();
req.checkBody('type', res.t('typeRequired')).notEmpty().isIn(['ios', 'android']);
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let pushDevices = user.pushDevices;
let item = {
regId: req.body.regId,
type: req.body.type,
};
if (pushDevices.find(device => device.regId === item.regId)) {
throw new NotAuthorized(res.t('pushDeviceAlreadyAdded'));
}
pushDevices.push(item);
await user.save();
res.respond(200, user.pushDevices, res.t('pushDeviceAdded'));
},
};
/**
* @apiIgnore
* @api {delete} /api/v3/user/push-devices remove a push device from a user
* @apiVersion 3.0.0
* @apiName UserRemovePushDevice
* @apiGroup User
*
* @apiParam {string} regId The id of the push device
*
* @apiSuccess {Object} data List of push devices
* @apiSuccess {string} message Success message
*/
api.removePushDevice = {
method: 'DELETE',
url: '/user/push-devices/:regId',
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
req.checkParams('regId', res.t('regIdRequired')).notEmpty();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let regId = req.params.regId;
let pushDevices = user.pushDevices;
let indexOfPushDevice = pushDevices.findIndex((element) => {
return element.regId === regId;
});
if (indexOfPushDevice === -1) {
throw new NotFound(res.t('pushDeviceNotFound'));
}
pushDevices.splice(indexOfPushDevice, 1);
await user.save();
res.respond(200, user.pushDevices, res.t('pushDeviceRemoved'));
},
};
module.exports = api;

View file

@ -106,11 +106,18 @@ api.inviteToQuest = {
let inviterVars = getUserInfo(user, ['name', 'email']);
let membersToEmail = members.filter(member => {
// send push notifications while filtering members before sending emails
sendPushNotification(
member,
common.i18n.t('questInvitationTitle'),
common.i18n.t('questInvitationInfo', { quest: quest.text() })
);
if (member.preferences.pushNotifications.invitedQuest !== false) {
sendPushNotification(
member,
{
title: res.t('questInvitationTitle'),
message: res.t('questInvitationInfo', {quest: quest.text(req.language)}),
identifier: 'questInvitation',
category: 'questInvitation',
}
);
}
return member.preferences.emailNotifications.invitedQuest !== false;
});

View file

@ -1266,32 +1266,6 @@ api.userReroll = {
},
};
/**
* @api {post} /api/v3/user/addPushDevice Add a push device to a user
* @apiVersion 3.0.0
* @apiName UserAddPushDevice
* @apiGroup User
*
* @apiParam {string} regId The id of the push device
* @apiParam {string} uuid The type of push device
*
* @apiSuccess {Object} data List of push devices
* @apiSuccess {string} message Success message
*/
api.userAddPushDevice = {
method: 'POST',
middlewares: [authWithHeaders()],
url: '/user/addPushDevice',
async handler (req, res) {
let user = res.locals.user;
let addPushDeviceRes = common.ops.addPushDevice(user, req);
await user.save();
res.respond(200, ...addPushDeviceRes);
},
};
/**
* @api {post} /api/v3/user/reset Reset user
* @apiVersion 3.0.0

View file

@ -9,16 +9,14 @@ import csvStringify from '../../libs/api-v3/csvStringify';
import moment from 'moment';
import js2xml from 'js2xmlparser';
import Pageres from 'pageres';
import AWS from 'aws-sdk';
import nconf from 'nconf';
import got from 'got';
import Bluebird from 'bluebird';
import locals from '../../middlewares/api-v3/locals';
import {
S3,
} from '../../libs/api-v3/aws';
let S3 = new AWS.S3({
accessKeyId: nconf.get('S3:accessKeyId'),
secretAccessKey: nconf.get('S3:secretAccessKey'),
});
const S3_BUCKET = nconf.get('S3:bucket');
const BASE_URL = nconf.get('BASE_URL');

View file

@ -0,0 +1,7 @@
import AWS from 'aws-sdk';
import nconf from 'nconf';
export const S3 = new AWS.S3({
accessKeyId: nconf.get('S3:accessKeyId'),
secretAccessKey: nconf.get('S3:secretAccessKey'),
});

View file

@ -105,7 +105,16 @@ api.createSubscription = async function createSubscription (data) {
}
if (data.gift.member._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself
sendPushNotification(data.gift.member, shared.i18n.t('giftedSubscription'), `${months} months - by ${byUserName}`);
if (data.gift.member.preferences.pushNotifications.giftedSubscription !== false) {
sendPushNotification(data.gift.member,
{
title: shared.i18n.t('giftedSubscription'),
message: shared.i18n.t('giftedSubscriptionInfo', {months, name: byUserName}),
identifier: 'giftedSubscription',
payload: {replyTo: data.user._id},
}
);
}
}
}
@ -178,7 +187,16 @@ api.buyGems = async function buyGems (data) {
}
if (data.gift.member._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself
sendPushNotification(data.gift.member, shared.i18n.t('giftedGems'), `${gemAmount} Gems - by ${byUsername}`);
if (data.gift.member.preferences.pushNotifications.giftedGems !== false) {
sendPushNotification(
data.gift.member,
{
title: shared.i18n.t('giftedGems'),
message: shared.i18n.t('giftedGemsInfo', {amount: gemAmount, name: byUsername}),
identifier: 'giftedGems',
}
);
}
}
await data.gift.member.save();

View file

@ -1,8 +1,12 @@
/* eslint-disable */
import _ from 'lodash';
import nconf from 'nconf';
import pushNotify from 'push-notify';
import apnLib from 'apn';
import logger from './logger';
import Bluebird from 'bluebird';
import {
S3,
} from './aws';
const GCM_API_KEY = nconf.get('PUSH_CONFIGS:GCM_SERVER_API_KEY');
@ -11,47 +15,95 @@ let gcm = GCM_API_KEY ? pushNotify.gcm({
retries: 3,
}) : undefined;
// TODO review and test this file when push notifications are added back
if (gcm) {
gcm.on('transmitted', (/* result, message, registrationId */) => {
// console.info("transmitted", result, message, registrationId);
});
gcm.on('transmissionError', (/* error, message, registrationId */) => {
// console.info("transmissionError", error, message, registrationId);
});
gcm.on('updated', (/* result, registrationId */) => {
// console.info("updated", result, registrationId);
gcm.on('transmissionError', (err, message, registrationId) => {
logger.error('GCM Error', err, message, registrationId);
});
}
module.exports = function sendNotification (user, title, message, timeToLive = 15) {
return; // TODO push notifications are not currently enabled
let apn;
if (!user) return;
// Load APN certificate and key from S3
const APN_ENABLED = nconf.get('PUSH_CONFIGS:APN_ENABLED') === 'true';
const S3_BUCKET = nconf.get('S3:bucket');
if (APN_ENABLED) {
Bluebird.all([
S3.getObject({
Bucket: S3_BUCKET,
Key: 'apple_apn/cert.pem',
}).promise(),
S3.getObject({
Bucket: S3_BUCKET,
Key: 'apple_apn/key.pem',
}).promise(),
])
.then(([certObj, keyObj]) => {
let cert = certObj.Body.toString();
let key = keyObj.Body.toString();
apn = pushNotify.apn({
key,
cert,
});
apn.on('error', err => logger.error('APN error', err));
apn.on('transmissionError', (errorCode, notification, device) => {
logger.error('APN transmissionError', errorCode, notification, device);
});
let feedback = new apnLib.Feedback({
key,
cert,
batchFeedback: true,
interval: 3600, // Check for feedback once an hour
});
feedback.on('feedback', (devices) => {
if (devices && devices.length > 0) {
logger.info('Delivery of push notifications failed for some Apple devices.', devices);
}
});
});
}
module.exports = function sendNotification (user, details = {}) {
if (!user) throw new Error('User is required.');
if (user.preferences.pushNotifications.unsubscribeFromAll === true) return;
let pushDevices = user.pushDevices.toObject ? user.pushDevices.toObject() : user.pushDevices;
if (!details.identifier) throw new Error('details.identifier is required.');
if (!details.title) throw new Error('details.title is required.');
if (!details.message) throw new Error('details.message is required.');
let payload = details.payload ? details.payload : {};
payload.identifier = details.identifier;
_.each(pushDevices, pushDevice => {
switch (pushDevice.type) {
case 'android':
if (gcm) {
payload.title = details.title;
payload.message = details.message;
gcm.send({
registrationId: pushDevice.regId,
// collapseKey: 'COLLAPSE_KEY',
delayWhileIdle: true,
timeToLive,
data: {
title,
message,
},
timeToLive: details.timeToLive ? details.timeToLive : 15,
data: payload,
});
}
break;
case 'ios':
if (apn) {
apn.send({
token: pushDevice.regId,
alert: details.message,
sound: 'default',
category: details.category,
payload,
});
}
break;
}
});

View file

@ -296,8 +296,14 @@ schema.methods.closeChal = async function closeChal (broken = {}) {
{name: 'CHALLENGE_NAME', content: challenge.name},
]);
}
sendPushNotification(savedWinner, shared.i18n.t('wonChallenge'), challenge.name);
if (savedWinner.preferences.pushNotifications.wonChallenge !== false) {
sendPushNotification(savedWinner,
{
title: challenge.name,
message: shared.i18n.t('wonChallenge'),
identifier: 'wonChallenge',
});
}
}
// Run some operations in the background withouth blocking the thread

View file

@ -405,18 +405,29 @@ schema.methods.startQuest = async function startQuest (user) {
// send notifications in the background without blocking
User.find(
{ _id: { $in: nonUserQuestMembers } },
'party.quest items.quests auth.facebook auth.local preferences.emailNotifications pushDevices profile.name'
'party.quest items.quests auth.facebook auth.local preferences.emailNotifications preferences.pushNotifications pushDevices profile.name'
).exec().then((membersToNotify) => {
let membersToEmail = _.filter(membersToNotify, (member) => {
// send push notifications and filter users that disabled emails
sendPushNotification(member, 'HabitRPG', `${shared.i18n.t('questStarted')}: ${quest.text()}`);
return member.preferences.emailNotifications.questStarted !== false &&
member._id !== user._id;
});
sendTxnEmail(membersToEmail, 'quest-started', [
{ name: 'PARTY_URL', content: '/#/options/groups/party' },
]);
let membersToPush = _.filter(membersToNotify, (member) => {
// send push notifications and filter users that disabled emails
return member.preferences.pushNotifications.questStarted !== false &&
member._id !== user._id;
});
_.each(membersToPush, (member) => {
sendPushNotification(member,
{
title: quest.text(),
message: `${shared.i18n.t('questStarted')}: ${quest.text()}`,
identifier: 'questStarted',
});
});
});
};

View file

@ -0,0 +1,21 @@
import mongoose from 'mongoose';
import baseModel from '../libs/api-v3/baseModel';
const Schema = mongoose.Schema;
export let schema = new Schema({
regId: {type: String, required: true},
type: {type: String, required: true, enum: ['ios', 'android']},
}, {
strict: true,
minimize: false, // So empty objects are returned
_id: false,
});
schema.plugin(baseModel, {
noSet: ['_id', 'regId'],
timestamps: true,
_id: false,
});
export let model = mongoose.model('PushDevice', schema);

View file

@ -3,6 +3,7 @@ import shared from '../../../../common';
import _ from 'lodash';
import validator from 'validator';
import { schema as TagSchema } from '../tag';
import { schema as PushDeviceSchema } from '../pushDevice';
import {
schema as UserNotificationSchema,
} from '../userNotification';
@ -431,6 +432,17 @@ let schema = new Schema({
importantAnnouncements: {type: Boolean, default: true},
weeklyRecaps: {type: Boolean, default: true},
},
pushNotifications: {
unsubscribeFromAll: {type: Boolean, default: false},
newPM: {type: Boolean, default: true},
wonChallenge: {type: Boolean, default: true},
giftedGems: {type: Boolean, default: true},
giftedSubscription: {type: Boolean, default: true},
invitedParty: {type: Boolean, default: true},
invitedGuild: {type: Boolean, default: true},
questStarted: {type: Boolean, default: true},
invitedQuest: {type: Boolean, default: true},
},
suppressModals: {
levelUp: {type: Boolean, default: false},
hatchPet: {type: Boolean, default: false},
@ -505,10 +517,7 @@ let schema = new Schema({
extra: {type: Schema.Types.Mixed, default: () => {
return {};
}},
pushDevices: [{
regId: {type: String},
type: {type: String},
}],
pushDevices: [PushDeviceSchema],
}, {
strict: true,
minimize: false, // So empty objects are returned

View file

@ -1,17 +1,17 @@
.container-fluid
.stable.row: .col-xs-12
div(class="#{env.worldDmg.seasonalShop ? 'seasonalshop_broken' : 'seasonalshop_closed'}").pull-left-sm.col-centered
div(class="#{env.worldDmg.seasonalShop ? 'seasonalshop_broken' : 'seasonalshop_open'}").pull-left-sm.col-centered
.popover.static-popover.fade.right.in.pull-left-sm.col-centered
.arrow.hidden-xs
h3.popover-title!=env.t('seasonalShopClosedTitle', {linkStart:"<a href='http://blog.habitrpg.com/who' target='_blank'>", linkEnd: "</a>"})
h3.popover-title!=env.t('seasonalShopTitle', {linkStart:"<a href='http://blog.habitrpg.com/who' target='_blank'>", linkEnd: "</a>"})
.popover-content
p(ng-if='!env.worldDmg.seasonalShop')!=env.t('seasonalShopClosedText')
p(ng-if='env.worldDmg.seasonalShop')!=env.t('seasonalShopClosedText')
p(ng-if='!env.worldDmg.seasonalShop')!=env.t('seasonalShopSummerText')
p(ng-if='env.worldDmg.seasonalShop')!=env.t('seasonalShopSummerText')
// .well(ng-if='User.user.achievements.rebirths > 0')=env.t('seasonalShopRebirth')
.well(ng-if='User.user.achievements.rebirths > 0')=env.t('seasonalShopRebirth')
// li.customize-menu.inventory-gear
menu.pets-menu(label=env.t('quests'))
li.customize-menu.inventory-gear
// menu.pets-menu(label=env.t('quests'))
div(ng-repeat='quest in ::getSeasonalShopQuests()')
button.customize-option(ng-class='(quest.previous && !user.achievements.quests[quest.previous]) ? "inventory_quest_scroll_locked inventory_quest_scroll_{{::quest.key}}_locked locked" : "inventory_quest_scroll inventory_quest_scroll_{{::quest.key}}"'
data-popover-html="{{::quest.previous && !user.achievements.quests[quest.previous] ? env.t('scrollsPre') : questPopover(quest) | markdown}}",
@ -20,7 +20,7 @@
ng-click='buyQuest(quest.key)')
p {{::quest.value}}&nbsp;
span.Pet_Currency_Gem1x.inline-gems
menu.pets-menu(label=env.t('seasonalItems'))
// menu.pets-menu(label=env.t('seasonalItems'))
div
button.customize-option(class='inventory_special_shinySeed',
popover='{{::Content.spells.special.shinySeed.notes()}}',
@ -43,7 +43,7 @@
button.customize-option(popover='{{::Content.spells.special.nye.notes()}}', popover-title='{{::Content.spells.special.nye.text()}}', popover-trigger='mouseenter', popover-placement='right', popover-append-to-body='true', ng-click='castStart(Content.spells.special.nye)', class='inventory_special_nye')
p {{Content.spells.special.nye.value}}
span(class='shop_gold')
menu.pets-menu(label='{{::label}}', ng-repeat='(set,label) in ::{springWarrior:env.t("mightyBunnySet"), springMage:env.t("magicMouseSet"), springHealer:env.t("lovingPupSet"), springRogue:env.t("stealthyKittySet"), spring2015Warrior:env.t("bewareDogSet"), spring2015Mage:env.t("magicianBunnySet"), spring2015Healer:env.t("comfortingKittySet"), spring2015Rogue:env.t("sneakySqueakerSet")}')
menu.pets-menu(label='{{::label}}', ng-repeat='(set,label) in ::{summerWarrior:env.t("daringSwashbucklerSet"), summerMage:env.t("emeraldMermageSet"), summerHealer:env.t("reefSeahealerSet"), summerRogue:env.t("roguishPirateSet"), summer2015Warrior:env.t("sunfishWarriorSet"), summer2015Mage:env.t("shipSoothsayerSet"), summer2015Healer:env.t("strappingSailorSet"), summer2015Rogue:env.t("reefRenegadeSet")}')
div(ng-repeat='item in ::getSeasonalShopArray(set)',
ng-class="{transparent: user.items.gear.owned[item.key] !== undefined}")
button.customize-option(class='shop_{{::item.key}}',

View file

@ -321,25 +321,46 @@ script(id='partials/options.settings.notifications.html', type="text/ng-template
.personal-options.col-md-6
.panel.panel-default
.panel-heading
=env.t('emailNotifications')
=env.t('notifications')
.panel-body
each notification in ['newPM', 'wonChallenge', 'giftedGems', 'giftedSubscription', 'invitedParty', 'invitedGuild', 'kickedGroup', 'questStarted', 'invitedQuest', 'importantAnnouncements', 'weeklyRecaps']
-var preference = 'user.preferences.emailNotifications.' + notification
-var unsubscribeFromAll = 'user.preferences.emailNotifications.unsubscribeFromAll'
.checkbox: label
input(type='checkbox', ng-model='#{preference}',
ng-disabled='#{unsubscribeFromAll} === true',
ng-checked='#{unsubscribeFromAll} === false && #{preference} === true',
ng-change='set({"preferences.emailNotifications.#{notification}": #{preference} ? true: false})')
span=env.t(notification)
table.table
tr
td
th
span=env.t("email")
th
span=env.t("push")
-var unsubscribeFromAllEmails = 'user.preferences.emailNotifications.unsubscribeFromAll'
-var unsubscribeFromAllPush = 'user.preferences.pushNotifications.unsubscribeFromAll'
each notification in ['newPM', 'wonChallenge', 'giftedGems', 'giftedSubscription', 'invitedParty', 'invitedGuild', 'kickedGroup', 'questStarted', 'invitedQuest', 'importantAnnouncements', 'weeklyRecaps']
tr
td
span=env.t(notification)
td
-var preference = 'user.preferences.emailNotifications.' + notification
input(type='checkbox', ng-model='#{preference}',
ng-disabled='#{unsubscribeFromAllEmails} === true || #{preference} === undefined',
ng-checked='#{unsubscribeFromAllEmails} === false && #{preference} === true',
ng-change='set({"preferences.emailNotifications.#{notification}": #{preference} ? true: false})')
td
-var preference = 'user.preferences.pushNotifications.' + notification
input(type='checkbox', ng-model='#{preference}',
ng-disabled='#{unsubscribeFromAllPush} === true || #{preference} === undefined',
ng-checked='#{unsubscribeFromAllPush} === false && #{preference} === true',
ng-change='set({"preferences.pushNotifications.#{notification}": #{preference} ? true: false})')
hr
.checkbox
label
input(type='checkbox', ng-model='user.preferences.emailNotifications.unsubscribeFromAll', ng-change='set({"preferences.emailNotifications.unsubscribeFromAll": user.preferences.emailNotifications.unsubscribeFromAll ? true: false})')
input(type='checkbox', ng-model='user.preferences.pushNotifications.unsubscribeFromAll',
ng-change='set({"preferences.pushNotifications.unsubscribeFromAll": user.preferences.pushNotifications.unsubscribeFromAll ? true: false})')
span=env.t('unsubscribeAllPush')
.checkbox
label
input(type='checkbox', ng-model='user.preferences.emailNotifications.unsubscribeFromAll',
ng-change='set({"preferences.emailNotifications.unsubscribeFromAll": user.preferences.emailNotifications.unsubscribeFromAll ? true: false})')
span=env.t('unsubscribeAllEmails')
small=env.t('unsubscribeAllEmailsText')

View file

@ -1,21 +1,44 @@
h2 6/17/2016 - EQUIPMENT SORTING AND GUILD SPOTLIGHT
h2 SUMMER SPLASH EVENT! LIMITED EDITION OUTFITS, SEASONAL SHOP OPENS, AND SUMMER NPCS
hr
tr
td
h3 New Equipment Sorting Option
p The Equipment page now has a new "Group by Type" option that arranges your equipment by armor, headgear, etc. Choose the equipment organization method that you like best!
p.small.muted by Hus274
h3 Summer Splash Begins
p The Summer Splash festival has arrived, and Habitica has moved to the undersea city of Dilatory for the summer! From today until July 31st, join us for fun in the sun.
tr
td
h3 June Guild Spotlight
p We're highlighting several Guilds that can help you achieve your goals this summer, including the Long-Term Accountability Guild for that massive personal project, the Young Adult - Unprepared Guild for all you recent grads, and the Legendary Book Club of Habitica to help with that summer reading. <a href='https://habitica.wordpress.com/2016/06/17/start-your-summer-projects-june-guild-spotlight/' target='_blank'>Learn more here</a>!
p.small.muted by redphoenix
.promo_summer_classes_2016.pull-right
h3 Limited Edition Class Outfits
p From now until July 31st, limited edition outfits are available in the Rewards column. Depending on your class, you can be a Seahorse Healer, a Shark Warrior, an Eel Rogue, or a Dolphin Mage! You'd better get productive to earn enough gold before they disappear. Good luck!
p.small.muted by Giu08. Jynn, TetoIsGreat, Kai-Wen, and Lemoness
tr
td
.promo_summer_classes_2015.pull-left.slight-right-margin
h3 Seasonal Shop Opens
p The <a href='/#/options/inventory/seasonalshop'>Seasonal Shop</a> has opened! It's stocking summertime Seasonal Edition goodies at the moment, including last year's summer outfits. Everything there will be available to purchase during the Summer Splash event each year, but it's only open until July 31st, so be sure to stock up now, or you'll have to wait a year to buy these items again.
p.small.muted by Lemoness
tr
td
.seasonalshop_open.pull-right
h3 Summer NPCs
p Looks like the NPCs are really getting in to the summer spirit. Ian, Bailey, Matt, and the Seasonal Sorceress are having fun under the sea in the sunken city of Dilatory, and Alex and Daniel have moved down to the beach. Even the Time Travelers are getting into the fun, although... oh dear... they seem to have overshot the season...
p.small.muted by Lemoness
if menuItem !== 'oldNews'
hr
a(href='/static/old-news', target='_blank') Read older news
mixin oldNews
h2 6/17/2016 - EQUIPMENT SORTING AND GUILD SPOTLIGHT
tr
td
h3 New Equipment Sorting Option
p The Equipment page now has a new "Group by Type" option that arranges your equipment by armor, headgear, etc. Choose the equipment organization method that you like best!
p.small.muted by Hus274
tr
td
h3 June Guild Spotlight
p We're highlighting several Guilds that can help you achieve your goals this summer, including the Long-Term Accountability Guild for that massive personal project, the Young Adult - Unprepared Guild for all you recent grads, and the Legendary Book Club of Habitica to help with that summer reading. <a href='https://habitica.wordpress.com/2016/06/17/start-your-summer-projects-june-guild-spotlight/' target='_blank'>Learn more here</a>!
p.small.muted by redphoenix
h2 6/15/2016 - NEW PET QUEST: AXOLOTL
span.Mount_Body_Axolotl-CottonCandyPink.pull-right
span.Mount_Head_Axolotl-CottonCandyPink.pull-right(style='margin:0')