Merge pull request #5855 from HabitRPG/sabrecat/party-page

Party Page Revamp
This commit is contained in:
Blade Barringer 2015-09-06 13:28:14 -05:00
commit 4177202cee
47 changed files with 2797 additions and 1755 deletions

File diff suppressed because one or more lines are too long

View file

@ -3012,157 +3012,157 @@
}
.shop_armor_armoire_gladiatorArmor {
background-image: url(spritesmith2.png);
background-position: 0px -1729px;
background-position: -123px -1729px;
width: 40px;
height: 40px;
}
.shop_armor_armoire_goldenToga {
background-image: url(spritesmith2.png);
background-position: -1744px -1681px;
background-position: -82px -1729px;
width: 40px;
height: 40px;
}
.shop_armor_armoire_hornedIronArmor {
background-image: url(spritesmith2.png);
background-position: -1744px -1558px;
background-position: -41px -1729px;
width: 40px;
height: 40px;
}
.shop_armor_armoire_lunarArmor {
background-image: url(spritesmith2.png);
background-position: -1744px -1517px;
background-position: -1729px -1640px;
width: 40px;
height: 40px;
}
.shop_armor_armoire_plagueDoctorOvercoat {
background-image: url(spritesmith2.png);
background-position: -1744px -1476px;
background-position: -1729px -1599px;
width: 40px;
height: 40px;
}
.shop_armor_armoire_rancherRobes {
background-image: url(spritesmith2.png);
background-position: -1744px -1312px;
background-position: -1729px -1558px;
width: 40px;
height: 40px;
}
.shop_eyewear_armoire_plagueDoctorMask {
background-image: url(spritesmith2.png);
background-position: -1744px -1271px;
background-position: -1729px -1394px;
width: 40px;
height: 40px;
}
.shop_head_armoire_blueHairbow {
background-image: url(spritesmith2.png);
background-position: -1744px -1230px;
background-position: -1729px -1353px;
width: 40px;
height: 40px;
}
.shop_head_armoire_gladiatorHelm {
background-image: url(spritesmith2.png);
background-position: -1744px -1189px;
background-position: -1729px -1312px;
width: 40px;
height: 40px;
}
.shop_head_armoire_goldenLaurels {
background-image: url(spritesmith2.png);
background-position: -1744px -1066px;
background-position: -1729px -1271px;
width: 40px;
height: 40px;
}
.shop_head_armoire_hornedIronHelm {
background-image: url(spritesmith2.png);
background-position: -1744px -1025px;
background-position: -1729px -1148px;
width: 40px;
height: 40px;
}
.shop_head_armoire_lunarCrown {
background-image: url(spritesmith2.png);
background-position: -1744px -984px;
background-position: -1729px -1107px;
width: 40px;
height: 40px;
}
.shop_head_armoire_plagueDoctorHat {
background-image: url(spritesmith2.png);
background-position: -1744px -943px;
background-position: -1729px -1066px;
width: 40px;
height: 40px;
}
.shop_head_armoire_rancherHat {
background-image: url(spritesmith2.png);
background-position: -1744px -902px;
background-position: -1729px -1025px;
width: 40px;
height: 40px;
}
.shop_head_armoire_redFloppyHat {
background-image: url(spritesmith2.png);
background-position: -1744px -861px;
background-position: -1729px -984px;
width: 40px;
height: 40px;
}
.shop_head_armoire_redHairbow {
background-image: url(spritesmith2.png);
background-position: -1744px -820px;
background-position: -1729px -943px;
width: 40px;
height: 40px;
}
.shop_head_armoire_royalCrown {
background-image: url(spritesmith2.png);
background-position: -1744px -779px;
background-position: -1729px -902px;
width: 40px;
height: 40px;
}
.shop_head_armoire_violetFloppyHat {
background-image: url(spritesmith2.png);
background-position: -1744px -738px;
background-position: -1729px -861px;
width: 40px;
height: 40px;
}
.shop_head_armoire_yellowHairbow {
background-image: url(spritesmith2.png);
background-position: -1744px -697px;
background-position: -1729px -820px;
width: 40px;
height: 40px;
}
.shop_shield_armoire_gladiatorShield {
background-image: url(spritesmith2.png);
background-position: -1744px -41px;
background-position: -1729px -779px;
width: 40px;
height: 40px;
}
.shop_weapon_armoire_basicCrossbow {
background-image: url(spritesmith2.png);
background-position: -1744px 0px;
background-position: -1729px -123px;
width: 40px;
height: 40px;
}
.shop_weapon_armoire_goldWingStaff {
background-image: url(spritesmith2.png);
background-position: -1696px -1679px;
background-position: -1729px -82px;
width: 40px;
height: 40px;
}
.shop_weapon_armoire_ironCrook {
background-image: url(spritesmith2.png);
background-position: -1655px -1679px;
background-position: -1729px -41px;
width: 40px;
height: 40px;
}
.shop_weapon_armoire_lunarSceptre {
background-image: url(spritesmith2.png);
background-position: -1614px -1679px;
background-position: -1729px 0px;
width: 40px;
height: 40px;
}
.shop_weapon_armoire_mythmakerSword {
background-image: url(spritesmith2.png);
background-position: -1573px -1679px;
background-position: -1670px -1679px;
width: 40px;
height: 40px;
}
.shop_weapon_armoire_rancherLasso {
background-image: url(spritesmith2.png);
background-position: -1532px -1679px;
background-position: -1629px -1679px;
width: 40px;
height: 40px;
}
@ -3372,205 +3372,205 @@
}
.shop_armor_healer_1 {
background-image: url(spritesmith2.png);
background-position: -1491px -1679px;
background-position: -1588px -1679px;
width: 40px;
height: 40px;
}
.shop_armor_healer_2 {
background-image: url(spritesmith2.png);
background-position: -1450px -1679px;
background-position: -1547px -1679px;
width: 40px;
height: 40px;
}
.shop_armor_healer_3 {
background-image: url(spritesmith2.png);
background-position: -1409px -1679px;
background-position: -1506px -1679px;
width: 40px;
height: 40px;
}
.shop_armor_healer_4 {
background-image: url(spritesmith2.png);
background-position: -1368px -1679px;
background-position: -1465px -1679px;
width: 40px;
height: 40px;
}
.shop_armor_healer_5 {
background-image: url(spritesmith2.png);
background-position: -1327px -1679px;
background-position: -1424px -1679px;
width: 40px;
height: 40px;
}
.shop_armor_rogue_1 {
background-image: url(spritesmith2.png);
background-position: -1286px -1679px;
background-position: -1383px -1679px;
width: 40px;
height: 40px;
}
.shop_armor_rogue_2 {
background-image: url(spritesmith2.png);
background-position: -1163px -1679px;
background-position: -1342px -1679px;
width: 40px;
height: 40px;
}
.shop_armor_rogue_3 {
background-image: url(spritesmith2.png);
background-position: -1122px -1679px;
background-position: -1219px -1679px;
width: 40px;
height: 40px;
}
.shop_armor_rogue_4 {
background-image: url(spritesmith2.png);
background-position: -1696px -1638px;
background-position: -1178px -1679px;
width: 40px;
height: 40px;
}
.shop_armor_rogue_5 {
background-image: url(spritesmith2.png);
background-position: -1655px -1638px;
background-position: -1137px -1679px;
width: 40px;
height: 40px;
}
.shop_armor_special_0 {
background-image: url(spritesmith2.png);
background-position: -1614px -1638px;
background-position: -1670px -1638px;
width: 40px;
height: 40px;
}
.shop_armor_special_1 {
background-image: url(spritesmith2.png);
background-position: -1573px -1638px;
background-position: -1629px -1638px;
width: 40px;
height: 40px;
}
.shop_armor_special_2 {
background-image: url(spritesmith2.png);
background-position: -1532px -1638px;
background-position: -1588px -1638px;
width: 40px;
height: 40px;
}
.shop_armor_special_finnedOceanicArmor {
background-image: url(spritesmith2.png);
background-position: -1491px -1638px;
background-position: -1547px -1638px;
width: 40px;
height: 40px;
}
.shop_armor_warrior_1 {
background-image: url(spritesmith2.png);
background-position: -1450px -1638px;
background-position: -1506px -1638px;
width: 40px;
height: 40px;
}
.shop_armor_warrior_2 {
background-image: url(spritesmith2.png);
background-position: -1409px -1638px;
background-position: -1465px -1638px;
width: 40px;
height: 40px;
}
.shop_armor_warrior_3 {
background-image: url(spritesmith2.png);
background-position: -1368px -1638px;
background-position: -1424px -1638px;
width: 40px;
height: 40px;
}
.shop_armor_warrior_4 {
background-image: url(spritesmith2.png);
background-position: -1327px -1638px;
background-position: -1383px -1638px;
width: 40px;
height: 40px;
}
.shop_armor_warrior_5 {
background-image: url(spritesmith2.png);
background-position: -1286px -1638px;
background-position: -1342px -1638px;
width: 40px;
height: 40px;
}
.shop_armor_wizard_1 {
background-image: url(spritesmith2.png);
background-position: -1245px -1638px;
background-position: -1301px -1638px;
width: 40px;
height: 40px;
}
.shop_armor_wizard_2 {
background-image: url(spritesmith2.png);
background-position: -1204px -1638px;
background-position: -1260px -1638px;
width: 40px;
height: 40px;
}
.shop_armor_wizard_3 {
background-image: url(spritesmith2.png);
background-position: -1163px -1638px;
background-position: -1219px -1638px;
width: 40px;
height: 40px;
}
.shop_armor_wizard_4 {
background-image: url(spritesmith2.png);
background-position: -1744px -246px;
background-position: -1729px -369px;
width: 40px;
height: 40px;
}
.shop_armor_wizard_5 {
background-image: url(spritesmith2.png);
background-position: -1122px -1638px;
background-position: -1178px -1638px;
width: 40px;
height: 40px;
}
.slim_armor_healer_1 {
background-image: url(spritesmith2.png);
background-position: -485px -1638px;
background-position: -500px -1638px;
width: 90px;
height: 90px;
}
.slim_armor_healer_2 {
background-image: url(spritesmith2.png);
background-position: -576px -1638px;
background-position: -591px -1638px;
width: 90px;
height: 90px;
}
.slim_armor_healer_3 {
background-image: url(spritesmith2.png);
background-position: -667px -1638px;
background-position: -682px -1638px;
width: 90px;
height: 90px;
}
.slim_armor_healer_4 {
background-image: url(spritesmith2.png);
background-position: -758px -1638px;
background-position: -773px -1638px;
width: 90px;
height: 90px;
}
.slim_armor_healer_5 {
background-image: url(spritesmith2.png);
background-position: -849px -1638px;
background-position: -864px -1638px;
width: 90px;
height: 90px;
}
.slim_armor_rogue_1 {
background-image: url(spritesmith2.png);
background-position: -940px -1638px;
background-position: -955px -1638px;
width: 90px;
height: 90px;
}
.slim_armor_rogue_2 {
background-image: url(spritesmith2.png);
background-position: -394px -1638px;
background-position: -1046px -1638px;
width: 90px;
height: 90px;
}
.slim_armor_rogue_3 {
background-image: url(spritesmith2.png);
background-position: -303px -1638px;
background-position: -409px -1638px;
width: 90px;
height: 90px;
}
.slim_armor_rogue_4 {
background-image: url(spritesmith2.png);
background-position: -212px -1638px;
background-position: -318px -1638px;
width: 90px;
height: 90px;
}
.slim_armor_rogue_5 {
background-image: url(spritesmith2.png);
background-position: -121px -1638px;
background-position: -227px -1638px;
width: 90px;
height: 90px;
}
@ -3660,13 +3660,13 @@
}
.shop_armor_special_birthday {
background-image: url(spritesmith2.png);
background-position: -1204px -1679px;
background-position: -1260px -1679px;
width: 40px;
height: 40px;
}
.shop_armor_special_birthday2015 {
background-image: url(spritesmith2.png);
background-position: -1245px -1679px;
background-position: -1301px -1679px;
width: 40px;
height: 40px;
}
@ -3690,313 +3690,319 @@
}
.broad_armor_special_fallMage {
background-image: url(spritesmith2.png);
background-position: 0px -1638px;
background-position: -106px -1638px;
width: 120px;
height: 90px;
}
.broad_armor_special_fallRogue {
background-image: url(spritesmith2.png);
background-position: -1638px 0px;
background-position: 0px -1638px;
width: 105px;
height: 90px;
}
.broad_armor_special_fallWarrior {
background-image: url(spritesmith2.png);
background-position: -1547px -1547px;
background-position: -1638px 0px;
width: 90px;
height: 90px;
}
.head_special_fallHealer {
background-image: url(spritesmith2.png);
background-position: -1424px -1456px;
background-position: -1547px -1547px;
width: 90px;
height: 90px;
}
.head_special_fallMage {
background-image: url(spritesmith2.png);
background-position: -1303px -1456px;
background-position: -1394px -1456px;
width: 120px;
height: 90px;
}
.head_special_fallRogue {
background-image: url(spritesmith2.png);
background-position: -1197px -1456px;
background-position: -1288px -1456px;
width: 105px;
height: 90px;
}
.head_special_fallWarrior {
background-image: url(spritesmith2.png);
background-position: -1106px -1456px;
background-position: -1197px -1456px;
width: 90px;
height: 90px;
}
.shield_special_fallHealer {
background-image: url(spritesmith2.png);
background-position: -1015px -1456px;
background-position: -1106px -1456px;
width: 90px;
height: 90px;
}
.shield_special_fallRogue {
background-image: url(spritesmith2.png);
background-position: -909px -1456px;
background-position: -1000px -1456px;
width: 105px;
height: 90px;
}
.shield_special_fallWarrior {
background-image: url(spritesmith2.png);
background-position: -818px -1456px;
background-position: -909px -1456px;
width: 90px;
height: 90px;
}
.shop_armor_special_fallHealer {
background-image: url(spritesmith2.png);
background-position: -1744px -82px;
background-position: -1729px -164px;
width: 40px;
height: 40px;
}
.shop_armor_special_fallMage {
background-image: url(spritesmith2.png);
background-position: -1744px -123px;
background-position: -1729px -205px;
width: 40px;
height: 40px;
}
.shop_armor_special_fallRogue {
background-image: url(spritesmith2.png);
background-position: -1744px -164px;
background-position: -1729px -246px;
width: 40px;
height: 40px;
}
.shop_armor_special_fallWarrior {
background-image: url(spritesmith2.png);
background-position: -1744px -205px;
background-position: -1729px -287px;
width: 40px;
height: 40px;
}
.shop_head_special_fallHealer {
background-image: url(spritesmith2.png);
background-position: -41px -1729px;
background-position: -1729px -328px;
width: 40px;
height: 40px;
}
.shop_head_special_fallMage {
background-image: url(spritesmith2.png);
background-position: -1744px -287px;
background-position: -1137px -1638px;
width: 40px;
height: 40px;
}
.shop_head_special_fallRogue {
background-image: url(spritesmith2.png);
background-position: -1744px -328px;
background-position: -1729px -410px;
width: 40px;
height: 40px;
}
.shop_head_special_fallWarrior {
background-image: url(spritesmith2.png);
background-position: -1744px -369px;
background-position: -1729px -451px;
width: 40px;
height: 40px;
}
.shop_shield_special_fallHealer {
background-image: url(spritesmith2.png);
background-position: -1744px -410px;
background-position: -1729px -492px;
width: 40px;
height: 40px;
}
.shop_shield_special_fallRogue {
background-image: url(spritesmith2.png);
background-position: -1744px -451px;
background-position: -1729px -533px;
width: 40px;
height: 40px;
}
.shop_shield_special_fallWarrior {
background-image: url(spritesmith2.png);
background-position: -1744px -492px;
background-position: -1729px -574px;
width: 40px;
height: 40px;
}
.shop_weapon_special_fallHealer {
background-image: url(spritesmith2.png);
background-position: -1744px -533px;
background-position: -1729px -615px;
width: 40px;
height: 40px;
}
.shop_weapon_special_fallMage {
background-image: url(spritesmith2.png);
background-position: -1744px -574px;
background-position: -1729px -656px;
width: 40px;
height: 40px;
}
.shop_weapon_special_fallRogue {
background-image: url(spritesmith2.png);
background-position: -1744px -615px;
background-position: -1729px -697px;
width: 40px;
height: 40px;
}
.shop_weapon_special_fallWarrior {
background-image: url(spritesmith2.png);
background-position: -1744px -656px;
background-position: -1729px -738px;
width: 40px;
height: 40px;
}
.slim_armor_special_fallHealer {
background-image: url(spritesmith2.png);
background-position: -727px -1456px;
background-position: -818px -1456px;
width: 90px;
height: 90px;
}
.slim_armor_special_fallMage {
background-image: url(spritesmith2.png);
background-position: -606px -1456px;
background-position: -697px -1456px;
width: 120px;
height: 90px;
}
.slim_armor_special_fallRogue {
background-image: url(spritesmith2.png);
background-position: -500px -1456px;
background-position: -591px -1456px;
width: 105px;
height: 90px;
}
.slim_armor_special_fallWarrior {
background-image: url(spritesmith2.png);
background-position: -409px -1456px;
background-position: -500px -1456px;
width: 90px;
height: 90px;
}
.weapon_special_fallHealer {
background-image: url(spritesmith2.png);
background-position: -318px -1456px;
background-position: -409px -1456px;
width: 90px;
height: 90px;
}
.weapon_special_fallMage {
background-image: url(spritesmith2.png);
background-position: -197px -1456px;
background-position: -288px -1456px;
width: 120px;
height: 90px;
}
.weapon_special_fallRogue {
background-image: url(spritesmith2.png);
background-position: -91px -1456px;
background-position: -182px -1456px;
width: 105px;
height: 90px;
}
.weapon_special_fallWarrior {
background-image: url(spritesmith2.png);
background-position: 0px -1456px;
background-position: -91px -1456px;
width: 90px;
height: 90px;
}
.broad_armor_special_gaymerx {
background-image: url(spritesmith2.png);
background-position: -1456px -1365px;
background-position: 0px -1456px;
width: 90px;
height: 90px;
}
.head_special_gaymerx {
background-image: url(spritesmith2.png);
background-position: -1456px -1274px;
background-position: -1456px -1365px;
width: 90px;
height: 90px;
}
.shop_armor_special_gaymerx {
background-image: url(spritesmith2.png);
background-position: -1744px -1107px;
background-position: -1729px -1189px;
width: 40px;
height: 40px;
}
.shop_head_special_gaymerx {
background-image: url(spritesmith2.png);
background-position: -1744px -1148px;
background-position: -1729px -1230px;
width: 40px;
height: 40px;
}
.slim_armor_special_gaymerx {
background-image: url(spritesmith2.png);
background-position: -1456px -1183px;
background-position: -1456px -1274px;
width: 90px;
height: 90px;
}
.back_mystery_201402 {
background-image: url(spritesmith2.png);
background-position: -1456px -1092px;
background-position: -1456px -1183px;
width: 90px;
height: 90px;
}
.broad_armor_mystery_201402 {
background-image: url(spritesmith2.png);
background-position: -1456px -1001px;
background-position: -1456px -1092px;
width: 90px;
height: 90px;
}
.head_mystery_201402 {
background-image: url(spritesmith2.png);
background-position: -1456px -910px;
background-position: -1456px -1001px;
width: 90px;
height: 90px;
}
.shop_armor_mystery_201402 {
background-image: url(spritesmith2.png);
background-position: -1744px -1353px;
background-position: -1729px -1435px;
width: 40px;
height: 40px;
}
.shop_back_mystery_201402 {
background-image: url(spritesmith2.png);
background-position: -1744px -1394px;
background-position: -1729px -1476px;
width: 40px;
height: 40px;
}
.shop_head_mystery_201402 {
background-image: url(spritesmith2.png);
background-position: -1744px -1435px;
background-position: -1729px -1517px;
width: 40px;
height: 40px;
}
.slim_armor_mystery_201402 {
background-image: url(spritesmith2.png);
background-position: -1456px -819px;
background-position: -1456px -910px;
width: 90px;
height: 90px;
}
.broad_armor_mystery_201403 {
background-image: url(spritesmith2.png);
background-position: -1456px -728px;
background-position: -1456px -819px;
width: 90px;
height: 90px;
}
.headAccessory_mystery_201403 {
background-image: url(spritesmith2.png);
background-position: -1456px -637px;
background-position: -1456px -728px;
width: 90px;
height: 90px;
}
.shop_armor_mystery_201403 {
background-image: url(spritesmith2.png);
background-position: -1744px -1599px;
background-position: -1729px -1681px;
width: 40px;
height: 40px;
}
.shop_headAccessory_mystery_201403 {
background-image: url(spritesmith2.png);
background-position: -1744px -1640px;
background-position: 0px -1729px;
width: 40px;
height: 40px;
}
.slim_armor_mystery_201403 {
background-image: url(spritesmith2.png);
background-position: -1456px -546px;
background-position: -1456px -637px;
width: 90px;
height: 90px;
}
.back_mystery_201404 {
background-image: url(spritesmith2.png);
background-position: -1456px -455px;
background-position: -1456px -546px;
width: 90px;
height: 90px;
}
.headAccessory_mystery_201404 {
background-image: url(spritesmith2.png);
background-position: -1031px -1638px;
background-position: -1456px -455px;
width: 90px;
height: 90px;
}
.shop_back_mystery_201404 {
background-image: url(spritesmith2.png);
background-position: -164px -1729px;
width: 40px;
height: 40px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 92 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 598 KiB

After

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -14,7 +14,7 @@
"resources": "Resources",
"tavernTalk": "Tavern Talk",
"tavernAlert1": "Note: if you're reporting a bug, the developers won't see it here. Please",
"tavernAlert2": "use Github instead",
"tavernAlert2": "use GitHub instead",
"moderatorIntro1": "Tavern and guild moderators are: ",
"communityGuidelines": "Community Guidelines",
"communityGuidelinesRead1": "Please read our",
@ -24,11 +24,17 @@
"updatedParty": "Party settings updated.",
"noPartyText": "You are either not in a party or your party is taking a while to load. You can either create one and invite friends, or if you want to join an existing party, have them enter your Unique User ID below and then come back here to look for the invitation:",
"LFG": "To advertise your new party or find one to join, go to the <%= linkStart %>Party Wanted (Looking for Group)<%= linkEnd %> Guild.",
"wantExistingParty": "Want to join an existing party? Go to the <%= linkStart %>Party Wanted Guild<%= linkEnd %> and post this User ID:",
"joinExistingParty": "Join Someone Else's Party",
"create": "Create",
"userId": "User ID",
"invite": "Invite",
"leave": "Leave",
"invitedTo": "Invited to <%= name %>",
"invitedToNewParty": "You were invited to join a party! Do you want to leave this party and join <%= partyName %>?",
"joinNewParty": "Join New Party",
"declineInvitation": "Decline Invitation",
"loadingNewParty": "Your new party is loading. Please wait...",
"newMsg": "New message in \"<%= name %>\"",
"chat": "Chat",
"sendChat": "Send Chat",
@ -44,8 +50,8 @@
"groupDescr": "Description shown in public Guilds list (Markdown OK)",
"logoUrl": "Logo URL",
"assignLeader": "Assign Group Leader",
"members": "Member(s)",
"partyList": "Order for party members in header (reload browser after changing order)",
"members": "Members",
"partyList": "Order for party members in header",
"banTip": "Boot Member",
"moreMembers": "more members",
"invited": "Invited",
@ -106,11 +112,16 @@
"leaderOnlyChallenges": "Only group leader can create challenges",
"sendGift": "Send Gift",
"inviteFriends": "Invite Friends",
"inviteAlertInfo": "Invite friends by <a href='http://habitica.wikia.com/wiki/API_Options' target='_blank'>User ID</a> here.",
"inviteExistUser": "Invite Existing User",
"inviteByEmail": "Invite friends by email. If they join via your email, they'll automatically be invited to this group.",
"inviteByEmail": "Invite by Email",
"inviteByEmailExplanation": "If a friend joins Habitica via your email, they'll automatically be invited to your party!",
"inviteFriendsNow": "Invite Friends Now",
"inviteFriendsLater": "Invite Friends Later",
"inviteAlertInfo": "If you have friends already using Habitica, invite them by <a href='http://habitica.wikia.com/wiki/API_Options' target='_blank'>User ID</a> here.",
"inviteExistUser": "Invite Existing Users",
"byColon": "By:",
"inviteNewUsers": "Invite New User(s)",
"inviteNewUsers": "Invite New Users",
"sendInvitations": "Send Invitations",
"invitationsSent": "Invitations sent!",
"inviteAlertInfo2": "Or share this link (copy/paste):",
"sendGiftHeading": "Send Gift to <%= name %>",
"sendGiftGemsBalance": "From <%= number %> Gems",
@ -120,7 +131,19 @@
"sendGiftMessagePlaceholder": "Personal message (optional)",
"sendGiftSubscription": "<%= months %> Month(s): $<%= price %>",
"battleWithFriends": "Battle Monsters With Friends",
"startPartyWithFriends": "Start a Party with your friends!",
"startAParty": "Start a Party",
"addToParty": "Add someone to your party",
"likePost": "Click if you like this post!"
"likePost": "Click if you like this post!",
"partyExplanation1": "Play Habitica with friends to stay accountable!",
"partyExplanation2": "Battle monsters and create Challenges!",
"partyExplanation3": "Invite friends now to earn a Quest Scroll!",
"wantToStartParty": "Do you want to start a party?",
"exclusiveQuestScroll": "Inviting a friend to your party will grant you an exclusive Quest Scroll to battle the Basi-List together!",
"nameYourParty": "Name your new party!",
"partyEmpty": "You're the only one in your party. Invite your friends!",
"partyChatEmpty": "Your party chat is empty! Type a message in the box above to start chatting.",
"guildChatEmpty": "This guild's chat is empty! Type a message in the box above to start chatting.",
"possessiveParty": "<%= name %>'s Party"
}

View file

@ -13,7 +13,7 @@
"youReceived": "You've Received",
"dropQuestCongrats": "Congratulations on earning this quest scroll! You can invite your party to begin the quest now, or come back to it any time in your Inventory > Quests.",
"questSend": "Clicking \"Invite\" will send an invitation to your party members. When all members have accepted or denied, the quest begins. See status under Social > Party.",
"inviteParty": "Invite Party",
"inviteParty": "Invite Party to Quest",
"questInvitation": "Quest Invitation: ",
"questInvitationTitle": "Quest Invitation",
"questInvitationInfo": "Invitation for the Quest <%= quest %>",
@ -29,12 +29,16 @@
"bossStrength": "Boss Strength",
"collect": "Collect",
"collected": "Collected",
"collectionItems": "<%= number %> <%= items %>",
"itemsToCollect": "Items to Collect",
"bossDmg1": "Each completed Daily and To-Do and each positive Habit hurts the boss. Hurt it more with redder tasks or Brutal Smash and Burst of Flames. The boss will deal damage to every quest participant for every Daily you've missed (multiplied by the boss's Strength) in addition to your regular damage, so keep your party healthy by completing your Dailies! <strong>All damage to and from a boss is tallied on cron (your day roll-over).</strong>",
"bossDmg2": "Only participants will fight the boss and share in the quest loot.",
"tavernBossInfo": "To hurt a world boss, complete your Dailies and To-Dos. Higher task damage means higher boss damage (completing reds, Mage spells, Warrior attacks, etc). For each Daily you've missed (multiplied by the boss's Strength), the boss's Rage will increase. Once his Rage reaches max, something bad will happen - so complete your Dailies! <strong>All damage to and from a boss is tallied on cron (your day roll-over).</strong>",
"bossColl1": "To collect items, do your positive tasks. Quest items drop just like normal items; however, you won't see the drops until the next day, then everything you've found will be tallied up and contributed to the pile.",
"bossColl2": "Only participants can collect items and share in the quest loot.",
"abort": "Abort",
"leaveQuest": "Leave Quest",
"sureLeave": "Are you sure you want to leave the active quest? All your quest progress will be lost.",
"questOwner": "Quest Owner",
"questOwnerNotInPendingQuest": "The quest owner has left the quest and can no longer begin it. It is recommended that you cancel it now. The quest owner will retain possession of the quest scroll.",
"questOwnerNotInRunningQuest": "The quest owner has left the quest. You can abort the quest if you need to. You can also allow it to keep running and all remaining participants will receive the quest rewards when the quest finishes.",
@ -59,5 +63,9 @@
"doubleSureAbort": "Are you double sure? Make sure they won't hate you forever!",
"questWarning": "If new players join the party before the quest starts, they will also receive an invitation. However once the quest has started, no new party members can join the quest.",
"bossRageTitle": "Rage",
"bossRageDescription": "When this bar fills, the boss will unleash a special attack!"
"bossRageDescription": "When this bar fills, the boss will unleash a special attack!",
"startAQuest": "START A QUEST",
"startQuest": "Start Quest",
"whichQuestStart": "Which quest do you want to start?",
"getMoreQuests": "Get more quests"
}

View file

@ -1203,6 +1203,12 @@ api.food =
_.each api.food, (food,key) ->
_.defaults food, {value: 1, key, notes: t('foodNotes')}
api.userCanOwnQuestCategories = [
'unlockable'
'gold'
'pet'
]
api.quests =
dilatory:

View file

@ -27,7 +27,6 @@ function dropPetsCurrentlyOwned(pets) {
return count;
}
function mountMasterProgress(mounts) {
var count = 0;
_(DROP_ANIMALS).each(function(animal) {
@ -51,9 +50,23 @@ function remainingGearInSet(userGear, set) {
return count;
}
function questsOfCategory(userQuests, category) {
var quests = _.filter(content.quests, function(quest) {
var categoryMatches = quest.category === category;
var hasQuest = userQuests[quest.key];
return categoryMatches && hasQuest;
});
var count = _.size(quests);
return count;
}
module.exports = {
beastMasterProgress: beastMasterProgress,
dropPetsCurrentlyOwned: dropPetsCurrentlyOwned,
mountMasterProgress: mountMasterProgress,
remainingGearInSet: remainingGearInSet
remainingGearInSet: remainingGearInSet,
questsOfCategory: questsOfCategory
};

View file

@ -38,7 +38,6 @@ module.exports = function(config) {
'test/spec/mocks/**/*.js',
"website/public/js/env.js",
"website/public/js/app.js",
"common/script/public/config.js",
"common/script/public/userServices.js",

View file

@ -129,7 +129,7 @@ describe "Party", ->
group = undefined
participating = []
notParticipating = []
before (done) ->
beforeEach (done) ->
# Tavern boss, side-by-side
Group.update(
_id: "habitrpg"
@ -310,6 +310,20 @@ describe "Party", ->
expect(_.size(res.body.quest.members)).to.equal 3
done()
it "allows quest participants to leave quest", (done) ->
leavingMember = party[1]
expect(group.quest.members[leavingMember._id]).to.eql(true)
request.post(baseURL + "/groups/" + group._id + "/questLeave")
.set("X-API-User", leavingMember._id)
.set("X-API-Key", leavingMember.apiToken)
.end (err, res) ->
expectCode res, 204
request.get(baseURL + '/groups/party')
.end (err, res) ->
expect(res.body.quest.members[leavingMember._id]).to.not.be.ok
done()
xit "Hurts the boss", (done) ->
request.post(baseURL + "/user/batch-update").end (res) ->
user = res.body

View file

@ -154,4 +154,23 @@ describe('count', function() {
expect(dropPets).to.eql(2);
});
});
describe('questsOfCategory', function() {
it('counts user quest scrolls of a particular category', function() {
var quests = {
"atom1": 2,
"whale": 4,
"kraken": 2,
"sheep": 1,
"goldenknight2": 1
};
var petQuestCount = count.questsOfCategory(quests, 'pet');
var unlockableQuestCount = count.questsOfCategory(quests, 'unlockable');
var goldQuestCount = count.questsOfCategory(quests, 'gold');
expect(petQuestCount).to.eql(3);
expect(unlockableQuestCount).to.eql(2);
expect(goldQuestCount).to.eql(0);
});
});
});

View file

@ -0,0 +1,382 @@
var sinon = require('sinon');
var chai = require("chai");
chai.use(require("sinon-chai"));
var expect = chai.expect;
var Q = require('q');
var Group = require('../../../website/src/models/group').model;
var groupsController = require('../../../website/src/controllers/groups');
describe('Groups Controller', function() {
var utils = require('../../../website/src/utils');
describe('#invite', function() {
var res, req, user, group;
beforeEach(function() {
group = {
_id: 'group-id',
name: 'group-name',
type: 'party',
members: [
'user-id',
'another-user'
],
save: sinon.stub().yields(),
markModified: sinon.spy()
};
user = {
_id: 'user-id',
name: 'inviter',
email: 'inviter@example.com',
save: sinon.stub().yields(),
markModified: sinon.spy()
};
res = {
locals: {
group: group,
user: user
},
json: sinon.stub(),
send: sinon.stub()
};
req = {
body: {}
};
});
context('uuids', function() {
beforeEach(function() {
req.body.uuids = ['invited-user'];
});
it('returns 400 if user not found');
it('returns a 400 if user is already in the group');
it('retuns 400 if user was already invited to that group');
it('returns 400 if user is already pending an invitation');
it('returns 400 is user is already in another party');
it('emails invited user');
it('does not email invited user if email preference is set to false');
});
context('emails', function() {
var EmailUnsubscription = require('../../../website/src/models/emailUnsubscription').model;
var execStub, selectStub;
beforeEach(function() {
sinon.stub(utils, 'encrypt').returns('http://link.com');
sinon.stub(utils, 'getUserInfo').returns({
name: user.name,
email: user.email
});
execStub = sinon.stub();
selectStub = sinon.stub().returns({
exec: execStub
});
sinon.stub(User, 'findOne').returns({
select: selectStub
});
sinon.stub(EmailUnsubscription, 'findOne');
sinon.stub(utils, 'txnEmail');
req.body.emails = [{email: 'user@example.com', name: 'user'}];
});
afterEach(function() {
User.findOne.restore();
EmailUnsubscription.findOne.restore();
utils.encrypt.restore();
utils.getUserInfo.restore();
utils.txnEmail.restore();
});
it('emails user with invite', function() {
execStub.yields(null, null);
EmailUnsubscription.findOne.yields(null, null);
groupsController.invite(req, res);
expect(utils.txnEmail).to.be.calledOnce;
expect(utils.txnEmail).to.be.calledWith(
{ email: 'user@example.com', name: 'user' },
'invite-friend',
[
{ name: 'LINK', content: '?partyInvite=http://link.com' },
{ name: 'INVITER', content: 'inviter' },
{ name: 'REPLY_TO_ADDRESS', content: 'inviter@example.com' }
]
);
});
it('does not email user if user is on unsubscribe list', function() {
EmailUnsubscription.findOne.yields(null, {_id: 'on-list'});
expect(utils.txnEmail).to.not.be.called;
});
it('checks if a user with provided email already exists');
});
context('others', function() {
it ('returns a 400 error', function() {
groupsController.invite(req, res);
expect(res.json).to.be.calledOnce;
expect(res.json).to.be.calledWith(
400,
{ err: 'Can invite only by email or uuid' }
);
});
});
});
describe('#leave', function() {
var res, req, user, group;
beforeEach(function() {
group = {
_id: 'group-id',
type: 'party',
members: [
'user-id',
'another-user'
],
save: sinon.stub().yields(),
markModified: sinon.spy()
};
user = {
_id: 'user-id',
save: sinon.stub().yields(),
markModified: sinon.spy()
};
res = {
locals: {
group: group,
user: user
},
json: sinon.stub(),
send: sinon.stub()
};
req = {
query: { keep: 'keep' }
};
});
context('party', function() {
beforeEach(function() {
group.type = 'party';
group.quest = {
leader : 'another-user',
active: true,
members: {
'user-id': true,
'another-user': true
},
key : 'vice1',
progress : {
hp : 364,
collect : {}
}
};
sinon.spy(Group, 'update');
});
afterEach(function() {
Group.update.restore();
});
it('prevents user from leaving party if quest is active', function() {
user.party = {
quest : {
key : 'vice1',
progress : {
up : 50,
down : 0,
collect : {}
},
completed : null,
RSVPNeeded : false
}
}
groupsController.leave(req, res);
expect(Group.update).to.not.be.called;
expect(res.json).to.be.calledOnce;
expect(res.json).to.be.calledWith(403, 'You cannot leave party during an active quest. Please leave the quest first');
});
it('leaves party if quest is not active', function() {
user.party = { quest: { key: null } };
groupsController.leave(req, res);
expect(Group.update).to.be.calledOnce;
expect(res.json).to.not.be.called;
});
});
});
describe('#questLeave', function() {
var res, req, group, user, saveSpy;
beforeEach(function() {
sinon.stub(Q, 'all').returns({
done: sinon.stub().yields()
});
group = {
_id: 'group-id',
type: 'party',
quest: {
leader : 'another-user',
active: true,
members: {
'user-id': true,
'another-user': true
},
key : 'vice1',
progress : {
hp : 364,
collect : {}
}
},
save: sinon.stub().yields(),
markModified: sinon.spy()
};
user = {
_id: 'user-id',
party : {
quest : {
key : 'vice1',
progress : {
up : 50,
down : 0,
collect : {}
},
completed : null,
RSVPNeeded : false
}
},
save: sinon.stub().yields(),
markModified: sinon.spy()
};
res = {
locals: {
group: group,
user: user
},
json: sinon.stub(),
send: sinon.stub()
};
req = { };
});
afterEach(function() {
Q.all.restore();
});
context('error conditions', function() {
it('errors if quest is not active', function() {
group.quest.active = false;
groupsController.questLeave(req, res);
expect(res.json).to.be.calledOnce;
expect(res.json).to.be.calledWith(
404,
{ err: 'No active quest to leave' }
);
});
it('errors if user is not part of quest', function() {
delete group.quest.members[user._id];
groupsController.questLeave(req, res);
expect(res.json).to.be.calledOnce;
expect(res.json).to.be.calledWith(
403,
{ err: 'You are not part of the quest' }
);
});
it('does not allow quest leader to leave quest', function() {
group.quest.leader = 'user-id';
groupsController.questLeave(req, res);
expect(res.json).to.be.calledOnce;
expect(res.json).to.be.calledWith(
403,
{ err: 'Quest leader cannot leave quest' }
);
});
it('sends 500 if group cannot save', function() {
Q.all.returns({
done: sinon.stub().callsArgWith(1, {err: 'save error'})
});
var nextSpy = sinon.spy();
groupsController.questLeave(req, res, nextSpy);
expect(res.json).to.not.be.called;
expect(nextSpy).to.be.calledOnce;
expect(nextSpy).to.be.calledWith({err: 'save error'});
});
});
context('success', function() {
it('removes user from quest', function() {
expect(group.quest.members[user._id]).to.exist;
groupsController.questLeave(req, res);
expect(group.quest.members[user._id]).to.not.exist;
});
it('scrubs quest data from user', function() {
user.party.quest.progress = {
up: 100,
down: 32,
collect: {
foo: 12,
bar: 4
}
};
groupsController.questLeave(req, res);
expect(user.party.quest.key).to.not.exist;
expect(user.party.quest.progress).to.eql({
up: 0,
down: 0,
collect: {}
});
});
it('sends back 204 on success', function() {
groupsController.questLeave(req, res);
expect(res.send).to.be.calledOnce;
expect(res.send).to.be.calledWith(204);
});
});
});
});

View file

@ -0,0 +1,194 @@
'use strict';
describe('Invite to Group Controller', function() {
var scope, ctrl, groups, user, guild, $rootScope;
beforeEach(function() {
user = specHelper.newUser({
profile: { name: 'Mario' }
});
module(function($provide) {
$provide.value('User', {});
$provide.value('injectedGroup', { user: user });
});
inject(function($rootScope, $controller, Groups){
scope = $rootScope.$new();
// Load RootCtrl to ensure shared behaviors are loaded
$controller('RootCtrl', {$scope: scope, User: {user: user}});
ctrl = $controller('InviteToGroupCtrl', {$scope: scope, User: {user: user}});
groups = Groups;
});
});
describe('addEmail', function() {
it('adds blank email to email list', function() {
scope.emails = [{name: 'Mario', email: 'mario@mushroomkingdom.com'}];
scope.addEmail();
expect(scope.emails).to.eql([{name: 'Mario', email: 'mario@mushroomkingdom.com'}, {name: '', email: ''}]);
});
});
describe('addUuid', function() {
it('adds blank uuid to invitees list', function() {
scope.invitees = [{uuid: 'user1'}];
scope.addUuid();
expect(scope.invitees).to.eql([{uuid: 'user1'}, {uuid: ''}]);
});
});
describe('inviteNewUsers', function() {
beforeEach(function() {
scope.group = specHelper.newGroup({
type: 'party',
$save: sinon.stub().returns({
then: function(cb) { cb(); }
})
});
sandbox.stub(groups.Group, 'invite');
});
context('if the party does not already exist', function() {
beforeEach(function() {
delete scope.group._id;
});
it('saves the group if a new group is being created', function() {
scope.inviteNewUsers('uuid');
expect(scope.group.$save).to.be.calledOnce;
});
it('uses provided name', function() {
scope.group.name = 'test party';
scope.inviteNewUsers('uuid');
expect(group.name).to.eql('test party');
});
it('names the group if no name is provided', function() {
scope.group.name = '';
scope.inviteNewUsers('uuid');
expect(group.name).to.eql(env.t('possessiveParty', {name: user.profile.name}));
});
});
context('email', function() {
it('invites user with emails', function() {
scope.emails = [
{name: 'Luigi', email: 'mario_bro@themushroomkingdom.com'},
{name: 'Mario', email: 'mario@tmk.com'}
];
scope.inviteNewUsers('email');
expect(groups.Group.invite).to.be.calledOnce;
expect(groups.Group.invite).to.be.calledWith({
gid: scope.group._id,
}, {
inviter: user.profile.name,
emails: [
{name: 'Luigi', email: 'mario_bro@themushroomkingdom.com'},
{name: 'Mario', email: 'mario@tmk.com'}
]
});
});
it('resets email list after sending', function() {
groups.Group.invite.yields();
scope.emails[0].name = 'Luigi';
scope.emails[0].email = 'mario_bro@themushroomkingdom.com';
scope.inviteNewUsers('email');
expect(scope.emails).to.eql([{name:'', email: ''},{name:'', email: ''}]);
});
it('filters out blank email inputs', function() {
scope.emails = [
{name: 'Luigi', email: 'mario_bro@themushroomkingdom.com'},
{name: 'Toad', email: ''},
{name: 'Mario', email: 'mario@tmk.com'}
];
scope.inviteNewUsers('email');
expect(groups.Group.invite).to.be.calledOnce;
expect(groups.Group.invite).to.be.calledWith({
gid: scope.group._id,
}, {
inviter: user.profile.name,
emails: [
{name: 'Luigi', email: 'mario_bro@themushroomkingdom.com'},
{name: 'Mario', email: 'mario@tmk.com'}
]
});
});
});
context('uuid', function() {
it('invites user with uuid', function() {
scope.invitees = [{uuid: '1234'}];
scope.inviteNewUsers('uuid');
expect(groups.Group.invite).to.be.calledOnce;
expect(groups.Group.invite).to.be.calledWith({
gid: scope.group._id,
}, {
uuids: ['1234']
});
});
it('invites users with uuids', function() {
scope.invitees = [{uuid: 'user1'}, {uuid: 'user2'}, {uuid: 'user3'}];
scope.inviteNewUsers('uuid');
expect(groups.Group.invite).to.be.calledOnce;
expect(groups.Group.invite).to.be.calledWith({
gid: scope.group._id,
}, {
uuids: ['user1', 'user2', 'user3']
});
});
it('resets invitee list after sending', function() {
groups.Group.invite.yields();
scope.invitees = [{uuid: 'user1'}, {uuid: 'user2'}, {uuid: 'user3'}];
scope.inviteNewUsers('uuid');
expect(scope.invitees).to.eql([{uuid: ''}]);
});
it('removes blank fields from being sent', function() {
groups.Group.invite.yields();
scope.invitees = [{uuid: 'user1'}, {uuid: ''}, {uuid: 'user3'}];
scope.inviteNewUsers('uuid');
expect(groups.Group.invite).to.be.calledOnce;
expect(groups.Group.invite).to.be.calledWith({
gid: scope.group._id,
}, {
uuids: ['user1', 'user3']
});
});
});
context('invalid invite method', function() {
it('logs error', function() {
sandbox.stub(console, 'log');
scope.inviteNewUsers();
expect(groups.Group.invite).to.not.be.called;
expect(console.log).to.be.calledOnce;
expect(console.log).to.be.calledWith('Invalid invite method.');
});
});
});
});

View file

@ -1,14 +1,14 @@
'use strict';
describe("Party Controller", function() {
var scope, ctrl, user, User, groups, $rootScope, $controller;
var scope, ctrl, user, User, groups, rootScope, $controller;
beforeEach(function() {
user = specHelper.newUser(),
user._id = "unique-user-id";
User = {
user: user,
sync: sinon.spy
sync: sandbox.spy
}
module(function($provide) {
@ -17,7 +17,7 @@ describe("Party Controller", function() {
inject(function(_$rootScope_, _$controller_, Groups){
$rootScope = _$rootScope_;
rootScope = _$rootScope_;
scope = _$rootScope_.$new();
@ -35,7 +35,7 @@ describe("Party Controller", function() {
describe('questAccept', function() {
it('calls Groups.questAccept', function() {
var party = {};
var groupSpy = sinon.stub(groups, "questAccept", function(){return true;});
var groupSpy = sandbox.stub(groups, "questAccept", function(){return true;});
scope.questAccept(party);
groupSpy.should.have.been.calledOnce;
});
@ -44,7 +44,7 @@ describe("Party Controller", function() {
describe('questReject', function() {
it('calls Groups.questReject', function() {
var party = {};
var groupSpy = sinon.stub(groups, "questReject", function(){return true;});
var groupSpy = sandbox.stub(groups, "questReject", function(){return true;});
scope.questReject(party);
groupSpy.should.have.been.calledOnce;
});
@ -54,7 +54,7 @@ describe("Party Controller", function() {
var party, cancelSpy, windowSpy;
beforeEach(function() {
party = {};
cancelSpy = sinon.stub(groups, "questCancel", function(){return true;});
cancelSpy = sandbox.stub(groups, "questCancel", function(){return true;});
});
afterEach(function() {
@ -63,7 +63,7 @@ describe("Party Controller", function() {
});
it('calls Groups.questCancel when alert box is confirmed', function() {
windowSpy = sinon.stub(window, "confirm", function(){return true});
windowSpy = sandbox.stub(window, "confirm", function(){return true});
scope.questCancel(party);
windowSpy.should.have.been.calledOnce;
@ -71,8 +71,8 @@ describe("Party Controller", function() {
cancelSpy.should.have.been.calledOnce;
});
it('does not call Groups.questCancel when alert box is confirmed', function() {
windowSpy = sinon.stub(window, "confirm", function(){return false});
it('does not call Groups.questCancel when alert box is not confirmed', function() {
windowSpy = sandbox.stub(window, "confirm", function(){return false});
scope.questCancel(party);
windowSpy.should.have.been.calledOnce;
@ -84,7 +84,7 @@ describe("Party Controller", function() {
var party, abortSpy, windowSpy;
beforeEach(function() {
party = {};
abortSpy = sinon.stub(groups, "questAbort", function(){return true;});
abortSpy = sandbox.stub(groups, "questAbort", function(){return true;});
});
afterEach(function() {
@ -93,7 +93,7 @@ describe("Party Controller", function() {
});
it('calls Groups.questAbort when two alert boxes are confirmed', function() {
windowSpy = sinon.stub(window, "confirm", function(){return true});
windowSpy = sandbox.stub(window, "confirm", function(){return true});
scope.questAbort(party);
windowSpy.should.have.been.calledTwice;
@ -103,7 +103,7 @@ describe("Party Controller", function() {
});
it('does not call Groups.questAbort when first alert box is not confirmed', function() {
windowSpy = sinon.stub(window, "confirm", function(){return false});
windowSpy = sandbox.stub(window, "confirm", function(){return false});
scope.questAbort(party);
windowSpy.should.have.been.calledOnce;
@ -115,7 +115,7 @@ describe("Party Controller", function() {
it('does not call Groups.questAbort when first alert box is confirmed but second one is not', function() {
// Hack to confirm first window, but not second
var shouldReturn = false;
windowSpy = sinon.stub(window, "confirm", function(){
windowSpy = sandbox.stub(window, "confirm", function(){
shouldReturn = !shouldReturn;
return shouldReturn;
});
@ -127,4 +127,118 @@ describe("Party Controller", function() {
abortSpy.should.not.have.been.calledOnce;
});
});
describe('#questLeave', function() {
var party, leaveSpy, windowSpy;
beforeEach(function() {
party = {};
scope.group = {
quest: { members: { 'user-id': true } }
};
leaveSpy = sandbox.stub(groups, 'questLeave').returns({
then: sandbox.stub().yields()
});
});
it('calls Groups.questLeave when alert box is confirmed', function() {
windowSpy = sandbox.stub(window, "confirm").returns(true);
scope.questLeave(party);
windowSpy.should.have.been.calledOnce;
windowSpy.should.have.been.calledWith(window.env.t('sureLeave'));
leaveSpy.should.have.been.calledOnce;
});
it('does not call Groups.questLeave when alert box is not confirmed', function() {
windowSpy = sandbox.stub(window, "confirm").returns(false);
scope.questLeave(party);
windowSpy.should.have.been.calledOnce;
leaveSpy.should.not.have.been.calledOnce;
});
});
describe('clickStartQuest', function() {
beforeEach(function() {
sandbox.stub(rootScope, 'openModal');
sandbox.stub(rootScope.$state, 'go');
});
it('opens quest modal if user has a quest', function() {
user.items.quests = {
whale: 1
};
scope.clickStartQuest();
expect(rootScope.$state.go).to.not.be.called;
expect(rootScope.openModal).to.be.calledOnce;
expect(rootScope.openModal).to.be.calledWith(
'ownedQuests',
{ controller: 'InventoryCtrl' }
);
});
it('does not open modal if user has no quests', function() {
user.items.quests = { };
scope.clickStartQuest();
expect(rootScope.openModal).to.not.be.called;
expect(rootScope.$state.go).to.be.calledOnce;
expect(rootScope.$state.go).to.be.calledWith('options.inventory.quests');
});
it('does not open modal if user had quests previously, but does not now', function() {
user.items.quests = {
whale: 0,
atom1: 0
};
scope.clickStartQuest();
expect(rootScope.openModal).to.not.be.called;
expect(rootScope.$state.go).to.be.calledOnce;
expect(rootScope.$state.go).to.be.calledWith('options.inventory.quests');
});
});
describe('#leaveOldPartyAndJoinNewParty', function() {
beforeEach(function() {
sandbox.stub(scope, 'join');
sandbox.stub(groups.Group, 'leave').yields();
sandbox.stub(groups, 'party').returns({
_id: 'old-party'
});
sandbox.stub(window, 'confirm').returns(true);
});
it('does nothing if user declines confirmation', function() {
window.confirm.returns(false);
scope.leaveOldPartyAndJoinNewParty('some-id', 'some-name');
expect(groups.Group.leave).to.not.be.called;
})
it('leaves user\'s current party', function() {
scope.leaveOldPartyAndJoinNewParty('some-id', 'some-name');
expect(groups.Group.leave).to.be.calledOnce;
expect(groups.Group.leave).to.be.calledWith({
gid: 'old-party',
keep: false
});
});
it('joins the new party', function() {
scope.leaveOldPartyAndJoinNewParty('some-id', 'some-name');
expect(scope.join).to.be.calledOnce;
expect(scope.join).to.be.calledWith({
id: 'some-id',
name: 'some-name'
});
});
});
});

View file

@ -41,99 +41,65 @@ describe('groupServices', function() {
});
context('quest function wrappers', function() {
var successPromise = function() {
return {
then: function(success, failure) {
success();
}
}
}
var successParty = {
$questAccept: successPromise,
$questReject: successPromise,
$questCancel: successPromise,
$questAbort: successPromise
}
var failPromise = function() {
return {
then: function(success, failure) {
failure('fail');
}
}
}
var failParty = {
$questAccept: failPromise,
$questReject: failPromise,
$questCancel: failPromise,
$questAbort: failPromise
}
var successPromise, failPromise;
beforeEach(function() {
sinon.spy(user, 'sync');
sinon.stub(console, 'log', function(arg) { return true; });
});
sandbox.spy(user, 'sync');
sandbox.stub(console, 'log');
afterEach(function() {
user.sync.restore();
console.log.restore();
});
describe('questAccept', function() {
it('syncs user if $questAccept succeeds', function() {
groups.questAccept(successParty);
user.sync.should.have.been.calledOnce;
successPromise = sandbox.stub().returns({
then: function(success, failure) {
return success();
}
});
it('does not sync user if $questAccept fails', function() {
groups.questAccept(failParty);
user.sync.should.not.have.been.calledOnce;
console.log.should.have.been.calledWith('fail');
failPromise = sandbox.stub().returns({
then: function(success, failure) {
return failure('fail');
}
});
});
describe('questReject', function() {
it('syncs user if $questReject succeeds', function() {
groups.questReject(successParty);
user.sync.should.have.been.calledOnce;
console.log.should.not.have.been.called;
});
var questFunctions = [
'questAccept',
'questReject',
'questCancel',
'questAbort',
'questLeave'
];
it('does not sync user if $questReject fails', function() {
groups.questReject(failParty);
user.sync.should.not.have.been.calledOnce;
console.log.should.have.been.calledWith('fail');
});
});
for (var i in questFunctions) {
var questFunc = questFunctions[i];
describe('questCancel', function() {
it('syncs user if $questCancel succeeds', function() {
groups.questCancel(successParty);
user.sync.should.have.been.calledOnce;
console.log.should.not.have.been.called;
});
describe('#' + questFunc, function() {
it('calls party.$' + questFunc, function() {
var party = { };
party['$' + questFunc] = successPromise;
it('does not sync user if $questCancel fails', function() {
groups.questCancel(failParty);
user.sync.should.not.have.been.calledOnce;
console.log.should.have.been.calledWith('fail');
});
});
groups[questFunc](party);
describe('questAbort', function() {
it('syncs user if $questAbort succeeds', function() {
groups.questAbort(successParty);
user.sync.should.have.been.calledOnce;
console.log.should.not.have.been.called;
});
expect(party['$' + questFunc]).to.be.calledOnce;
});
it('does not sync user if $questAbort fails', function() {
groups.questAbort(failParty);
user.sync.should.not.have.been.calledOnce;
console.log.should.have.been.calledWith('fail');
it('syncs user if $' + questFunc + ' succeeds', function() {
var successParty = { };
successParty['$' + questFunc] = successPromise;
groups[questFunc](successParty);
user.sync.should.have.been.calledOnce;
});
it('does not sync user if $' + questFunc + ' fails', function() {
var failParty = { };
failParty['$' + questFunc] = failPromise;
groups[questFunc](failParty);
user.sync.should.not.have.been.calledOnce;
console.log.should.have.been.calledWith('fail');
});
});
});
}
});
});

View file

@ -67,3 +67,6 @@ li.spaced
.flex-column
display: flex
flex-direction: column
.row-margin
margin: auto auto 1em auto

View file

@ -0,0 +1,73 @@
'use strict';
habitrpg.controller('AutocompleteCtrl', ['$scope', '$timeout', 'Groups', 'User', 'InputCaret', function ($scope,$timeout,Groups,User,InputCaret) {
$scope.clearUserlist = function() {
$scope.response = [];
$scope.usernames = [];
}
$scope.filterUser = function(msg) {
if (!$scope.query || !msg.user) {
return false;
}
// Ignore casing when checking for username
var user = msg.user.toLowerCase();
var text = $scope.query.text.toLowerCase();
return user.indexOf(text) == 0;
}
$scope.performCompletion = function(msg) {
$scope.autoComplete(msg);
$scope.query = null;
}
$scope.addNewUser = function(user) {
if($.inArray(user.user,$scope.usernames) == -1) {
user.username = user.user;
$scope.usernames.push(user.user);
$scope.response.push(user);
}
}
$scope.clearUserlist();
$scope.chatChanged = function(newvalue,oldvalue){
if($scope.group && $scope.group.chat && $scope.group.chat.length > 0){
for(var i = 0; i < $scope.group.chat.length; i++) {
$scope.addNewUser($scope.group.chat[i]);
}
}
}
$scope.$watch('group.chat',$scope.chatChanged);
$scope.caretChanged = function(newCaretPos) {
var relativeelement = $('.chat-form div:first');
var textarea = $('.chat-form textarea');
var userlist = $('.list-at-user');
var offset = {
x: textarea.offset().left - relativeelement.offset().left,
y: textarea.offset().top - relativeelement.offset().top,
};
if(relativeelement) {
var caretOffset = InputCaret.getPosition(textarea);
userlist.css({
left: caretOffset.left + offset.x,
top: caretOffset.top + offset.y + 16
});
}
}
$scope.updateTimer = false;
$scope.$watch(function () { return $scope.caretPos; },function(newCaretPos) {
if($scope.updateTimer){
$timeout.cancel($scope.updateTimer)
}
$scope.updateTimer = $timeout(function(){
$scope.caretChanged(newCaretPos);
},$scope.watchDelay)
});
}]);

View file

@ -0,0 +1,139 @@
'use strict';
habitrpg.controller('ChatCtrl', ['$scope', 'Groups', 'Chat', 'User', '$http', 'ApiUrl', 'Notification', 'Members', '$rootScope', 'Analytics',
function($scope, Groups, Chat, User, $http, ApiUrl, Notification, Members, $rootScope, Analytics){
$scope.message = {content:''};
$scope._sending = false;
$scope.isUserMentioned = function(user, message) {
if(message.hasOwnProperty("highlight"))
return message.highlight;
message.highlight = false;
var messagetext = message.text.toLowerCase();
var username = user.profile.name;
var mentioned = messagetext.indexOf(username.toLowerCase());
var pattern = username+"([^\w]|$){1}";
if(mentioned > -1) {
var preceedingchar = messagetext.substring(mentioned-1,mentioned);
if(mentioned == 0 || preceedingchar.trim() == '' || preceedingchar == '@'){
var regex = new RegExp(pattern,'i');
message.highlight = regex.test(messagetext);
}
}
return message.highlight;
}
$scope.postChat = function(group, message){
if (_.isEmpty(message) || $scope._sending) return;
$scope._sending = true;
var previousMsg = (group.chat && group.chat[0]) ? group.chat[0].id : false;
Chat.utils.postChat({gid: group._id, message:message, previousMsg: previousMsg}, undefined, function(data){
if(data.chat){
group.chat = data.chat;
}else if(data.message){
group.chat.unshift(data.message);
}
$scope.message.content = '';
$scope._sending = false;
if (group.type == 'party') {
Analytics.updateUser({'partyID':group.id,'partySize':group.memberCount});
}
if (group.privacy == 'public'){
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'group chat','groupType':group.type,'privacy':group.privacy,'groupName':group.name,'message':message});
} else {
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'group chat','groupType':group.type,'privacy':group.privacy});
}
}, function(err){
$scope._sending = false;
});
}
$scope.deleteChatMessage = function(group, message){
if(message.uuid === User.user.id || (User.user.backer && User.user.contributor.admin)){
var previousMsg = (group.chat && group.chat[0]) ? group.chat[0].id : false;
if(confirm('Are you sure you want to delete this message?')){
Chat.utils.deleteChatMessage({gid:group._id, messageId:message.id, previousMsg:previousMsg}, undefined, function(data){
if(data.chat) group.chat = data.chat;
var i = _.findIndex(group.chat, {id: message.id});
if(i !== -1) group.chat.splice(i, 1);
});
}
}
}
$scope.likeChatMessage = function(group,message) {
if (message.uuid == User.user._id)
return Notification.text(window.env.t('foreverAlone'));
if (!message.likes) message.likes = {};
if (message.likes[User.user._id]) {
delete message.likes[User.user._id];
} else {
message.likes[User.user._id] = true;
}
Chat.utils.like({ gid:group._id, messageId:message.id }, undefined);
}
$scope.flagChatMessage = function(groupId,message) {
if(!message.flags) message.flags = {};
if(message.flags[User.user._id])
Notification.text(window.env.t('abuseAlreadyReported'));
else {
$scope.abuseObject = message;
$scope.groupId = groupId;
Members.selectMember(message.uuid, function(){
$rootScope.openModal('abuse-flag',{
controller:'MemberModalCtrl',
scope: $scope
});
});
}
};
$scope.copyToDo = function(message) {
var taskNotes = env.t("messageWroteIn", {
user: message.uuid == 'system'
? 'system'
: '[' + message.user + '](' + env.BASE_URL + '/static/front/#?memberId=' + message.uuid + ')',
group: '[' + $scope.group.name + '](' + window.location.href + ')'
});
var newScope = $scope.$new();
newScope.text = message.text;
newScope.notes = taskNotes;
$rootScope.openModal('copyChatToDo',{
controller:'CopyMessageModalCtrl',
scope: newScope
});
};
$scope.sync = function(group){
if(group.type == 'party') {
group.$syncParty(); // Syncs the whole party, not just 15 members
} else {
group.$get();
}
// When the user clicks fetch recent messages we need to update
// that the user has seen the new messages
Chat.seenMessage(group._id);
}
// List of Ordering options for the party members list
$scope.partyOrderChoices = {
'level': window.env.t('sortLevel'),
'random': window.env.t('sortRandom'),
'pets': window.env.t('sortPets'),
'habitrpg_date_joined' : window.env.t('sortHabitrpgJoined'),
'party_date_joined': window.env.t('sortJoined'),
'habitrpg_last_logged_in': window.env.t('sortHabitrpgLastLoggedIn'),
'name': window.env.t('sortName'),
'backgrounds': window.env.t('sortBackgrounds'),
};
$scope.partyOrderAscendingChoices = {
'ascending': window.env.t('ascendingSort'),
'descending': window.env.t('descendingSort')
}
}]);

View file

@ -0,0 +1,17 @@
'use strict';
habitrpg.controller("CopyMessageModalCtrl", ['$scope', 'User', 'Notification',
function($scope, User, Notification){
$scope.saveTodo = function() {
var newTask = {
text: $scope.text,
type: 'todo',
notes: $scope.notes
};
User.user.ops.addTask({body:newTask});
Notification.text(window.env.t('messageAddedAsToDo'));
$scope.$close();
}
}]);

View file

@ -7,13 +7,13 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
if (!group.quest || !group.quest.members) return false;
if (group.quest.active) return false; // quest is started, not pending
return userid in group.quest.members && group.quest.members[userid] != false;
}
};
$scope.isMemberOfRunningQuest = function(userid, group) {
if (!group.quest || !group.quest.members) return false;
if (!group.quest.active) return false; // quest is pending, not started
return group.quest.members[userid];
}
};
$scope.isMemberOfGroup = function(userid, group){
@ -32,11 +32,11 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
if (!group.members) return false;
var memberIds = _.map(group.members, function(x){return x._id});
return ~(memberIds.indexOf(userid));
}
};
$scope.isMember = function(user, group){
return ~(group.members.indexOf(user._id));
}
};
$scope.Members = Members;
$scope._editing = {group:false};
@ -45,13 +45,13 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
if(group._newLeader && group._newLeader._id) group.leader = group._newLeader._id;
group.$save();
group._editing = false;
}
};
$scope.deleteAllMessages = function() {
if (confirm(window.env.t('confirmDeleteAllMessages'))) {
User.user.ops.clearPMs({});
}
}
};
// ------ Modals ------
@ -69,7 +69,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$rootScope.openModal('member', {controller:'MemberModalCtrl', windowClass:'profile-modal', size:'lg'});
});
}
}
};
$scope.removeMember = function(group, member, isMember){
@ -80,7 +80,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
isMember: isMember
};
$rootScope.openModal('remove-member', {scope: $scope});
}
};
$scope.confirmRemoveMember = function(confirm){
if(confirm){
@ -100,31 +100,21 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
}else{
$scope.removeMemberData = undefined;
}
}
$scope.openInviteModal = function(group){
$rootScope.openModal('invite-friends', {controller:'InviteToGroupCtrl', resolve:
{injectedGroup: function(){
return group;
}}});
};
//var serializeQs = function(obj, prefix){
// var str = [];
// for(var p in obj) {
// if (obj.hasOwnProperty(p)) {
// var k = prefix ? prefix + "[" + p + "]" : p, v = obj[p];
// str.push(typeof v == "object" ?
// serializeQs(v, k) :
// encodeURIComponent(k) + "=" + encodeURIComponent(v));
// }
// }
// return str.join("&");
//}
//
//$scope.inviteLink = function(obj){
// return window.env.BASE_URL + '?' + serializeQs({partyInvite: obj});
//}
$scope.openInviteModal = function(group){
if (group.type !== 'party' && group.type !== 'guild') {
return console.log('Invalid group type.')
}
$rootScope.openModal('invite-' + group.type, {
controller:'InviteToGroupCtrl',
resolve: {
injectedGroup: function(){
return group;
}
}
});
};
$scope.quickReply = function(uid) {
Members.selectMember(uid, function(){
@ -132,457 +122,4 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
});
}
}])
.controller('InviteToGroupCtrl', ['$scope', 'User', 'Groups', 'injectedGroup', '$http', 'Notification', function($scope, User, Groups, injectedGroup, $http, Notification){
$scope.group = injectedGroup;
$scope.inviter = User.user.profile.name;
$scope.emails = [{name:"",email:""},{name:"",email:""}];
$scope.inviteEmails = function(){
Groups.Group.invite({gid: $scope.group._id}, {inviter: $scope.inviter, emails: $scope.emails}, function(){
Notification.text("Invitation(s) sent!");
$scope.emails = [{name:'',email:''},{name:'',email:''}];
}, function(){
$scope.emails = [{name:'',email:''},{name:'',email:''}];
});
};
$scope.invite = function(){
Groups.Group.invite({gid: $scope.group._id}, {uuids: [$scope.invitee]}, function(){
Notification.text("Invitation(s) sent!");
$scope.invitee = '';
}, function(){
$scope.invitee = '';
});
};
}])
.controller('AutocompleteCtrl', ['$scope', '$timeout', 'Groups', 'User', 'InputCaret', function ($scope,$timeout,Groups,User,InputCaret) {
$scope.clearUserlist = function() {
$scope.response = [];
$scope.usernames = [];
}
$scope.filterUser = function(msg) {
if (!$scope.query || !msg.user) {
return false;
}
// Ignore casing when checking for username
var user = msg.user.toLowerCase();
var text = $scope.query.text.toLowerCase();
return user.indexOf(text) == 0;
}
$scope.performCompletion = function(msg) {
$scope.autoComplete(msg);
$scope.query = null;
}
$scope.addNewUser = function(user) {
if($.inArray(user.user,$scope.usernames) == -1) {
user.username = user.user;
$scope.usernames.push(user.user);
$scope.response.push(user);
}
}
$scope.clearUserlist();
$scope.chatChanged = function(newvalue,oldvalue){
if($scope.group && $scope.group.chat && $scope.group.chat.length > 0){
for(var i = 0; i < $scope.group.chat.length; i++) {
$scope.addNewUser($scope.group.chat[i]);
}
}
}
$scope.$watch('group.chat',$scope.chatChanged);
$scope.caretChanged = function(newCaretPos) {
var relativeelement = $('.chat-form div:first');
var textarea = $('.chat-form textarea');
var userlist = $('.list-at-user');
var offset = {
x: textarea.offset().left - relativeelement.offset().left,
y: textarea.offset().top - relativeelement.offset().top,
};
if(relativeelement) {
var caretOffset = InputCaret.getPosition(textarea);
userlist.css({
left: caretOffset.left + offset.x,
top: caretOffset.top + offset.y + 16
});
}
}
$scope.updateTimer = false;
$scope.$watch(function () { return $scope.caretPos; },function(newCaretPos) {
if($scope.updateTimer){
$timeout.cancel($scope.updateTimer)
}
$scope.updateTimer = $timeout(function(){
$scope.caretChanged(newCaretPos);
},$scope.watchDelay)
});
}])
.controller('ChatCtrl', ['$scope', 'Groups', 'Chat', 'User', '$http', 'ApiUrl', 'Notification', 'Members', '$rootScope', 'Analytics',
function($scope, Groups, Chat, User, $http, ApiUrl, Notification, Members, $rootScope, Analytics){
$scope.message = {content:''};
$scope._sending = false;
$scope.isUserMentioned = function(user, message) {
if(message.hasOwnProperty("highlight"))
return message.highlight;
message.highlight = false;
var messagetext = message.text.toLowerCase();
var username = user.profile.name;
var mentioned = messagetext.indexOf(username.toLowerCase());
var pattern = username+"([^\w]|$){1}";
if(mentioned > -1) {
var preceedingchar = messagetext.substring(mentioned-1,mentioned);
if(mentioned == 0 || preceedingchar.trim() == '' || preceedingchar == '@'){
var regex = new RegExp(pattern,'i');
message.highlight = regex.test(messagetext);
}
}
return message.highlight;
}
$scope.postChat = function(group, message){
if (_.isEmpty(message) || $scope._sending) return;
$scope._sending = true;
var previousMsg = (group.chat && group.chat[0]) ? group.chat[0].id : false;
Chat.utils.postChat({gid: group._id, message:message, previousMsg: previousMsg}, undefined, function(data){
if(data.chat){
group.chat = data.chat;
}else if(data.message){
group.chat.unshift(data.message);
}
$scope.message.content = '';
$scope._sending = false;
if (group.type == 'party') {
Analytics.updateUser({'partyID':group.id,'partySize':group.memberCount});
}
if (group.privacy == 'public'){
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'group chat','groupType':group.type,'privacy':group.privacy,'groupName':group.name,'message':message});
} else {
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'group chat','groupType':group.type,'privacy':group.privacy});
}
}, function(err){
$scope._sending = false;
});
}
$scope.deleteChatMessage = function(group, message){
if(message.uuid === User.user.id || (User.user.backer && User.user.contributor.admin)){
var previousMsg = (group.chat && group.chat[0]) ? group.chat[0].id : false;
if(confirm('Are you sure you want to delete this message?')){
Chat.utils.deleteChatMessage({gid:group._id, messageId:message.id, previousMsg:previousMsg}, undefined, function(data){
if(data.chat) group.chat = data.chat;
var i = _.findIndex(group.chat, {id: message.id});
if(i !== -1) group.chat.splice(i, 1);
});
}
}
}
$scope.likeChatMessage = function(group,message) {
if (message.uuid == User.user._id)
return Notification.text(window.env.t('foreverAlone'));
if (!message.likes) message.likes = {};
if (message.likes[User.user._id]) {
delete message.likes[User.user._id];
} else {
message.likes[User.user._id] = true;
}
Chat.utils.like({ gid:group._id, messageId:message.id }, undefined);
}
$scope.flagChatMessage = function(groupId,message) {
if(!message.flags) message.flags = {};
if(message.flags[User.user._id])
Notification.text(window.env.t('abuseAlreadyReported'));
else {
$scope.abuseObject = message;
$scope.groupId = groupId;
Members.selectMember(message.uuid, function(){
$rootScope.openModal('abuse-flag',{
controller:'MemberModalCtrl',
scope: $scope
});
});
}
};
$scope.copyToDo = function(message) {
var taskNotes = env.t("messageWroteIn", {
user: message.uuid == 'system'
? 'system'
: '[' + message.user + '](' + env.BASE_URL + '/static/front/#?memberId=' + message.uuid + ')',
group: '[' + $scope.group.name + '](' + window.location.href + ')'
});
var newScope = $scope.$new();
newScope.text = message.text;
newScope.notes = taskNotes;
$rootScope.openModal('copyChatToDo',{
controller:'CopyMessageModalCtrl',
scope: newScope
});
};
$scope.sync = function(group){
if(group.type == 'party') {
group.$syncParty(); // Syncs the whole party, not just 15 members
} else {
group.$get();
}
// When the user clicks fetch recent messages we need to update
// that the user has seen the new messages
Chat.seenMessage(group._id);
}
// List of Ordering options for the party members list
$scope.partyOrderChoices = {
'level': window.env.t('sortLevel'),
'random': window.env.t('sortRandom'),
'pets': window.env.t('sortPets'),
'habitrpg_date_joined' : window.env.t('sortHabitrpgJoined'),
'party_date_joined': window.env.t('sortJoined'),
'habitrpg_last_logged_in': window.env.t('sortHabitrpgLastLoggedIn'),
'name': window.env.t('sortName'),
'backgrounds': window.env.t('sortBackgrounds'),
};
$scope.partyOrderAscendingChoices = {
'ascending': window.env.t('ascendingSort'),
'descending': window.env.t('descendingSort')
}
}])
.controller("GuildsCtrl", ['$scope', 'Groups', 'User', 'Challenges', '$rootScope', '$state', '$location', '$compile', 'Analytics',
function($scope, Groups, User, Challenges, $rootScope, $state, $location, $compile, Analytics) {
$scope.groups = {
guilds: Groups.myGuilds(),
"public": Groups.publicGuilds()
}
$scope.type = 'guild';
$scope.text = window.env.t('guild');
var newGroup = function(){
return new Groups.Group({type:'guild', privacy:'private'});
}
$scope.newGroup = newGroup()
$scope.create = function(group){
if (User.user.balance < 1)
return $rootScope.openModal('buyGems', {track:"Gems > Create Group"});
if (confirm(window.env.t('confirmGuild'))) {
group.$save(function(saved){
if (saved.privacy == 'public') {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':true,'groupType':'guild','privacy':saved.privacy,'groupName':saved.name})}
else {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':true,'groupType':'guild','privacy':saved.privacy})}
$rootScope.hardRedirect('/#/options/groups/guilds/' + saved._id);
});
}
}
$scope.join = function(group){
// If we're accepting an invitation, we don't have the actual group object, but a faux group object (for performance
// purposes) {id, name}. Let's trick ngResource into thinking we have a group, so we can call the same $join
// function (server calls .attachGroup(), which finds group by _id and handles this properly)
if (group.id && !group._id) {
group = new Groups.Group({_id:group.id});
}
group.$join(function(joined){
if (joined.privacy == 'public') {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':false,'groupType':'guild','privacy':joined.privacy,'groupName':joined.name})}
else {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':false,'groupType':'guild','privacy':joined.privacy})}
$rootScope.hardRedirect('/#/options/groups/guilds/' + joined._id);
})
}
$scope.leave = function(keep) {
if (keep == 'cancel') {
$scope.selectedGroup = undefined;
$scope.popoverEl.popover('destroy');
} else {
Groups.Group.leave({gid: $scope.selectedGroup._id, keep:keep}, undefined, function(){
$rootScope.hardRedirect('/#/options/groups/guilds');
});
}
}
$scope.clickLeave = function(group, $event){
$scope.selectedGroup = group;
$scope.popoverEl = $($event.target);
var html, title;
Challenges.Challenge.query(function(challenges) {
challenges = _.pluck(_.filter(challenges, function(c) {
return c.group._id == group._id;
}), '_id');
if (_.intersection(challenges, User.user.challenges).length > 0) {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'remove-all\')">' + window.env.t('removeTasks') + '</a><br/>\n<a ng-click="leave(\'keep-all\')">' + window.env.t('keepTasks') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leaveGroupCha');
} else {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'keep-all\')">' + window.env.t('confirm') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leaveGroup')
}
$scope.popoverEl.popover('destroy').popover({
html: true,
placement: 'top',
trigger: 'manual',
title: title,
content: html
}).popover('show');
});
}
$scope.reject = function(guild){
var i = _.findIndex(User.user.invitations.guilds, {id:guild.id});
if (~i){
User.user.invitations.guilds.splice(i, 1);
User.set({'invitations.guilds':User.user.invitations.guilds});
}
}
}
])
.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','Challenges','$state','$compile','Analytics','Quests',
function($rootScope,$scope,Groups,Chat,User,Challenges,$state,$compile,Analytics,Quests) {
$scope.type = 'party';
$scope.text = window.env.t('party');
$scope.group = $rootScope.party = Groups.party();
$scope.newGroup = new Groups.Group({type:'party'});
$scope.inviteOrStartParty = Groups.inviteOrStartParty;
$scope.questInit = Quests.questInit;
if ($state.is('options.social.party')) {
$scope.group.$syncParty(); // Sync party automatically when navigating to party page
}
Chat.seenMessage($scope.group._id);
$scope.create = function(group){
group.$save(function(){
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':true,'groupType':'party','privacy':'private'});
Analytics.updateUser({'partyID':group.id,'partySize':1});
$rootScope.hardRedirect('/#/options/groups/party');
});
};
$scope.join = function(party){
var group = new Groups.Group({_id: party.id, name: party.name});
group.$join(function(){
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':false,'groupType':'party','privacy':'private'});
Analytics.updateUser({'partyID':party.id});
$rootScope.hardRedirect('/#/options/groups/party');
});
};
// TODO: refactor guild and party leave into one function
$scope.leave = function(keep) {
if (keep == 'cancel') {
$scope.selectedGroup = undefined;
$scope.popoverEl.popover('destroy');
} else {
Groups.Group.leave({gid: $scope.selectedGroup._id, keep:keep}, undefined, function(){
Analytics.updateUser({'partySize':null,'partyID':null});
$rootScope.hardRedirect('/#/options/groups/party');
});
}
};
// TODO: refactor guild and party clickLeave into one function
$scope.clickLeave = function(group, $event){
$scope.selectedGroup = group;
$scope.popoverEl = $($event.target);
var html, title;
Challenges.Challenge.query(function(challenges) {
challenges = _.pluck(_.filter(challenges, function(c) {
return c.group._id == group._id;
}), '_id');
if (_.intersection(challenges, User.user.challenges).length > 0) {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'remove-all\')">' + window.env.t('removeTasks') + '</a><br/>\n<a ng-click="leave(\'keep-all\')">' + window.env.t('keepTasks') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leavePartyCha');
} else {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'keep-all\')">' + window.env.t('confirm') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leaveParty');
}
$scope.popoverEl.popover('destroy').popover({
html: true,
placement: 'top',
trigger: 'manual',
title: title,
content: html
}).popover('show');
});
}
$scope.reject = function(){
//User.user.invitations.party = undefined;
User.set({'invitations.party':{}});
}
$scope.questCancel = function(party){
if (!confirm(window.env.t('sureCancel'))) return;
Groups.questCancel(party);
}
$scope.questAbort = function(party){
if (!confirm(window.env.t('sureAbort'))) return;
if (!confirm(window.env.t('doubleSureAbort'))) return;
Groups.questAbort(party);
}
$scope.questAccept = function(party){
Groups.questAccept(party);
};
$scope.questReject = function(party){
Groups.questReject(party);
}
}
])
.controller("TavernCtrl", ['$scope', 'Groups', 'User',
function($scope, Groups, User) {
$scope.group = Groups.tavern();
$scope.toggleUserTier = function($event) {
$($event.target).next().toggle();
}
}
])
.controller("CopyMessageModalCtrl", ['$scope', 'User', 'Notification',
function($scope, User, Notification){
$scope.saveTodo = function() {
var newTask = {
text: $scope.text,
type: 'todo',
notes: $scope.notes
};
User.user.ops.addTask({body:newTask});
Notification.text(window.env.t('messageAddedAsToDo'));
$scope.$close();
}
}
]);
}]);

View file

@ -0,0 +1,92 @@
'use strict';
habitrpg.controller("GuildsCtrl", ['$scope', 'Groups', 'User', 'Challenges', '$rootScope', '$state', '$location', '$compile', 'Analytics',
function($scope, Groups, User, Challenges, $rootScope, $state, $location, $compile, Analytics) {
$scope.groups = {
guilds: Groups.myGuilds(),
"public": Groups.publicGuilds()
}
$scope.type = 'guild';
$scope.text = window.env.t('guild');
var newGroup = function(){
return new Groups.Group({type:'guild', privacy:'private'});
}
$scope.newGroup = newGroup()
$scope.create = function(group){
if (User.user.balance < 1)
return $rootScope.openModal('buyGems', {track:"Gems > Create Group"});
if (confirm(window.env.t('confirmGuild'))) {
group.$save(function(saved){
if (saved.privacy == 'public') {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':true,'groupType':'guild','privacy':saved.privacy,'groupName':saved.name})}
else {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':true,'groupType':'guild','privacy':saved.privacy})}
$rootScope.hardRedirect('/#/options/groups/guilds/' + saved._id);
});
}
}
$scope.join = function(group){
// If we're accepting an invitation, we don't have the actual group object, but a faux group object (for performance
// purposes) {id, name}. Let's trick ngResource into thinking we have a group, so we can call the same $join
// function (server calls .attachGroup(), which finds group by _id and handles this properly)
if (group.id && !group._id) {
group = new Groups.Group({_id:group.id});
}
group.$join(function(joined){
if (joined.privacy == 'public') {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':false,'groupType':'guild','privacy':joined.privacy,'groupName':joined.name})}
else {Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':false,'groupType':'guild','privacy':joined.privacy})}
$rootScope.hardRedirect('/#/options/groups/guilds/' + joined._id);
})
}
$scope.leave = function(keep) {
if (keep == 'cancel') {
$scope.selectedGroup = undefined;
$scope.popoverEl.popover('destroy');
} else {
Groups.Group.leave({gid: $scope.selectedGroup._id, keep:keep}, undefined, function(){
$rootScope.hardRedirect('/#/options/groups/guilds');
});
}
}
$scope.clickLeave = function(group, $event){
$scope.selectedGroup = group;
$scope.popoverEl = $($event.target);
var html, title;
Challenges.Challenge.query(function(challenges) {
challenges = _.pluck(_.filter(challenges, function(c) {
return c.group._id == group._id;
}), '_id');
if (_.intersection(challenges, User.user.challenges).length > 0) {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'remove-all\')">' + window.env.t('removeTasks') + '</a><br/>\n<a ng-click="leave(\'keep-all\')">' + window.env.t('keepTasks') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leaveGroupCha');
} else {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'keep-all\')">' + window.env.t('confirm') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leaveGroup')
}
$scope.popoverEl.popover('destroy').popover({
html: true,
placement: 'top',
trigger: 'manual',
title: title,
content: html
}).popover('show');
});
}
$scope.reject = function(guild){
var i = _.findIndex(User.user.invitations.guilds, {id:guild.id});
if (~i){
User.user.invitations.guilds.splice(i, 1);
User.set({'invitations.guilds':User.user.invitations.guilds});
}
}
}
]);

View file

@ -0,0 +1,72 @@
'use strict';
habitrpg.controller('InviteToGroupCtrl', ['$scope', 'User', 'Groups', 'injectedGroup', '$http', 'Notification', function($scope, User, Groups, injectedGroup, $http, Notification) {
$scope.group = injectedGroup;
$scope.inviter = User.user.profile.name;
_resetInvitees();
$scope.addUuid = function() {
$scope.invitees.push({uuid: ''});
};
$scope.addEmail = function() {
$scope.emails.push({name: '', email: ''});
};
$scope.inviteNewUsers = function(inviteMethod) {
if (!$scope.group._id) {
$scope.group.name = $scope.group.name || env.t('possessiveParty', {name: User.user.profile.name});
return $scope.group.$save()
.then(function(res) {
_inviteByMethod(inviteMethod);
});
}
_inviteByMethod(inviteMethod);
};
function _inviteByMethod(inviteMethod) {
var invitationDetails;
if (inviteMethod === 'email') {
var emails = _getEmails();
invitationDetails = {inviter: $scope.inviter, emails: emails};
} else if (inviteMethod === 'uuid') {
var uuids = _getOnlyUuids();
invitationDetails = {uuids: uuids};
} else {
return console.log('Invalid invite method.')
}
Groups.Group.invite({gid: $scope.group._id}, invitationDetails, function(){
Notification.text(window.env.t('invitationsSent'));
_resetInvitees();
}, function(){
_resetInvitees();
});
}
function _getOnlyUuids() {
var uuids = _.pluck($scope.invitees, 'uuid');
var filteredUuids = _.filter(uuids, function(id) {
return id != '';
});
return filteredUuids;
}
function _getEmails() {
var emails = _.filter($scope.emails, function(obj) {
return obj.email != '';
});
return emails;
}
function _resetInvitees() {
var emptyEmails = [{name:"",email:""},{name:"",email:""}];
var emptyInvitees = [{uuid: ''}];
$scope.emails = emptyEmails;
$scope.invitees = emptyInvitees;
}
}]);

View file

@ -0,0 +1,132 @@
'use strict';
habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','Challenges','$state','$compile','Analytics','Quests',
function($rootScope,$scope,Groups,Chat,User,Challenges,$state,$compile,Analytics,Quests) {
$scope.type = 'party';
$scope.text = window.env.t('party');
$scope.group = $rootScope.party = Groups.party();
$scope.newGroup = new Groups.Group({type:'party'});
$scope.inviteOrStartParty = Groups.inviteOrStartParty;
$scope.questInit = Quests.questInit;
if ($state.is('options.social.party')) {
$scope.group.$syncParty(); // Sync party automatically when navigating to party page
}
Chat.seenMessage($scope.group._id);
$scope.create = function(group){
if (!group.name) group.name = env.t('possessiveParty', {name: User.user.profile.name});
group.$save(function(){
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':true,'groupType':'party','privacy':'private'});
Analytics.updateUser({'partyID':group.id,'partySize':1});
$rootScope.hardRedirect('/#/options/groups/party');
});
};
$scope.join = function(party){
var group = new Groups.Group({_id: party.id, name: party.name});
group.$join(function(){
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':false,'groupType':'party','privacy':'private'});
Analytics.updateUser({'partyID':party.id});
$rootScope.hardRedirect('/#/options/groups/party');
});
};
// TODO: refactor guild and party leave into one function
$scope.leave = function(keep) {
if (keep == 'cancel') {
$scope.selectedGroup = undefined;
$scope.popoverEl.popover('destroy');
} else {
Groups.Group.leave({gid: $scope.selectedGroup._id, keep:keep}, undefined, function(){
Analytics.updateUser({'partySize':null,'partyID':null});
$rootScope.hardRedirect('/#/options/groups/party');
});
}
};
// TODO: refactor guild and party clickLeave into one function
$scope.clickLeave = function(group, $event){
$scope.selectedGroup = group;
$scope.popoverEl = $($event.target);
var html, title;
Challenges.Challenge.query(function(challenges) {
challenges = _.pluck(_.filter(challenges, function(c) {
return c.group._id == group._id;
}), '_id');
if (_.intersection(challenges, User.user.challenges).length > 0) {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'remove-all\')">' + window.env.t('removeTasks') + '</a><br/>\n<a ng-click="leave(\'keep-all\')">' + window.env.t('keepTasks') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leavePartyCha');
} else {
html = $compile(
'<a ng-controller="GroupsCtrl" ng-click="leave(\'keep-all\')">' + window.env.t('confirm') + '</a><br/>\n<a ng-click="leave(\'cancel\')">' + window.env.t('cancel') + '</a><br/>'
)($scope);
title = window.env.t('leaveParty');
}
$scope.popoverEl.popover('destroy').popover({
html: true,
placement: 'top',
trigger: 'manual',
title: title,
content: html
}).popover('show');
});
};
$scope.clickStartQuest = function(){
var hasQuests = _.find(User.user.items.quests, function(quest) {
return quest > 0;
});
if (hasQuests){
$rootScope.openModal("ownedQuests", { controller:"InventoryCtrl" });
} else {
$rootScope.$state.go('options.inventory.quests');
}
};
$scope.leaveOldPartyAndJoinNewParty = function(newPartyId, newPartyName) {
if (confirm('Are you sure you want to delete your party and join ' + newPartyName + '?')) {
Groups.Group.leave({gid: Groups.party()._id, keep:false}, undefined, function() {
$scope.group = {
loadingNewParty: true
};
$scope.join({ id: newPartyId, name: newPartyName });
});
}
}
$scope.reject = function(){
User.set({'invitations.party':{}});
}
$scope.questCancel = function(party){
if (!confirm(window.env.t('sureCancel'))) return;
Groups.questCancel(party);
}
$scope.questAbort = function(party){
if (!confirm(window.env.t('sureAbort'))) return;
if (!confirm(window.env.t('doubleSureAbort'))) return;
Groups.questAbort(party);
}
$scope.questLeave = function(party){
if (!confirm(window.env.t('sureLeave'))) return;
delete $scope.group.quest.members[User.user._id];
Groups.questLeave(party);
}
$scope.questAccept = function(party){
Groups.questAccept(party);
};
$scope.questReject = function(party){
Groups.questReject(party);
}
}
]);

View file

@ -0,0 +1,10 @@
'use strict';
habitrpg.controller("TavernCtrl", ['$scope', 'Groups', 'User',
function($scope, Groups, User) {
$scope.group = Groups.tavern();
$scope.toggleUserTier = function($event) {
$($event.target).next().toggle();
}
}
]);

View file

@ -1,95 +1,114 @@
'use strict';
/**
* Services that persists and retrieves user from localStorage.
*/
(function() {
angular
.module('habitrpg')
.factory('Groups', groupsFactory);
angular.module('habitrpg').factory('Groups',
['$rootScope','ApiUrl', '$resource', '$q', '$http', 'User', 'Challenges', 'Analytics', '$location',
function($rootScope, ApiUrl, $resource, $q, $http, User, Challenges, Analytics, $location) {
var Group = $resource(ApiUrl.get() + '/api/v2/groups/:gid',
{gid:'@_id', messageId: '@_messageId'},
{
get: {
method: "GET",
isArray:false,
// Wrap challenges as ngResource so they have functions like $leave or $join
transformResponse: function(data, headers) {
data = angular.fromJson(data);
_.each(data && data.challenges, function(c) {
angular.extend(c, Challenges.Challenge.prototype);
});
return data;
}
},
groupsFactory.$inject = [
'$location',
'$resource',
'$rootScope',
'Analytics',
'ApiUrl',
'Challenges',
'User'
];
syncParty: {method: "GET", url: '/api/v2/groups/party'},
join: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/join'},
leave: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/leave'},
invite: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/invite'},
removeMember: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/removeMember'},
questAccept: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questAccept'},
questReject: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questReject'},
questCancel: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questCancel'},
questAbort: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questAbort'}
});
function groupsFactory($location, $resource, $rootScope, Analytics, ApiUrl, Challenges, User) {
// Defer loading everything until they're requested
var data = {party: undefined, myGuilds: undefined, publicGuilds: undefined, tavern: undefined};
var data = {party: undefined, myGuilds: undefined, publicGuilds: undefined, tavern: undefined};
var Group = $resource(ApiUrl.get() + '/api/v2/groups/:gid',
{gid:'@_id', messageId: '@_messageId'},
{
get: {
method: "GET",
isArray:false,
// Wrap challenges as ngResource so they have functions like $leave or $join
transformResponse: function(data) {
data = angular.fromJson(data);
_.each(data && data.challenges, function(c) {
angular.extend(c, Challenges.Challenge.prototype);
});
return data;
}
},
var syncUser = function(res) {
User.sync();
}
var logError = function(err) {
console.log(err);
}
syncParty: {method: "GET", url: '/api/v2/groups/party'},
join: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/join'},
leave: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/leave'},
invite: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/invite'},
removeMember: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/removeMember'},
questAccept: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questAccept'},
questReject: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questReject'},
questCancel: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questCancel'},
questAbort: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questAbort'},
questLeave: {method: "POST", url: ApiUrl.get() + '/api/v2/groups/:gid/questLeave'}
});
return {
party: function(cb){
function _syncUser() {
User.sync();
}
function _logError(err) {
console.log(err);
}
function party(cb) {
if (!data.party) return (data.party = Group.get({gid: 'party'}, cb));
return (cb) ? cb(party) : data.party;
},
publicGuilds: function(){
}
function publicGuilds() {
//TODO combine these as {type:'guilds,public'} and create a $filter() to separate them
if (!data.publicGuilds) data.publicGuilds = Group.query({type:'public'});
return data.publicGuilds;
},
myGuilds: function(){
}
function myGuilds() {
if (!data.myGuilds) data.myGuilds = Group.query({type:'guilds'});
return data.myGuilds;
},
tavern: function(){
}
function tavern() {
if (!data.tavern) data.tavern = Group.get({gid:'habitrpg'});
return data.tavern;
},
}
questAccept: function(party){
function questAccept(party) {
Analytics.updateUser({'partyID':party.id,'partySize':party.memberCount});
party.$questAccept()
.then(syncUser, logError);
},
return party.$questAccept()
.then(_syncUser, _logError);
}
questReject: function(party){
function questReject(party) {
Analytics.updateUser({'partyID':party.id,'partySize':party.memberCount});
party.$questReject()
.then(syncUser, logError);
},
return party.$questReject()
.then(_syncUser, _logError);
}
questCancel: function(party){
function questCancel(party) {
Analytics.updateUser({'partyID':party.id,'partySize':party.memberCount});
party.$questCancel()
.then(syncUser, logError);
},
return party.$questCancel()
.then(_syncUser, _logError);
}
questAbort: function(party){
function questAbort(party) {
Analytics.updateUser({'partyID':party.id,'partySize':party.memberCount});
party.$questAbort()
.then(syncUser, logError);
},
return party.$questAbort()
.then(_syncUser, _logError);
}
inviteOrStartParty: function(group) {
if (group.type === "party") {
$rootScope.openModal('invite-friends', {
function questLeave(party) {
Analytics.updateUser({'partyID':party.id,'partySize':party.memberCount});
return party.$questLeave()
.then(_syncUser, _logError);
}
function inviteOrStartParty(group) {
if (group.type === "party" || $location.$$path === "/options/groups/party") {
group.type = 'party';
$rootScope.openModal('invite-party', {
controller:'InviteToGroupCtrl',
resolve: {
injectedGroup: function(){ return group; }
@ -99,12 +118,22 @@ function($rootScope, ApiUrl, $resource, $q, $http, User, Challenges, Analytics,
Analytics.track({'hitType':'event','eventCategory':'button','eventAction':'click','eventLabel':'Invite Friends'});
$location.path("/options/groups/party");
}
},
}
// Pass reference to party, myGuilds, publicGuilds, tavern; inside data in order to
// be able to modify them directly (otherwise will be stick with cached version)
data: data,
return {
party: party,
publicGuilds: publicGuilds,
myGuilds: myGuilds,
tavern: tavern,
questAccept: questAccept,
questReject: questReject,
questAbort: questAbort,
questLeave: questLeave,
questCancel: questCancel,
inviteOrStartParty: inviteOrStartParty,
Group: Group
data: data,
Group: Group
}
}
}])
})();

View file

@ -71,20 +71,27 @@
"js/directives/when-scrolled.directive.js",
"js/controllers/authCtrl.js",
"js/controllers/menuCtrl.js",
"js/controllers/autoCompleteCtrl.js",
"js/controllers/challengesCtrl.js",
"js/controllers/chatCtrl.js",
"js/controllers/copyMessageModalCtrl.js",
"js/controllers/filtersCtrl.js",
"js/controllers/footerCtrl.js",
"js/controllers/groupsCtrl.js",
"js/controllers/guildsCtrl.js",
"js/controllers/hallCtrl.js",
"js/controllers/headerCtrl.js",
"js/controllers/inventoryCtrl.js",
"js/controllers/inviteToGroupCtrl.js",
"js/controllers/memberModalCtrl.js",
"js/controllers/menuCtrl.js",
"js/controllers/notificationCtrl.js",
"js/controllers/partyCtrl.js",
"js/controllers/rootCtrl.js",
"js/controllers/settingsCtrl.js",
"js/controllers/headerCtrl.js",
"js/controllers/tavernCtrl.js",
"js/controllers/tasksCtrl.js",
"js/controllers/filtersCtrl.js",
"js/controllers/userCtrl.js",
"js/controllers/groupsCtrl.js",
"js/controllers/inventoryCtrl.js",
"js/controllers/footerCtrl.js",
"js/controllers/challengesCtrl.js",
"js/controllers/hallCtrl.js"
"js/controllers/userCtrl.js"
],
"css": [
"bower_components/bootstrap/dist/css/bootstrap.css",

View file

@ -8,6 +8,7 @@ function clone(a) {
var _ = require('lodash');
var nconf = require('nconf');
var async = require('async');
var Q = require('q');
var utils = require('./../utils');
var shared = require('../../../common');
var User = require('./../models/user').model;
@ -489,15 +490,12 @@ api.join = function(req, res, next) {
api.leave = function(req, res, next) {
var user = res.locals.user,
group = res.locals.group;
if (group.type === 'party' && user.party.quest && user.party.quest.key) return res.json(403, 'You cannot leave party during an active quest. Please leave the quest first');
// When removing the user from challenges, should we keep the tasks?
var keep = (/^remove-all/i).test(req.query.keep) ? 'remove-all' : 'keep-all';
async.parallel([
// Remove active quest from user if they're leaving the party
function(cb){
if (group.type != 'party') return cb(null,{},1);
user.party.quest = Group.cleanQuestProgress();
user.save(cb);
},
// Remove user from group challenges
function(cb){
async.waterfall([
@ -534,10 +532,7 @@ api.leave = function(req, res, next) {
// Update the group
function(cb){
var update = {$pull:{members:user._id}};
if (group.type == 'party' && group.quest.key){
update['$unset'] = {};
update['$unset']['quest.members.' + user._id] = 1;
}
// FIXME do we want to remove the group `if group.members.length == 0` ? (well, 1 since the update hasn't gone through yet)
if (group.members.length > 1) {
var seniorMember = _.find(group.members, function (m) {return m != user._id});
@ -547,11 +542,6 @@ api.leave = function(req, res, next) {
update['$set'] = update['$set'] || {};
update['$set'].leader = seniorMember;
}
leader = group.quest && group.quest.leader;
if (leader && (leader == user._id || !~group.members.indexOf(leader))) {
update['$set'] = update['$set'] || {};
update['$set']['quest.leader'] = seniorMember;
}
}
update['$inc'] = {memberCount: -1};
Group.update({_id:group._id},update,cb);
@ -580,8 +570,9 @@ var inviteByUUIDs = function(uuids, group, req, res, next){
return cb({code:400,err:"User already pending invitation."});
Group.find({type:'party', members:{$in:[uuid]}}, function(err, groups){
if (err) return cb(err);
if (!_.isEmpty(groups))
return cb({code:400,err:"User already in a party."})
if (!_.isEmpty(groups) && groups[0].members.length > 1) {
return cb({code:400, err:"User already in a party."})
}
sendInvite();
});
}
@ -603,9 +594,6 @@ var inviteByUUIDs = function(uuids, group, req, res, next){
async.series([
function(cb){
invite.save(cb);
},
function(cb){
group.save(cb);
}
], function(err, results){
if (err) return cb(err);
@ -639,14 +627,20 @@ var inviteByUUIDs = function(uuids, group, req, res, next){
}, function(err){
if(err) return err.code ? res.json(err.code, {err: err.err}) : next(err);
// TODO pass group from save above don't find it again, or you have to find it again in order to run populate?
populateQuery(group.type, Group.findById(group._id)).exec(function(err, populatedGroup){
if(err) return next(err);
async.series([
function(cb) {
group.save(cb);
},
function(cb) {
// TODO pass group from save above don't find it again, or you have to find it again in order to run populate?
populateQuery(group.type, Group.findById(group._id)).exec(function(err, populatedGroup){
if(err) return next(err);
res.json(populatedGroup);
});
res.json(populatedGroup);
});
}
]);
});
};
var inviteByEmails = function(invites, group, req, res, next){
@ -667,6 +661,7 @@ var inviteByEmails = function(invites, group, req, res, next){
}
// yeah, it supports guild too but for backward compatibility we'll use partyInvite as query
var link = '?partyInvite='+ utils.encrypt(JSON.stringify({id:group._id, inviter:res.locals.user._id, name:group.name}));
var inviterVars = utils.getUserInfo(res.locals.user, ['name', 'email']);
@ -697,9 +692,9 @@ var inviteByEmails = function(invites, group, req, res, next){
}, function(err){
if(err) return err.code ? res.json(err.code, {err: err.err}) : next(err);
if(usersAlreadyRegistered.length > 0){
if (usersAlreadyRegistered.length > 0){
inviteByUUIDs(usersAlreadyRegistered, group, req, res, next);
}else{
} else{
// Send only status code down the line because it doesn't need
// info on invited users since they are not yet registered
@ -711,11 +706,11 @@ var inviteByEmails = function(invites, group, req, res, next){
api.invite = function(req, res, next){
var group = res.locals.group;
if(req.body.uuids){
if (req.body.uuids) {
inviteByUUIDs(req.body.uuids, group, req, res, next);
}else if(req.body.emails){
} else if (req.body.emails) {
inviteByEmails(req.body.emails, group, req, res, next)
}else{
} else {
return res.json(400,{err: "Can invite only by email or uuid"});
}
}
@ -1075,3 +1070,37 @@ api.questAbort = function(req, res, next){
group = null;
})
}
api.questLeave = function(req, res, next) {
// Non-member leave quest while still in progress
var group = res.locals.group;
var user = res.locals.user;
if (!(group.quest && group.quest.active)) {
return res.json(404, { err: 'No active quest to leave' });
}
if (!(group.quest.members && group.quest.members[user._id])) {
return res.json(403, { err: 'You are not part of the quest' });
}
if (group.quest.leader === user._id) {
return res.json(403, { err: 'Quest leader cannot leave quest' });
}
delete group.quest.members[user._id];
group.markModified('quest.members');
user.party.quest = Group.cleanQuestProgress();
user.markModified('party.quest');
var groupSavePromise = Q.nbind(group.save, group);
var userSavePromise = Q.nbind(user.save, user);
Q.all([groupSavePromise(), userSavePromise()])
.done(function(values) {
return res.send(204);
}, function(error) {
return next(error);
});
}

View file

@ -527,6 +527,14 @@ module.exports = (swagger, v2) ->
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.questAbort
"/groups/{gid}/questLeave":
spec:
method: 'POST'
description: 'Leave an active quest (Quest leaders cannot leave active quests. They must abort the quest to leave)'
parameters: [path('gid','Group to leave quest in','string')]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.questLeave
#TODO PUT /groups/:gid/chat/:messageId
"/groups/{gid}/chat:GET":

View file

@ -1,37 +1,25 @@
//- If this variable gets any bigger, please move it out of this jade template and into common/script/content
- var questCategories = {'unlockable':'unlockableQuests','gold':'goldQuests','pet':'petQuests'}
include ../../shared/mixins
.container-fluid
.row: .col-xs-12
.npc_ian.pull-left-sm.col-centered
.popover.static-popover.fade.right.in.pull-left-sm.col-centered
.arrow.hidden-xs
h3.popover-title=env.t('ian')
.popover-content
p=env.t('ianText')
.row
.col-md-6
.clearfix
.npc_ian.pull-left-sm.col-centered
.popover.static-popover.fade.right.in.pull-left-sm.col-centered
.arrow.hidden-xs
h3.popover-title=env.t('ian')
.popover-content
p=env.t('ianText')
h3.equipment-title=env.t('yourQuests')
menu.inventory-list
li.customize-menu
p.muted(ng-show='questCount < 1')=env.t('noScrolls')
each caption,type in questCategories
menu.pets-menu(label=env.t(caption))
div(ng-repeat='(quest_key,points) in ownedItems(user.items.quests)', ng-init='quest = Content.quests[quest_key]', ng-if='Content.quests[quest_key].category === "#{type}"')
button.customize-option(ng-class='lockQuest(quest) ? "inventory_quest_scroll_locked inventory_quest_scroll_{{::quest.key}}_locked locked" : "inventory_quest_scroll inventory_quest_scroll_{{::quest.key}}"',
ng-click='showQuest(quest_key)',
data-popover-html="{{::lockQuest(quest) ? env.t('scrollsPre') : questPopover(quest) | markdown}}",
popover-title='{{::quest.text()}}', popover-trigger='mouseenter',
popover-placement='right', popover-append-to-body='true')
.badge.badge-info.stack-count {{points}}
+ownedQuests(true,'right')
.col-md-6.border-left
li.customize-menu
h3.equipment-title=env.t('questsForSale')
each caption,type in questCategories
menu.pets-menu(label=env.t(caption))
div(ng-repeat='quest in Content.questsByLevel', ng-if='quest.canBuy && quest.category === "#{type}"')
div(ng-repeat='type in Content.userCanOwnQuestCategories')
menu.pets-menu(label='{{env.t(type + "Quests")}}')
div(ng-repeat='quest in Content.questsByLevel', ng-if='quest.canBuy && quest.category === type')
button.customize-option(ng-class='lockQuest(quest) ? "inventory_quest_scroll_locked inventory_quest_scroll_{{::quest.key}}_locked locked" : "inventory_quest_scroll inventory_quest_scroll_{{::quest.key}}"',
data-popover-html="{{::lockQuest(quest,true) ? env.t('scrollsPre') : questPopover(quest) | markdown}}",
popover-title='{{::quest.text()}}', popover-append-to-body="true",

View file

@ -7,7 +7,7 @@ div.chat-form.guidelines-not-accepted(ng-if='!user.flags.communityGuidelinesAcce
div
button.btn.btn-warning(ng-click='acceptCommunityGuidelines()')=env.t('iAcceptCommunityGuidelines')
.chat-buttons
button(type="button", ng-click='sync(group)')=env.t('toolTipMsg')
button(ng-if='group.chat.length > 0 || group.memberCount > 1', type="button", ng-click='sync(group)')=env.t('toolTipMsg')
form.chat-form(ng-if='user.flags.communityGuidelinesAccepted' ng-submit='postChat(group,message.content)')
div(ng-controller='AutocompleteCtrl')
@ -19,5 +19,5 @@ form.chat-form(ng-if='user.flags.communityGuidelinesAccepted' ng-submit='postCha
.chat-controls.clearfix
.chat-buttons
input.btn(type='submit', value=env.t('sendChat'), ng-disabled='_sending')
button.btn(type="button", ng-click='sync(group)', ng-disabled='_sending')=env.t('toolTipMsg')
button.btn(ng-if='group.chat.length > 0 || group.memberCount > 1', type="button", ng-click='sync(group)', ng-disabled='_sending')=env.t('toolTipMsg')
include ../../shared/formatting-help

View file

@ -16,7 +16,7 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
.panel.panel-default
.panel-heading(bindonce='group')
h3.panel-title
| {{group.name}}
span {{group.name}}
span.group-leave-join(ng-if='group')
a.btn.btn-sm.btn-danger.pull-right(ng-if=":: isMemberOfGroup(User.user._id, group)", ng-click='clickLeave(group, $event)')
span.glyphicon.glyphicon-ban-circle
@ -50,16 +50,18 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
div(ng-show='!group._editing')
img.img-rendering-auto.pull-right(ng-show='group.logo', ng-src='{{group.logo}}')
markdown(text='group.description')
hr
br
p=env.t('groupLeader')
|:
a(class='badge badge-info', ng-click='clickMember(group.leader._id, true)')
a.badge.badge-info(ng-click='clickMember(group.leader._id, true)')
| {{group.leader.profile.name}}
.slight-vertical-padding
small.muted=env.t('groupID')
|: {{group._id}}
include ./challenge-box
.text-center(ng-if='group.type === "party"')
.row.row-margin: .col-sm-6.col-sm-offset-3
button.btn.btn-success.btn-block(
ng-if='!party.quest.key',
ng-click='clickStartQuest();'
)=env.t('startAQuest')
// ------ Members -------
.panel.panel-default
@ -67,56 +69,68 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
h3.panel-title
=env.t('members')
button.pull-right.btn.btn-primary(ng-click="openInviteModal(group)")=env.t("inviteFriends")
.panel-body.modal-fixed-height
div.form-group(ng-if='::group.type=="party"')
p=env.t('partyList')
select.form-control#partyOrder(
ng-model='user.party.order',
ng-controller='ChatCtrl',
ng-options='k as v for (k , v) in partyOrderChoices',
ng-change='set({"party.order": user.party.order})'
)
|&nbsp;
select.form-control#partyOrderAscending(
ng-model='user.party.orderAscending',
ng-controller='ChatCtrl',
ng-options='k as v for (k , v) in partyOrderAscendingChoices',
ng-change='set({"party.orderAscending": user.party.orderAscending})'
)
table.table.table-striped(bindonce='group')
h4(ng-show='::group.memberCount === 1 && group.type === "party"')=env.t('partyEmpty')
table.table.table-striped(ng-show='::group.memberCount > 1 || group.type !== "party"' bindonce='group')
tr(ng-repeat='member in group.members track by member._id')
td.media
// allow leaders to ban members
div.pull-left(ng-show='group.leader._id == user.id && member._id != user._id')
.pull-left(ng-show='group.leader._id == user.id && member._id != user._id')
a.media-object(ng-click='removeMember(group, member, true)')
span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip'))
a.media-body
span(ng-click='clickMember(member._id, true)')
| {{member.profile.name}}
tr(ng-if='group.memberCount > group.members.length')
tr(ng-if='::group.memberCount > group.members.length')
td
|<span class='badge'>{{group.memberCount - group.members.length}}
span.badge {{group.memberCount - group.members.length}}
= ' ' + env.t('moreMembers')
|</span>
h4(ng-show='group.invites.length > 0')=env.t('invited')
table.table.table-striped
tr(ng-repeat='invite in group.invites')
td.media
// allow leaders to ban members
div.pull-left(ng-show='group.leader._id == user.id')
.pull-left(ng-show='group.leader._id == user.id')
a.media-object(ng-click='removeMember(group, invite, false)')
span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip'))
a.media-body
span(ng-click='clickMember(invite._id, true)')
| {{invite.profile.name}}
.panel.panel-default(ng-if='::group.type=="party" && group.memberCount > 1')
.panel-heading
h3.panel-title=env.t('partyList')
.panel-body
.form-group
select.form-control#partyOrder(
ng-model='user.party.order',
ng-controller='ChatCtrl',
ng-options='k as v for (k , v) in partyOrderChoices',
ng-change='set({"party.order": user.party.order})'
)
| &nbsp;
select.form-control#partyOrderAscending(
ng-model='user.party.orderAscending',
ng-controller='ChatCtrl',
ng-options='k as v for (k , v) in partyOrderAscendingChoices',
ng-change='set({"party.orderAscending": user.party.orderAscending})'
)
include ./challenge-box
div(ng-if="group")
a.btn.btn-danger(ng-if=':: isMemberOfGroup(User.user._id, group)', data-id='{{group.id}}', ng-click='clickLeave(group, $event)')=env.t('leave')
a.btn.btn-success(ng-if=':: !isMemberOfGroup(User.user._id, group)', ng-click='join(group)')=env.t('join')
.slight-vertical-padding
small.muted=env.t('groupID')
| : {{group._id}}
.slight-vertical-padding(ng-if='group.type === "party" && group.memberCount === 1')
small.muted=env.t('userId')
| : {{user._id}}
.col-md-8
div()
div
textarea.form-control(ng-show='group._editing', rows=6, placeholder=env.t('leaderMsg'), ng-model='group.leaderMessage')
table(ng-show='group.leaderMessage')
tr
@ -131,3 +145,5 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
include ./chat-box
+chatMessages()
h4(ng-if='group.chat.length < 1 && group.type === "party"')=env.t('partyChatEmpty')
h4(ng-if='group.chat.length < 1 && group.type === "guild"')=env.t('guildChatEmpty')

View file

@ -5,6 +5,7 @@ include ./challenges
include ./hall
include ./quests/index
include ./chat-message
include ./party
script(type='text/ng-template', id='partials/options.social.inbox.html')
.options-blurbmenu
@ -26,23 +27,6 @@ script(type='text/ng-template', id='partials/options.social.inbox.html')
script(type='text/ng-template', id='partials/options.social.tavern.html')
include ./tavern
script(type='text/ng-template', id='partials/options.social.party.html')
div(ng-if='group._id')
include ./group
div(ng-if='!group._id')
div(ng-show='user.invitations.party.id').container-fluid
h2=env.t('invitedTo', {name: '{{user.invitations.party.name}}'})
a.btn.btn-success(data-type='party', ng-click='join(user.invitations.party)')=env.t('accept')
a.btn.btn-danger(ng-click='reject()')=env.t('reject')
div(ng-hide='user.invitations.party.id').container-fluid
h2=env.t('createAParty')
p
=env.t('noPartyText')
pre.prettyprint.
{{user.id}}
p!=env.t('LFG', {linkStart: "<a href='https://habitica.com/#/options/groups/guilds/f2db2a7f-13c5-454d-b3ee-ea1f5089e601'>", linkEnd: "</a>"})
include ./create-group
script(type='text/ng-template', id='partials/options.social.guilds.public.html')
.options-blurbmenu
small.muted=env.t('blurbGuildsPage')

View file

@ -0,0 +1,13 @@
include ../../shared/avatar/generated_avatar
script(type='text/ng-template', id='partials/options.social.party.html')
div(ng-if='group.loadingNewParty')
include ./party/loading-new-party
div(ng-if='group._id')
include ./party/leave-party-and-join-another
include ./group
div(ng-if='!group._id && !group.loadingNewParty')
include ./party/party-invitation
include ./party/start-a-party

View file

@ -0,0 +1,8 @@
- var newParty = 'User.user.invitations.party'
.containter-fluid(ng-if='#{newParty}.id && party._id')
.row.text-center
.col-sm-6.col-sm-offset-3.alert.alert-warning
p {{::env.t('invitedToNewParty', { partyName: #{newParty}.name })}}
p
button.btn.btn-success(ng-click='leaveOldPartyAndJoinNewParty(#{newParty}.id, #{newParty}.name)')=env.t('joinNewParty')
button.btn.btn-default(ng-click='reject()')=env.t('declineInvitation')

View file

@ -0,0 +1,5 @@
.container-fluid
.row.text-center: .col-xs-12
h2=env.t('loadingNewParty')
img(src='/page-loader.gif')

View file

@ -0,0 +1,9 @@
.container-fluid(ng-show='user.invitations.party.id')
h2=env.t('invitedTo', {name: '{{user.invitations.party.name}}'})
a.btn.btn-success(
data-type='party',
ng-click='join(user.invitations.party)'
)=env.t('accept')
a.btn.btn-danger(ng-click='reject()')=env.t('reject')

View file

@ -0,0 +1,23 @@
.container.text-center(ng-hide='user.invitations.party.id')
.row.row-margin: .col-sm-6.col-sm-offset-3
a.btn.btn-primary.btn-lg.btn-block(ng-click="inviteOrStartParty(group)")=env.t("startPartyWithFriends")
.row.row-margin(ng-controller='UserCtrl')
span.inline-block.party_preview
span.figure.herobox.inline-block
.character-sprites(style='margin:0 0 0 4.2em')
+generatedAvatar
.row.row-margin
ul
li=env.t('partyExplanation1')
li=env.t('partyExplanation2')
li=env.t('partyExplanation3')
.row.row-margin
h4=env.t('wantToStartParty')
.row.row-margin
.btn.btn-primary(ng-click='inviteOrStartParty(group)')=env.t('inviteFriendsNow')
.btn.btn-default(ng-click='create(newGroup); group.loadingNewParty = true')=env.t('inviteFriendsLater')
.btn.btn-default(ng-click='openModal("user-id")')=env.t('joinExistingParty')

View file

@ -2,7 +2,7 @@ mixin participants(questStart)
-var isMember = 'group.quest.members[member._id]'
table.table.table-striped
tr(ng-repeat='member in group.members track by member._id',
ng-if='::#{!questStart} || #{isMember}')
ng-if='#{!questStart} || #{isMember}')
td
span(ng-if=':: group.quest.leader && group.quest.leader==member._id') *&nbsp;
a: span(ng-click='clickMember(member._id, true)') {{::member.profile.name}}

View file

@ -20,5 +20,7 @@ div(ng-if='group.quest.active==true')
include ./ianQuestInfo
unless tavern
button.btn.btn-sm.btn-warning(ng-if=':: (group.quest.leader && group.quest.leader==user._id && isMemberOfGroup(group.quest.leader,group) && isMemberOfRunningQuest(group.quest.leader,group))',
button.btn.btn-sm.btn-warning(ng-if=':: (group.quest.leader && group.quest.leader==user._id && isMemberOfRunningQuest(group.quest.leader,group))',
ng-click='questAbort(party)')=env.t('abort')
button.btn.btn-sm.btn-warning(ng-if='!(group.quest.leader && group.quest.leader === user._id) && isMemberOfRunningQuest(user._id,group)',
ng-click='questLeave(party)')=env.t('leaveQuest')

View file

@ -15,7 +15,7 @@ mixin generatedAvatar(options)
+costumeSetting('armor', {prefix: "profile.preferences.size + '_' + "})
+costumeSetting('back', {suffix: " + '_collar'"})
+costumeSetting('body')
span(class='head_0')
span.head_0
- var hairTypes = ['base', 'bangs', 'mustache', 'beard']
each type in hairTypes
span(class='hair_#{type}_{{profile.preferences.hair.#{type}}}_{{profile.preferences.hair.color}}')

View file

@ -16,4 +16,20 @@ mixin previewMarkdown(text)
.panel.panel-warning
.panel-heading=env.t('msgPreviewHeading')
.panel-body.markdown-preview
markdown(text='#{text}')
markdown(text='#{text}')
mixin ownedQuests(popoverAppend, popoverPlacement)
menu.inventory-list
li.customize-menu
p.muted(ng-show='questCount < 1')=env.t('noScrolls')
div(ng-repeat='type in Content.userCanOwnQuestCategories track by $index')
menu.pets-menu(ng-if='Shared.count.questsOfCategory(user.items.quests,type) > 0', label='{{::env.t(type + "Quests")}}')
div(ng-repeat='(quest_key,points) in ownedItems(user.items.quests)', ng-init='quest = Content.quests[quest_key]', ng-if='Content.quests[quest_key].category === type')
button.customize-option(ng-class='lockQuest(quest) ? "inventory_quest_scroll_locked inventory_quest_scroll_{{::quest.key}}_locked locked" : "inventory_quest_scroll inventory_quest_scroll_{{::quest.key}}"',
ng-click='$close(); showQuest(quest_key)',
data-popover-html="{{::lockQuest(quest) ? env.t('scrollsPre') : questPopover(quest) | markdown}}",
popover-title='{{::quest.text()}}',
popover-trigger='mouseenter',
popover-append-to-body='#{popoverAppend}',
popover-placement='#{popoverPlacement}')
.badge.badge-info.stack-count {{points}}

View file

@ -1,21 +1,29 @@
script(type='text/ng-template', id='modals/invite-friends.html')
script(type='text/ng-template', id='modals/invite-guild.html')
.modal-header
h4=env.t('inviteFriends')
.modal-body
p.alert.alert-info!=env.t('inviteAlertInfo')
form.form-inline(ng-submit='invite()')
//-.alert.alert-danger(ng-show='_groupError') {{_groupError}}
.form-group
input.form-control(type='text', placeholder=env.t('userId'), ng-model='invitee')
|&nbsp;
button.btn.btn-primary(type='submit')=env.t('inviteExistUser')
form.form-horizontal(ng-submit='inviteNewUsers("uuid")')
table.table.table-striped
thead
tr
th=env.t('userId')
tbody
tr(ng-repeat='user in invitees track by $index')
td
input.form-control(type='text', ng-model='user.uuid')
//- @TODO: Refactor src/controllers/groups api.invite to use promises before implimenting this
//- tr
//- td
//- a.btn.btn-xs.pull-right(ng-click='addUuid()')
//- i.glyphicon.glyphicon-plus
tr
td
.col-sm-4.col-sm-offset-8
button.btn.btn-primary.btn-block(type='submit')=env.t('sendInvitations')
hr
p.alert.alert-info=env.t('inviteByEmail')
form.form-horizontal(ng-submit='inviteEmails()')
form.form-horizontal(ng-submit='inviteNewUsers("email")')
table.table.table-striped
thead
tr
@ -29,7 +37,7 @@ script(type='text/ng-template', id='modals/invite-friends.html')
input.form-control(type='email', ng-model='email.email')
tr
td(colspan=2)
a.btn.btn-xs.pull-right(ng-click='emails = emails.concat([{name:"",email:""}])')
a.btn.btn-xs.pull-right(ng-click='addEmail()')
i.glyphicon.glyphicon-plus
tr
td.form-group(colspan=2)
@ -37,12 +45,76 @@ script(type='text/ng-template', id='modals/invite-friends.html')
.col-sm-7
input.form-control(type='text', ng-model='inviter')
.col-sm-4
button.btn.btn-primary.btn-wrap(type='submit')=env.t('inviteNewUsers')
//-
hr
p.alert.alert-info=env.t('inviteAlertInfo2')
input.form-control(type='text', ng-value='inviteLink({id: party._id, inviter: user._id, name: party.name})')
button.btn.btn-primary.btn-block(type='submit')=env.t('sendInvitations')
.modal-footer
button.btn.btn-default(ng-click='$close()')=env.t('close')
script(type='text/ng-template', id='modals/invite-party.html')
.modal-body
div(ng-if='!group._id')
h4(style='margin-top:0')=env.t('nameYourParty')
input.form-control#new-group-name.input-medium.option-content(required, type='text', ng-model='group.name')
tabset
tab(heading=env.t('inviteByEmail'))
p.alert.alert-info=env.t('inviteByEmailExplanation')
form.form-horizontal(ng-submit='inviteNewUsers("email")')
table.table.table-striped
thead
tr
th=env.t('name')
th=env.t('email')
tbody
tr(ng-repeat='email in emails')
td
input.form-control(type='text', ng-model='email.name')
td
input.form-control(type='email', ng-model='email.email')
tr
td(colspan=2)
a.btn.btn-xs.pull-right(ng-click='addEmail()')
i.glyphicon.glyphicon-plus
tr
td.form-group(colspan=2)
label.col-sm-1.control-label=env.t('byColon')
.col-sm-7
input.form-control(type='text', ng-model='inviter')
.col-sm-4
button.btn.btn-primary.btn-block(type='submit')=env.t('sendInvitations')
tab(heading=env.t('inviteExistUser'))
p.alert.alert-info!=env.t('inviteAlertInfo')
form.form-horizontal(ng-submit='inviteNewUsers("uuid")')
table.table.table-striped
thead
tr
th=env.t('userId')
tbody
tr(ng-repeat='user in invitees track by $index')
td
input.form-control(type='text', ng-model='user.uuid')
//- @TODO: Refactor src/controllers/groups api.invite to use promises before implimenting this
//- tr
//- td
//- a.btn.btn-xs.pull-right(ng-click='addUuid()')
//- i.glyphicon.glyphicon-plus
tr
td
.col-sm-4.col-sm-offset-8
button.btn.btn-primary.btn-block(type='submit')=env.t('sendInvitations')
.row
.quest_basilist.pull-left
p(style='margin:2em')=env.t('exclusiveQuestScroll')
button.btn.btn-default.pull-right(style='margin:auto 1em', ng-click='$close()')=env.t('close')
.modal-footer(style='padding-bottom:0',ng-if='!group._id || group.memberCount < 2')
p!=env.t('wantExistingParty', {linkStart: "<a href='/#/options/groups/guilds/f2db2a7f-13c5-454d-b3ee-ea1f5089e601'>", linkEnd: "</a>"})
pre(style='margin:1em 0 0 0').
{{::user._id}}
script(type='text/ng-template', id='modals/user-id.html')
.modal-header
h4=env.t('userId')
.modal-body
p!=env.t('wantExistingParty', {linkStart: "<a href='/#/options/groups/guilds/f2db2a7f-13c5-454d-b3ee-ea1f5089e601'>", linkEnd: "</a>"})
pre(style='margin:1em 0 0 0').
{{::user._id}}
.modal-footer
.btn.btn-default.pull-right(ng-click='$close()')=env.t('close')

View file

@ -1,24 +1,27 @@
include ./quest-rewards
include ../mixins
mixin questInfo
.pull-right-sm.text-center
.col-centered(class='quest_{{::selectedQuest.key}}')
div(ng-if='::selectedQuest.boss')
h4 {{::selectedQuest.boss.name()}}
p
strong=env.t('bossHP') + ': '
| {{::selectedQuest.boss.hp}}
p
strong=env.t('bossStrength') + ': '
| {{::selectedQuest.boss.str}}
.well.well-sm
p
strong=env.t('bossHP') + ': '
| {{::selectedQuest.boss.hp}}
p
strong=env.t('bossStrength') + ': '
| {{::selectedQuest.boss.str}}
div(ng-if='::selectedQuest.collect')
p(ng-repeat='(k,v) in ::selectedQuest.collect')
strong=env.t('collect') + ': '
| {{::selectedQuest.collect[k].count}} {{::selectedQuest.collect[k].text()}}
h4=env.t('itemsToCollect')
.well.well-sm
p(ng-repeat='(k,v) in ::selectedQuest.collect')
| {{:: env.t('collectionItems', { number: selectedQuest.collect[k].count, items: selectedQuest.collect[k].text() })}}
div(ng-bind-html='::selectedQuest.notes()')
quest-rewards(key='{{::selectedQuest.key}}', header=env.t('rewards'))
script(type='text/ng-template', id='modals/questCompleted.html')
script(type='text/ng-template', id='modals/questCompleted.html')
.modal-header
h4 "{{::Content.quests[user.party.quest.completed].text()}}"
=env.t('completed')
@ -29,7 +32,7 @@ script(type='text/ng-template', id='modals/questCompleted.html')
.modal-footer
button.btn.btn-primary(ng-click='set({"party.quest.completed":""}); $close()')=env.t('ok')
script(type='text/ng-template', id='modals/showQuest.html')
script(type='text/ng-template', id='modals/showQuest.html')
.modal-header
h4 {{::selectedQuest.text()}}
.modal-body
@ -40,9 +43,10 @@ script(type='text/ng-template', id='modals/showQuest.html')
p=env.t('questWarning')
.modal-footer
button.btn.btn-default(ng-click='closeQuest(); $close()')=env.t('cancel')
button.btn.btn-primary(ng-click='questInit(); $close()')=env.t('inviteParty')
button.btn.btn-primary(ng-click='questInit(); $close()')
| {{:: party.memberCount > 1 ? env.t('inviteParty') : env.t('startQuest')}}
script(type='text/ng-template', id='modals/buyQuest.html')
script(type='text/ng-template', id='modals/buyQuest.html')
.modal-header
h4 {{::selectedQuest.text()}}
.modal-body
@ -52,7 +56,7 @@ script(type='text/ng-template', id='modals/buyQuest.html')
button.btn.btn-primary(ng-if='::selectedQuest.category !== "gold"', ng-click='purchase("quests", quest); closeQuest(); $close()')=env.t('buyQuest') + ': {{::selectedQuest.value}} ' + env.t('gems')
button.btn.btn-primary(ng-if='::selectedQuest.category === "gold"', ng-click='user.ops.buyQuest({params:{key:selectedQuest.key}}); closeQuest(); $close()')=env.t('buyQuest') + ': {{::selectedQuest.goldValue}} ' + env.t('gold')
script(type='text/ng-template', id='modals/questInvitation.html')
script(type='text/ng-template', id='modals/questInvitation.html')
.modal-header
h4=env.t('questInvitation')
|&nbsp;{{::Content.quests[user.party.quest.key].text()}}
@ -62,7 +66,7 @@ script(type='text/ng-template', id='modals/questInvitation.html')
div(ng-if='::Content.quests[user.party.quest.key].boss')
h4 {{::Content.quests[user.party.quest.key].boss.name()}}
p
strong=env.t('bossHP') + ': '
strong=env.t('bossHP') + ': '
| {{::Content.quests[user.party.quest.key].boss.hp}}
p
strong=env.t('bossStrength') + ': '
@ -70,7 +74,7 @@ script(type='text/ng-template', id='modals/questInvitation.html')
div(ng-if='::Content.quests[user.party.quest.key].collect')
p(ng-repeat='(k,v) in ::Content.quests[user.party.quest.key].collect')
strong=env.t('collect') + ': '
| {{::Content.quests[user.party.quest.key].collect[k].count}} {{::Content.quests[user.party.quest.key].collect[k].text()}}
| {{::Content.quests[user.party.quest.key].collect[k].count}} {{::Content.quests[user.party.quest.key].collect[k].text()}}
div(ng-bind-html='::Content.quests[user.party.quest.key].notes()')
quest-rewards(key='{{::user.party.quest.key}}', header=env.t('rewards'))
.modal-footer
@ -92,3 +96,13 @@ script(type='text/ng-template', id='modals/questDrop.html')
button.btn.btn-default(ng-click='closeQuest(); $close()')=env.t('questLater')
button.btn.btn-primary(ng-click='inviteOrStartParty(group); $close()', ng-if='!party.members')=env.t('startAParty')
button.btn.btn-primary(ng-click='questInit(); $close()', ng-if='party.members')=env.t('inviteParty')
script(type='text/ng-template', id='modals/ownedQuests.html')
.modal-header
h4=env.t('whichQuestStart')
.modal-body
+ownedQuests(false,'top')
.modal-footer
h4.pull-left
a(ui-sref='options.inventory.quests', ng-click='$close()')=env.t('getMoreQuests')
button.btn.btn-default.pull-right(ng-click='$close()')=env.t('neverMind')