Achievement list renovation & Achievements API (#7904)

* pull apart achievements into different subcategories

* achievs previously hidden to others if unachieved are now always shown

* achievs previously always hidden if unachieved are now always shown

* pull apart ultimate gear achievs

* add achiev wrapper mixin

* add achiev mixin for simple counts

* add achiev mixin for singular/plural achievs

* add simpleAchiev mixin and support attributes

* always hide potentially unearnable achievs if unearned

* contributor achiev now uses string interpolation for readMore link

* transition to basic achiev grid layout

* fix npc achievement img bug introduced in c90f7e2

* move surveys and contributor achievs into special section so it is never empty

* double size of achievs in achievs grid

* achievs in grid are muted if unachieved (includes recompiled sprites)

* fix streak notification strings

* add counts to achievement badges for applicable achieved achievs

* list achievements by api

* fix achievement strings in new api

* unearned achievs now use dedicated (WIP) 'unearned' badge instead of muted versions of the normal badges

* fix & cleanup achievements api

* extract generation of the achievements result to a class

* clean up achievement counter css using existing classes

* simplify exports of new achievementBuilder lib

* remove class logic from achievementBuilder lib

* move achievs to common, add rebirth achiev logic, misc fixes

* replace achievs jade logic with results of api call

* fix linting errors

* achievs lib now returns achievs object subdivided by type (basic/seasonal/special

* add tests for new achievs lib

* fix linting errors

* update controllers and views for updated achievs lib

* add indices to achievements to preserve intended order

* move achiev popovers to left

* rename achievs lib to achievements

* adjust positioning of achieve popovers now that stats and achievs pages
are separate

* fix: achievements api correctly decides whether to append extra string for master and triadBingo achievs

* revert compiled sprites so they don't bog down the PR

* pull out achievs api integration tests

* parameterize ultimate gear achievements' text string

* break out static achievement data from user-specific data

* reorg content.achievements to add achiev data in related chunks

* cleanup, respond to feedback

* improve api documentation

* fix merge issues

* Helped Habit Grow --> Helped Habitica Grow

* achievement popovers are muted if the achiev is unearned

* fix singular achievement labels / achievement popover on click

* update apidoc for achievements (description, param-type, successExample, error-types)

* fix whitespace issues in members.js

* move html to a variable

* updated json example

* fix syntax after merge
This commit is contained in:
Kaitlin Hipkin
2016-12-13 13:48:18 -05:00
committed by Keith Holliday
parent 97e1d75dce
commit 0817cf96e1
27 changed files with 1091 additions and 290 deletions

View File

@@ -0,0 +1,44 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('GET /members/:memberId/achievements', () => {
let user;
before(async () => {
user = await generateUser();
});
it('validates req.params.memberId', async () => {
await expect(user.get('/members/invalidUUID/achievements')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns achievements based on given user', async () => {
let member = await generateUser({
contributor: {level: 1},
backer: {tier: 3},
});
let achievementsRes = await user.get(`/members/${member._id}/achievements`);
expect(achievementsRes.special.achievements.contributor.earned).to.equal(true);
expect(achievementsRes.special.achievements.contributor.value).to.equal(1);
expect(achievementsRes.special.achievements.kickstarter.earned).to.equal(true);
expect(achievementsRes.special.achievements.kickstarter.value).to.equal(3);
});
it('handles non-existing members', async () => {
let dummyId = generateUUID();
await expect(user.get(`/members/${dummyId}/achievements`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', {userId: dummyId}),
});
});
});

View File

@@ -0,0 +1,348 @@
import shared from '../../../website/common';
import {
generateUser,
} from '../../helpers/common.helper';
describe('achievements', () => {
describe('general well-formedness', () => {
let user = generateUser();
let achievements = shared.achievements.getAchievementsForProfile(user);
it('each category has \'label\' and \'achievements\' fields', () => {
_.each(achievements, (category) => {
expect(category).to.have.property('label')
.that.is.a('string');
expect(category).to.have.property('achievements')
.that.is.a('object');
});
});
it('each achievement has all required fields of correct types', () => {
_.each(achievements, (category) => {
_.each(category.achievements, (achiev) => {
// May have additional fields (such as 'value' and 'optionalCount').
expect(achiev).to.contain.all.keys(['title', 'text', 'icon', 'earned', 'index']);
expect(achiev.title).to.be.a('string');
expect(achiev.text).to.be.a('string');
expect(achiev.icon).to.be.a('string');
expect(achiev.earned).to.be.a('boolean');
expect(achiev.index).to.be.a('number');
});
});
});
it('categories have unique labels', () => {
let achievementsArray = _.values(achievements).map(cat => cat.label);
let labels = _.uniq(achievementsArray);
expect(labels.length).to.be.greaterThan(0);
expect(labels.length).to.eql(_.size(achievements));
});
it('achievements have unique keys', () => {
let keysSoFar = {};
_.each(achievements, (category) => {
_.keys(category.achievements).forEach((key) => {
expect(keysSoFar[key]).to.be.undefined;
keysSoFar[key] = key;
});
});
});
it('achievements have unique indices', () => {
let indicesSoFar = {};
_.each(achievements, (category) => {
_.each(category.achievements, (achiev) => {
let i = achiev.index;
expect(indicesSoFar[i]).to.be.undefined;
indicesSoFar[i] = i;
});
});
});
it('all categories have at least 1 achievement', () => {
_.each(achievements, (category) => {
expect(_.size(category.achievements)).to.be.greaterThan(0);
});
});
});
describe('unearned basic achievements', () => {
let user = generateUser();
let basicAchievs = shared.achievements.getAchievementsForProfile(user).basic.achievements;
it('streak and perfect day achievements exist with counts', () => {
let streak = basicAchievs.streak;
let perfect = basicAchievs.perfect;
expect(streak).to.exist;
expect(streak).to.have.property('optionalCount')
.that.is.a('number');
expect(perfect).to.exist;
expect(perfect).to.have.property('optionalCount')
.that.is.a('number');
});
it('party up/on achievements exist with no counts', () => {
let partyUp = basicAchievs.partyUp;
let partyOn = basicAchievs.partyOn;
expect(partyUp).to.exist;
expect(partyUp.optionalCount).to.be.undefined;
expect(partyOn).to.exist;
expect(partyOn.optionalCount).to.be.undefined;
});
it('pet/mount master and triad bingo achievements exist with counts', () => {
let beastMaster = basicAchievs.beastMaster;
let mountMaster = basicAchievs.mountMaster;
let triadBingo = basicAchievs.triadBingo;
expect(beastMaster).to.exist;
expect(beastMaster).to.have.property('optionalCount')
.that.is.a('number');
expect(mountMaster).to.exist;
expect(mountMaster).to.have.property('optionalCount')
.that.is.a('number');
expect(triadBingo).to.exist;
expect(triadBingo).to.have.property('optionalCount')
.that.is.a('number');
});
it('ultimate gear achievements exist with no counts', () => {
let gearTypes = ['healer', 'rogue', 'warrior', 'mage'];
gearTypes.forEach((gear) => {
let gearAchiev = basicAchievs[`${gear}UltimateGear`];
expect(gearAchiev).to.exist;
expect(gearAchiev.optionalCount).to.be.undefined;
});
});
it('rebirth achievement exists with no count', () => {
let rebirth = basicAchievs.rebirth;
expect(rebirth).to.exist;
expect(rebirth.optionalCount).to.be.undefined;
});
});
describe('unearned seasonal achievements', () => {
let user = generateUser();
let seasonalAchievs = shared.achievements.getAchievementsForProfile(user).seasonal.achievements;
it('habiticaDays and habitBirthdays achievements exist with counts', () => {
let habiticaDays = seasonalAchievs.habiticaDays;
let habitBirthdays = seasonalAchievs.habitBirthdays;
expect(habiticaDays).to.exist;
expect(habiticaDays).to.have.property('optionalCount')
.that.is.a('number');
expect(habitBirthdays).to.exist;
expect(habitBirthdays).to.have.property('optionalCount')
.that.is.a('number');
});
it('spell achievements exist with counts', () => {
let spellTypes = ['snowball', 'spookySparkles', 'shinySeed', 'seafoam'];
spellTypes.forEach((spell) => {
let spellAchiev = seasonalAchievs[spell];
expect(spellAchiev).to.exist;
expect(spellAchiev).to.have.property('optionalCount')
.that.is.a('number');
});
});
it('quest achievements exist with counts', () => {
let quests = ['dilatory', 'stressbeast', 'burnout', 'bewilder'];
quests.forEach((quest) => {
let questAchiev = seasonalAchievs[`${quest}Quest`];
expect(questAchiev).to.exist;
expect(questAchiev.optionalCount).to.be.undefined;
});
});
it('costumeContests achievement exists with count', () => {
let costumeContests = seasonalAchievs.costumeContests;
expect(costumeContests).to.exist;
expect(costumeContests).to.have.property('optionalCount')
.that.is.a('number');
});
it('card achievements exist with counts', () => {
let cardTypes = ['greeting', 'thankyou', 'nye', 'valentine', 'birthday'];
cardTypes.forEach((card) => {
let cardAchiev = seasonalAchievs[`${card}Cards`];
expect(cardAchiev).to.exist;
expect(cardAchiev).to.have.property('optionalCount')
.that.is.a('number');
});
});
});
describe('unearned special achievements', () => {
let user = generateUser();
let specialAchievs = shared.achievements.getAchievementsForProfile(user).special.achievements;
it('habitSurveys achievement exists with count', () => {
let habitSurveys = specialAchievs.habitSurveys;
expect(habitSurveys).to.exist;
expect(habitSurveys).to.have.property('optionalCount')
.that.is.a('number');
});
it('contributor achievement exists with value and no count', () => {
let contributor = specialAchievs.contributor;
expect(contributor).to.exist;
expect(contributor).to.have.property('value')
.that.is.a('number');
expect(contributor.optionalCount).to.be.undefined;
});
it('npc achievement is hidden if unachieved', () => {
let npc = specialAchievs.npc;
expect(npc).to.not.exist;
});
it('kickstarter achievement is hidden if unachieved', () => {
let kickstarter = specialAchievs.kickstarter;
expect(kickstarter).to.not.exist;
});
it('veteran achievement is hidden if unachieved', () => {
let veteran = specialAchievs.veteran;
expect(veteran).to.not.exist;
});
it('originalUser achievement is hidden if unachieved', () => {
let originalUser = specialAchievs.originalUser;
expect(originalUser).to.not.exist;
});
});
describe('earned special achievements', () => {
let user = generateUser({
achievements: {
habitSurveys: 2,
veteran: true,
originalUser: true,
},
backer: {tier: 3},
contributor: {level: 1},
});
let specialAchievs = shared.achievements.getAchievementsForProfile(user).special.achievements;
it('habitSurveys achievement is earned with correct value', () => {
let habitSurveys = specialAchievs.habitSurveys;
expect(habitSurveys).to.exist;
expect(habitSurveys.earned).to.eql(true);
expect(habitSurveys.value).to.eql(2);
});
it('contributor achievement is earned with correct value', () => {
let contributor = specialAchievs.contributor;
expect(contributor).to.exist;
expect(contributor.earned).to.eql(true);
expect(contributor.value).to.eql(1);
});
it('npc achievement is earned with correct value', () => {
let npcUser = generateUser({
backer: {npc: 'test'},
});
let npc = shared.achievements.getAchievementsForProfile(npcUser).special.achievements.npc;
expect(npc).to.exist;
expect(npc.earned).to.eql(true);
expect(npc.value).to.eql('test');
});
it('kickstarter achievement is earned with correct value', () => {
let kickstarter = specialAchievs.kickstarter;
expect(kickstarter).to.exist;
expect(kickstarter.earned).to.eql(true);
expect(kickstarter.value).to.eql(3);
});
it('veteran achievement is earned', () => {
let veteran = specialAchievs.veteran;
expect(veteran).to.exist;
expect(veteran.earned).to.eql(true);
});
it('originalUser achievement is earned', () => {
let originalUser = specialAchievs.originalUser;
expect(originalUser).to.exist;
expect(originalUser.earned).to.eql(true);
});
});
describe('mountMaster, beastMaster, and triadBingo achievements', () => {
it('master and triad bingo achievements do not include *Text2 strings if no keys have been used', () => {
let user = generateUser();
let basicAchievs = shared.achievements.getAchievementsForProfile(user).basic.achievements;
let beastMaster = basicAchievs.beastMaster;
let mountMaster = basicAchievs.mountMaster;
let triadBingo = basicAchievs.triadBingo;
expect(beastMaster.text).to.not.match(/released/);
expect(beastMaster.text).to.not.match(/0 time\(s\)/);
expect(mountMaster.text).to.not.match(/released/);
expect(mountMaster.text).to.not.match(/0 time\(s\)/);
expect(triadBingo.text).to.not.match(/released/);
expect(triadBingo.text).to.not.match(/0 time\(s\)/);
});
it('master and triad bingo achievements includes *Text2 strings if keys have been used', () => {
let user = generateUser({
achievements: {
beastMasterCount: 1,
mountMasterCount: 2,
triadBingoCount: 3,
},
});
let basicAchievs = shared.achievements.getAchievementsForProfile(user).basic.achievements;
let beastMaster = basicAchievs.beastMaster;
let mountMaster = basicAchievs.mountMaster;
let triadBingo = basicAchievs.triadBingo;
expect(beastMaster.text).to.match(/released/);
expect(beastMaster.text).to.match(/1 time\(s\)/);
expect(mountMaster.text).to.match(/released/);
expect(mountMaster.text).to.match(/2 time\(s\)/);
expect(triadBingo.text).to.match(/released/);
expect(triadBingo.text).to.match(/3 time\(s\)/);
});
});
describe('ultimateGear achievements', () => {
it('title and text contain localized class info', () => {
let user = generateUser();
let basicAchievs = shared.achievements.getAchievementsForProfile(user).basic.achievements;
let gearTypes = ['healer', 'rogue', 'warrior', 'mage'];
gearTypes.forEach((gear) => {
let gearAchiev = basicAchievs[`${gear}UltimateGear`];
let classNameRegex = new RegExp(gear.charAt(0).toUpperCase() + gear.slice(1));
expect(gearAchiev.title).to.match(classNameRegex);
expect(gearAchiev.text).to.match(classNameRegex);
});
});
});
});

View File

@@ -50,6 +50,11 @@
margin-right: 10px;
}
.achievement .counter {
bottom: 0;
right: 10px;
}
.multi-achievement {
margin: auto;
padding-left: 0.5em;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -17,6 +17,10 @@ habitrpg
$scope.$watch( function() { return Members.selectedMember; }, function (member) {
if(member) {
$scope.profile = member;
$scope.achievements = Shared.achievements.getAchievementsForProfile($scope.profile);
$scope.achievPopoverPlacement = 'left';
$scope.achievAppendToBody = 'false'; // append-to-body breaks popovers in modal windows
}
});

View File

@@ -104,5 +104,9 @@ habitrpg.controller("UserCtrl", ['$rootScope', '$scope', '$location', 'User', '$
var nextRewardAt = currentLoginDay.nextRewardAt;
return ($scope.profile.loginIncentives - previousRewardDay)/(nextRewardAt - previousRewardDay) * 100;
};
$scope.achievements = Shared.achievements.getAchievementsForProfile($scope.profile);
$scope.achievPopoverPlacement = 'right';
$scope.achievAppendToBody = 'true'; // append-to-body breaks popovers in modal windows
}
]);

View File

@@ -94,7 +94,7 @@ angular.module("habitrpg").factory("Notification",
}
function streak(val) {
_notify(window.env.t('streakName') + ': ' + val, 'streak', 'glyphicon glyphicon-repeat');
_notify(window.env.t('streaks') + ': ' + val, 'streak', 'glyphicon glyphicon-repeat');
}
function text(val, onClick){

View File

@@ -62,8 +62,8 @@
"gearAchievement": "You have earned the \"Ultimate Gear\" Achievement for upgrading to the maximum gear set for a class! You have attained the following complete sets:",
"moreGearAchievements": "To attain more Ultimate Gear badges, change classes on <a href='/#/options/profile/stats' target='_blank'>your stats page</a> and buy up your new class's gear!",
"armoireUnlocked": "For more equipment, check out the <strong>Enchanted Armoire!</strong> Click on the Enchanted Armoire Reward for a random chance at special Equipment! It may also give you random XP or food items.",
"ultimGearName": "Ultimate Gear",
"ultimGearText": "Has upgraded to the maximum weapon and armor set for the following classes:",
"ultimGearName": "Ultimate Gear - <%= ultClass %>",
"ultimGearText": "Has upgraded to the maximum weapon and armor set for the <%= ultClass %> class.",
"level": "Level",
"levelUp": "Level Up!",
"gainedLevel": "You gained a level!",

View File

@@ -21,11 +21,11 @@
"contribModal": "<%= name %>, you awesome person! You're now a tier <%= level %> contributor for helping Habitica. See",
"contribLink": "what prizes you've earned for your contribution!",
"contribName": "Contributor",
"contribText": "Has contributed to Habitica (code, design, pixel art, legal advice, docs, etc). Want this badge? ",
"contribText": "Has contributed to Habitica (code, design, pixel art, legal advice, docs, etc). Want this badge? <a href='http://habitica.wikia.com/wiki/Contributing_to_Habitica' target='_blank'>Read more.</a>",
"readMore": "Read More",
"kickstartName": "Kickstarter Backer - $<%= tier %> Tier",
"kickstartName": "Kickstarter Backer - $<%= key %> Tier",
"kickstartText": "Backed the Kickstarter Project",
"helped": "Helped Habit Grow",
"helped": "Helped Habitica Grow",
"helpedText1": "Helped Habitica grow by filling out",
"helpedText2": "this survey.",
"hall": "Hall of Heroes",
@@ -59,7 +59,7 @@
"conLearnURL": "http://habitica.wikia.com/wiki/Contributing_to_Habitica",
"conRewardsURL": "http://habitica.wikia.com/wiki/Contributor_Rewards",
"surveysSingle": "Helped Habitica grow, either by filling out a survey or helping with a major testing effort. Thank you!",
"surveysMultiple": "Helped Habitica grow on <%= surveys %> occasions, either by filling out a survey or helping with a major testing effort. Thank you!",
"surveysMultiple": "Helped Habitica grow on <%= count %> occasions, either by filling out a survey or helping with a major testing effort. Thank you!",
"currentSurvey": "Current Survey",
"surveyWhen": "The badge will be awarded to all participants when surveys have been processed, in late March.",
"blurbInbox": "This is where your private messages are stored! You can send someone a message by clicking on the envelope icon next to their name in Tavern, Party, or Guild Chat. If you've received an inappropriate PM, you should email a screenshot of it to Lemoness (<a href=\"mailto:leslie@habitica.com\">leslie@habitica.com</a>)",

View File

@@ -44,6 +44,9 @@
"unorderedListMarkdown": "+ First item\n+ Second item\n+ Third item",
"code": "`code`",
"achievements": "Achievements",
"basicAchievs": "Basic Achievements",
"seasonalAchievs": "Seasonal Achievements",
"specialAchievs": "Special Achievements",
"modalAchievement": "Achievement!",
"special": "Special",
"site": "Site",
@@ -89,15 +92,15 @@
"originalUserText": "One of the <em>very</em> original early adopters. Talk about alpha tester!",
"habitBirthday": "Habitica Birthday Bash",
"habitBirthdayText": "Celebrated the Habitica Birthday Bash!",
"habitBirthdayPluralText": "Celebrated <%= number %> Habitica Birthday Bashes!",
"habitBirthdayPluralText": "Celebrated <%= count %> Habitica Birthday Bashes!",
"habiticaDay": "Habitica Naming Day",
"habiticaDaySingularText": "Celebrated Habitica's Naming Day! Thanks for being a fantastic user.",
"habiticaDayPluralText": "Celebrated <%= number %> Naming Days! Thanks for being a fantastic user.",
"habiticaDayPluralText": "Celebrated <%= count %> Naming Days! Thanks for being a fantastic user.",
"achievementDilatory": "Savior of Dilatory",
"achievementDilatoryText": "Helped defeat the Dread Drag'on of Dilatory during the 2014 Summer Splash Event!",
"costumeContest": "Costume Contestant",
"costumeContestText": "Participated in the Habitoween Costume Contest. See some of the entries <a href='http://blog.habitrpg.com/tagged/cosplay' target='_blank'>on the Habitica blog</a>!",
"costumeContestTextPlural": "Participated in <%= number %> Habitoween Costume Contests. See some of the entries <a href='http://blog.habitrpg.com/tagged/cosplay' target='_blank'>on the Habitica blog</a>!",
"costumeContestTextPlural": "Participated in <%= count %> Habitoween Costume Contests. See some of the entries <a href='http://blog.habitrpg.com/tagged/cosplay' target='_blank'>on the Habitica blog</a>!",
"memberSince": "- Member since",
"lastLoggedIn": "- Last logged in",
"notPorted": "This feature is not yet ported from the original site.",
@@ -159,7 +162,7 @@
"greeting2": "`waves frantically`",
"greeting3": "Hey you!",
"greetingCardAchievementTitle": "Cheery Chum",
"greetingCardAchievementText": "Hey! Hi! Hello! Sent or received <%= cards %> greeting cards.",
"greetingCardAchievementText": "Hey! Hi! Hello! Sent or received <%= count %> greeting cards.",
"thankyouCard": "Thank-You Card",
"thankyouCardExplanation": "You both receive the Greatly Grateful achievement!",
"thankyouCardNotes": "Send a Thank-You card to a party member.",
@@ -168,13 +171,13 @@
"thankyou2": "Sending you a thousand thanks.",
"thankyou3": "I'm very grateful - thank you!",
"thankyouCardAchievementTitle": "Greatly Grateful",
"thankyouCardAchievementText": "Thanks for being thankful! Sent or received <%= cards %> Thank-You cards.",
"thankyouCardAchievementText": "Thanks for being thankful! Sent or received <%= count %> Thank-You cards.",
"birthdayCard": "Birthday Card",
"birthdayCardExplanation": "You both receive the Birthday Bonanza achievement!",
"birthdayCardNotes": "Send a birthday card to a party member.",
"birthday0": "Happy birthday to you!",
"birthdayCardAchievementTitle": "Birthday Bonanza",
"birthdayCardAchievementText": "Many happy returns! Sent or received <%= cards %> birthday cards.",
"birthdayCardAchievementText": "Many happy returns! Sent or received <%= count %> birthday cards.",
"streakAchievement": "You earned a streak achievement!",
"firstStreakAchievement": "21-Day Streak",
"streakAchievementCount": "<%= streaks %> 21-Day Streaks",

View File

@@ -172,8 +172,8 @@
"requestAcceptGuidelines": "If you would like to post messages in the Tavern or any party or guild chat, please first read our <%= linkStart %>Community Guidelines<%= linkEnd %> and then click the button below to indicate that you accept them.",
"partyUpName": "Party Up",
"partyOnName": "Party On",
"partyUpAchievement": "Joined a Party with another person! Have fun battling monsters and supporting each other.",
"partyOnAchievement": "Joined a Party with at least four people! Enjoy your increased accountability as you unite with your friends to vanquish your foes!",
"partyUpText": "Joined a Party with another person! Have fun battling monsters and supporting each other.",
"partyOnText": "Joined a Party with at least four people! Enjoy your increased accountability as you unite with your friends to vanquish your foes!",
"largeGroupNote": "Note: This Guild is now too large to support notifications! Be sure to check back every day to see new messages.",
"groupIdRequired": "\"groupId\" must be a valid UUID",
"groupNotFound": "Group not found or you don't have access.",

View File

@@ -3,13 +3,13 @@
"seasonalEdition": "Seasonal Edition",
"winterColors": "Winter Colors",
"annoyingFriends": "Annoying Friends",
"annoyingFriendsText": "Got snowballed <%= snowballs %> times by party members.",
"annoyingFriendsText": "Got snowballed <%= count %> times by party members.",
"alarmingFriends": "Alarming Friends",
"alarmingFriendsText": "Got spooked <%= spookySparkles %> times by party members.",
"alarmingFriendsText": "Got spooked <%= count %> times by party members.",
"agriculturalFriends": "Agricultural Friends",
"agriculturalFriendsText": "Got transformed into a flower <%= seeds %> times by party members.",
"agriculturalFriendsText": "Got transformed into a flower <%= count %> times by party members.",
"aquaticFriends": "Aquatic Friends",
"aquaticFriendsText": "Got splashed <%= seafoam %> times by party members.",
"aquaticFriendsText": "Got splashed <%= count %> times by party members.",
"valentineCard": "Valentine's Day Card",
"valentineCardExplanation": "For enduring such a saccharine poem, you both receive the \"Adoring Friends\" badge!",
"valentineCardNotes": "Send a Valentine's Day card to a party member.",
@@ -18,7 +18,7 @@
"valentine2": "\"Roses are red\n\nThis poem style is old\n\nI hope that you like this\n\n'Cause it cost ten Gold.\"",
"valentine3": "\"Roses are red\n\nIce Drakes are blue\n\nNo treasure is better\n\nThan time spent with you!\"",
"valentineCardAchievementTitle": "Adoring Friends",
"valentineCardAchievementText": "Aww, you and your friend must really care about each other! Sent or received <%= cards %> Valentine's Day cards.",
"valentineCardAchievementText": "Aww, you and your friend must really care about each other! Sent or received <%= count %> Valentine's Day cards.",
"polarBear": "Polar Bear",
"turkey": "Turkey",
"gildedTurkey": "Gilded Turkey",
@@ -49,7 +49,7 @@
"nyeCardNotes": "Send a New Year's card to a party member.",
"seasonalItems": "Seasonal Items",
"nyeCardAchievementTitle": "Auld Acquaintance",
"nyeCardAchievementText": "Happy New Year! Sent or received <%= cards %> New Year's cards.",
"nyeCardAchievementText": "Happy New Year! Sent or received <%= count %> New Year's cards.",
"nye0": "Happy New Year! May you slay many a bad Habit.",
"nye1": "Happy New Year! May you reap many Rewards.",
"nye2": "Happy New Year! May you earn many a Perfect Day.",

View File

@@ -1,5 +1,6 @@
{
"npc": "NPC",
"npcAchievementName": "<%= key %> NPC",
"npcAchievementText": "Backed the Kickstarter project at the maximum level!",
"mattBoch": "Matt Boch",
"mattShall": "Shall I bring you your steed, <%= name %>? Once you've fed a pet enough food to turn it into a mount, it will appear here. Click a mount to saddle up!",

View File

@@ -18,8 +18,9 @@
"rebirthAchievement100": "You've begun a new adventure! This is Rebirth <%= number %> for you, and the highest Level you've attained is 100 or higher. To stack this Achievement, begin your next new adventure when you've reached at least 100!",
"rebirthBegan": "Began a New Adventure",
"rebirthText": "Began <%= rebirths %> New Adventures",
"rebirthOrb": "Used an Orb of Rebirth to start over after attaining Level",
"rebirthOrb100": "Used an Orb of Rebirth to start over after attaining Level 100 or higher",
"rebirthOrb": "Used an Orb of Rebirth to start over after attaining Level <%= level %>.",
"rebirthOrb100": "Used an Orb of Rebirth to start over after attaining Level 100 or higher.",
"rebirthOrbNoLevel": "Used an Orb of Rebirth to start over.",
"rebirthPop": "Begin a new character at Level 1 while retaining achievements, collectibles, equipment, and tasks with history.",
"rebirthName": "Orb of Rebirth",
"reborn": "Reborn, max level <%= reLevel %>",

View File

@@ -78,12 +78,13 @@
"startDate": "Start Date",
"startDateHelpTitle": "When should this task start?",
"startDateHelp": "Set the date for which this task takes effect. Will not be due on earlier days.",
"streakName": "Streak Achievements",
"streakText": "Has performed <%= streaks %> 21-day streaks on Dailies",
"streaks": "Streak Achievements",
"streakName": "<%= count %> Streak Achievements",
"streakText": "Has performed <%= count %> 21-day streaks on Dailies",
"streakSingular": "Streaker",
"streakSingularText": "Has performed a 21-day streak on a Daily",
"perfectName": "Perfect Days",
"perfectText": "Completed all active Dailies on <%= perfects %> days. With this achievement you get a +level/2 buff to all attributes for the next day. Levels greater than 100 don't have any additional effects on buffs.",
"perfectName": "<%= count %> Perfect Days",
"perfectText": "Completed all active Dailies on <%= count %> days. With this achievement you get a +level/2 buff to all attributes for the next day. Levels greater than 100 don't have any additional effects on buffs.",
"perfectSingular": "Perfect Day",
"perfectSingularText": "Completed all active Dailies in one day. With this achievement you get a +level/2 buff to all attributes for the next day. Levels greater than 100 don't have any additional effects on buffs.",
"streakerAchievement": "You have attained the \"Streaker\" Achievement! The 21-day mark is a milestone for habit formation. You can continue to stack this Achievement for every additional 21 days, on this Daily or any other!",

View File

@@ -0,0 +1,188 @@
import { each } from 'lodash';
let achievementsData = {};
let worldQuestAchievs = {
dilatoryQuest: {
icon: 'achievement-dilatory',
titleKey: 'achievementDilatory',
textKey: 'achievementDilatoryText',
},
stressbeastQuest: {
icon: 'achievement-stoikalm',
titleKey: 'achievementStressbeast',
textKey: 'achievementStressbeastText',
},
burnoutQuest: {
icon: 'achievement-burnout',
titleKey: 'achievementBurnout',
textKey: 'achievementBurnoutText',
},
bewilderQuest: {
icon: 'achievement-bewilder',
titleKey: 'achievementBewilder',
textKey: 'achievementBewilderText',
},
};
Object.assign(achievementsData, worldQuestAchievs);
let seasonalSpellAchievs = {
snowball: {
icon: 'achievement-snowball',
titleKey: 'annoyingFriends',
textKey: 'annoyingFriendsText',
},
spookySparkles: {
icon: 'achievement-spookySparkles',
titleKey: 'alarmingFriends',
textKey: 'alarmingFriendsText',
},
shinySeed: {
icon: 'achievement-shinySeed',
titleKey: 'agriculturalFriends',
textKey: 'agriculturalFriendsText',
},
seafoam: {
icon: 'achievement-seafoam',
titleKey: 'aquaticFriends',
textKey: 'aquaticFriendsText',
},
};
Object.assign(achievementsData, seasonalSpellAchievs);
let masterAchievs = {
beastMaster: {
icon: 'achievement-rat',
titleKey: 'beastMasterName',
textKey: 'beastMasterText',
text2Key: 'beastMasterText2',
},
mountMaster: {
icon: 'achievement-wolf',
titleKey: 'mountMasterName',
textKey: 'mountMasterText',
text2Key: 'mountMasterText2',
},
triadBingo: {
icon: 'achievement-triadbingo',
titleKey: 'triadBingoName',
textKey: 'triadBingoText',
text2Key: 'triadBingoText2',
},
};
Object.assign(achievementsData, masterAchievs);
let basicAchievs = {
partyUp: {
icon: 'achievement-partyUp',
titleKey: 'partyUpName',
textKey: 'partyUpText',
},
partyOn: {
icon: 'achievement-partyOn',
titleKey: 'partyOnName',
textKey: 'partyOnText',
},
streak: {
icon: 'achievement-thermometer',
singularTitleKey: 'streakSingular',
singularTextKey: 'streakSingularText',
pluralTitleKey: 'streakName',
pluralTextKey: 'streakText',
},
perfect: {
icon: 'achievement-perfect',
singularTitleKey: 'perfectSingular',
singularTextKey: 'perfectSingularText',
pluralTitleKey: 'perfectName',
pluralTextKey: 'perfectText',
},
};
Object.assign(achievementsData, basicAchievs);
let specialAchievs = {
contributor: {
icon: 'achievement-boot',
titleKey: 'contribName',
textKey: 'contribText',
},
npc: {
icon: 'achievement-ultimate-warrior',
titleKey: 'npcAchievementName',
textKey: 'npcAchievementText',
},
kickstarter: {
icon: 'achievement-heart',
titleKey: 'kickstartName',
textKey: 'kickstartText',
},
veteran: {
icon: 'achievement-cake',
titleKey: 'veteran',
textKey: 'veteranText',
},
originalUser: {
icon: 'achievement-alpha',
titleKey: 'originalUser',
textKey: 'originalUserText',
},
habitSurveys: {
icon: 'achievement-tree',
singularTitleKey: 'helped',
singularTextKey: 'surveysSingle',
pluralTitleKey: 'helped',
pluralTextKey: 'surveysMultiple',
},
};
Object.assign(achievementsData, specialAchievs);
let holidayAchievs = {
habiticaDays: {
icon: 'achievement-habiticaDay',
singularTitleKey: 'habiticaDay',
singularTextKey: 'habiticaDaySingularText',
pluralTitleKey: 'habiticaDay',
pluralTextKey: 'habiticaDayPluralText',
},
habitBirthdays: {
icon: 'achievement-habitBirthday',
singularTitleKey: 'habitBirthday',
singularTextKey: 'habitBirthdayText',
pluralTitleKey: 'habitBirthday',
pluralTextKey: 'habitBirthdayPluralText',
},
costumeContests: {
icon: 'achievement-costumeContest',
singularTitleKey: 'costumeContest',
singularTextKey: 'costumeContestText',
pluralTitleKey: 'costumeContest',
pluralTextKey: 'costumeContestTextPlural',
},
};
Object.assign(achievementsData, holidayAchievs);
let ultimateGearAchievs = ['healer', 'rogue', 'warrior', 'mage'].reduce((achievs, type) => {
achievs[`${type}UltimateGear`] = {
icon: `achievement-ultimate-${type}`,
titleKey: 'ultimGearName',
textKey: 'ultimGearText',
};
return achievs;
}, {});
Object.assign(achievementsData, ultimateGearAchievs);
let cardAchievs = ['greeting', 'thankyou', 'nye', 'valentine', 'birthday'].reduce((achievs, type) => {
achievs[`${type}Cards`] = {
icon: `achievement-${type}`,
titleKey: `${type}CardAchievementTitle`,
textKey: `${type}CardAchievementText`,
};
return achievs;
}, {});
Object.assign(achievementsData, cardAchievs);
each(achievementsData, (value, key) => {
value.key = key;
});
module.exports = achievementsData;

View File

@@ -12,6 +12,8 @@ import {
let api = module.exports;
import achievements from './achievements';
import mysterySets from './mystery-sets';
import eggs from './eggs';
@@ -25,6 +27,8 @@ import spells from './spells';
import faq from './faq';
import loginIncentives from './loginIncentives';
api.achievements = achievements;
api.mystery = mysterySets;
api.itemList = ITEM_LIST;

View File

@@ -93,6 +93,9 @@ api.statsComputed = statsComputed;
import shops from './libs/shops';
api.shops = shops;
import achievements from './libs/achievements';
api.achievements = achievements;
import randomVal from './libs/randomVal';
api.randomVal = randomVal;

View File

@@ -0,0 +1,306 @@
import content from '../content/index';
import i18n from '../i18n';
import { get } from 'lodash';
let achievs = {};
let achievsContent = content.achievements;
let index = 0;
function contribText (contrib, backer, language) {
if (!contrib && !backer) return;
if (backer && backer.npc) return backer.npc;
let lvl = contrib && contrib.level;
if (lvl && lvl > 0) {
let contribTitle = '';
if (lvl < 3) {
contribTitle = i18n.t('friend', language);
} else if (lvl < 5) {
contribTitle = i18n.t('elite', language);
} else if (lvl < 7) {
contribTitle = i18n.t('champion', language);
} else if (lvl < 8) {
contribTitle = i18n.t('legendary', language);
} else if (lvl < 9) {
contribTitle = i18n.t('guardian', language);
} else {
contribTitle = i18n.t('heroic', language);
}
return `${contribTitle} ${contrib.text}`;
}
}
function _add (result, data) {
result[data.key] = {
title: data.title,
text: data.text,
icon: data.icon,
earned: data.earned,
value: data.value,
index: index++,
optionalCount: data.optionalCount,
};
}
function _addSimpleWithCustomPath (result, user, data) {
let value = get(user, data.path);
let thisContent = achievsContent[data.key];
_add(result, {
title: i18n.t(thisContent.titleKey, {key: value}, data.language),
text: i18n.t(thisContent.textKey, data.language),
icon: thisContent.icon,
key: data.key,
value,
earned: Boolean(value),
});
}
function _addQuest (result, user, data) {
data.key = `${data.path}Quest`;
data.path = `achievements.quests.${data.path}`;
_addSimpleWithCustomPath(result, user, data);
}
function _addSimple (result, user, data) {
let value = user.achievements[data.path];
let key = data.key || data.path;
let thisContent = achievsContent[key];
_add(result, {
title: i18n.t(thisContent.titleKey, data.language),
text: i18n.t(thisContent.textKey, data.language),
icon: thisContent.icon,
key,
value,
earned: Boolean(value),
});
}
function _addSimpleWithMasterCount (result, user, data) {
let language = data.language;
let value = user.achievements[`${data.path}Count`] || 0;
let thisContent = achievsContent[data.path];
let text = i18n.t(thisContent.textKey, language);
if (value > 0) {
text += i18n.t(thisContent.text2Key, {count: value}, language);
}
_add(result, {
title: i18n.t(thisContent.titleKey, language),
text,
icon: thisContent.icon,
key: data.path,
value,
optionalCount: value,
earned: Boolean(value),
});
}
function _addSimpleWithCount (result, user, data) {
let value = user.achievements[data.path] || 0;
let key = data.key || data.path;
let thisContent = achievsContent[key];
_add(result, {
title: i18n.t(thisContent.titleKey, data.language),
text: i18n.t(thisContent.textKey, {count: value}, data.language),
icon: thisContent.icon,
key,
value,
optionalCount: value,
earned: Boolean(value),
});
}
function _addPlural (result, user, data) {
let value = user.achievements[data.path] || 0;
let key = data.key || data.path;
let thisContent = achievsContent[key];
let titleKey;
let textKey;
// If value === 1, use singular versions of strings.
// If value !== 1, use plural versions of strings.
if (value === 1) {
titleKey = thisContent.singularTitleKey;
textKey = thisContent.singularTextKey;
} else {
titleKey = thisContent.pluralTitleKey;
textKey = thisContent.pluralTextKey;
}
_add(result, {
title: i18n.t(titleKey, {count: value}, data.language),
text: i18n.t(textKey, {count: value}, data.language),
icon: thisContent.icon,
key,
value,
optionalCount: value,
earned: Boolean(value),
});
}
function _addUltimateGear (result, user, data) {
if (!data.altPath) {
data.altPath = data.path;
}
let value = user.achievements.ultimateGearSets[data.altPath];
let key = `${data.path}UltimateGear`;
let thisContent = achievsContent[key];
let localizedClass = i18n.t(data.path, data.language);
let title = i18n.t(thisContent.titleKey, {ultClass: localizedClass}, data.language);
let text = i18n.t(thisContent.textKey, {ultClass: localizedClass}, data.language);
_add(result, {
title,
text,
icon: thisContent.icon,
key,
value,
earned: Boolean(value),
});
}
function _getBasicAchievements (user, language) {
let result = {};
_addPlural(result, user, {path: 'streak', language});
_addPlural(result, user, {path: 'perfect', language});
_addSimple(result, user, {path: 'partyUp', language});
_addSimple(result, user, {path: 'partyOn', language});
_addSimpleWithMasterCount(result, user, {path: 'beastMaster', language});
_addSimpleWithMasterCount(result, user, {path: 'mountMaster', language});
_addSimpleWithMasterCount(result, user, {path: 'triadBingo', language});
_addUltimateGear(result, user, {path: 'healer', language});
_addUltimateGear(result, user, {path: 'rogue', language});
_addUltimateGear(result, user, {path: 'warrior', language});
_addUltimateGear(result, user, {path: 'mage', altpath: 'wizard', language});
let rebirthTitle;
let rebirthText;
if (user.achievements.rebirths > 1) {
rebirthTitle = i18n.t('rebirthText', {rebirths: user.achievements.rebirths}, language);
} else {
rebirthTitle = i18n.t('rebirthBegan', language);
}
if (!user.achievements.rebirthLevel) {
rebirthText = i18n.t('rebirthOrbNoLevel', language);
} else if (user.achievements.rebirthLevel < 100) {
rebirthText = i18n.t('rebirthOrb', {level: user.achievements.rebirthLevel}, language);
} else {
rebirthText = i18n.t('rebirthOrb100', language);
}
_add(result, {
key: 'rebirth',
title: rebirthTitle,
text: rebirthText,
icon: 'achievement-sun',
earned: Boolean(user.achievements.rebirths),
});
return result;
}
function _getSeasonalAchievements (user, language) {
let result = {};
_addPlural(result, user, {path: 'habiticaDays', language});
_addPlural(result, user, {path: 'habitBirthdays', language});
let spellAchievements = ['snowball', 'spookySparkles', 'shinySeed', 'seafoam'];
spellAchievements.forEach(path => {
_addSimpleWithCount(result, user, {path, language});
});
let questAchievements = ['dilatory', 'stressbeast', 'burnout', 'bewilder'];
questAchievements.forEach(path => {
_addQuest(result, user, {path, language});
});
_addPlural(result, user, {path: 'costumeContests', language});
let cardAchievements = ['greeting', 'thankyou', 'nye', 'valentine', 'birthday'];
cardAchievements.forEach(path => {
_addSimpleWithCount(result, user, {path, key: `${path}Cards`, language});
});
return result;
}
function _getSpecialAchievements (user, language) {
let result = {};
_addPlural(result, user, {path: 'habitSurveys', language});
let contribKey = 'contributor';
let contribContent = achievsContent[contribKey];
let contributorAchiev = {
key: contribKey,
text: i18n.t(contribContent.textKey, language),
icon: contribContent.icon,
earned: Boolean(user.contributor && user.contributor.level),
};
if (user.contributor && user.contributor.level) {
contributorAchiev.value = user.contributor.level;
contributorAchiev.title = contribText(user.contributor, user.backer, language);
} else {
contributorAchiev.value = 0;
contributorAchiev.title = i18n.t(contribContent.titleKey, language);
}
_add(result, contributorAchiev);
if (user.backer && user.backer.npc) {
_addSimpleWithCustomPath(result, user, {key: 'npc', path: 'backer.npc', language});
}
if (user.backer && user.backer.tier) {
_addSimpleWithCustomPath(result, user, {key: 'kickstarter', path: 'backer.tier', language});
}
if (user.achievements.veteran) {
_addSimple(result, user, {path: 'veteran', language});
}
if (user.achievements.originalUser) {
_addSimple(result, user, {path: 'originalUser', language});
}
return result;
}
// Build and return the given user's achievement data.
achievs.getAchievementsForProfile = function getAchievementsForProfile (user, language) {
let result = {
basic: {
label: 'Basic',
achievements: _getBasicAchievements(user, language),
},
seasonal: {
label: 'Seasonal',
achievements: _getSeasonalAchievements(user, language),
},
special: {
label: 'Special',
achievements: _getSpecialAchievements(user, language),
},
};
return result;
};
module.exports = achievs;

View File

@@ -17,6 +17,7 @@ import {
} from '../../libs/email';
import Bluebird from 'bluebird';
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
import { achievements } from '../../../../website/common/';
let api = {};
@@ -58,6 +59,113 @@ api.getMember = {
},
};
/**
* @api {get} /api/v3/members/:memberId/achievements Get member achievements object
* @apiName GetMemberAchievements
* @apiGroup Member
* @apiDescription Get a list of achievements of the requested member, grouped by basic / seasonal / special.
*
* @apiParam (Path) {UUID} memberId The member's id
*
* @apiSuccess {Object} data The achievements object
*
* @apiSuccess {Object} data.basic The basic achievements object
* @apiSuccess {Object} data.seasonal The seasonal achievements object
* @apiSuccess {Object} data.special The special achievements object
*
* @apiSuccess {String} data.*.label The label for that category
* @apiSuccess {Object} data.*.achievements The achievements in that category
*
* @apiSuccess {String} data.*.achievements.title The localized title string
* @apiSuccess {String} data.*.achievements.text The localized description string
* @apiSuccess {Boolean} data.*.achievements.earned Whether the user has earned the achievement
* @apiSuccess {Number} data.*.achievements.index The unique index assigned to the achievement (only for sorting purposes)
* @apiSuccess {Anything} data.*.achievements.value The value related to the achievement (if applicable)
* @apiSuccess {Number} data.*.achievements.optionalCount The count related to the achievement (if applicable)
*
* @apiSuccessExample {json} Successful Response
* {
* basic: {
* label: "Basic",
* achievements: {
* streak: {
* title: "0 Streak Achievements",
* text: "Has performed 0 21-day streaks on Dailies",
* icon: "achievement-thermometer",
* earned: false,
* value: 0,
* index: 60,
* optionalCount: 0
* },
* perfect: {
* title: "5 Perfect Days",
* text: "Completed all active Dailies on 5 days. With this achievement you get a +level/2 buff to all attributes for the next day. Levels greater than 100 don't have any additional effects on buffs.",
* icon: "achievement-perfect",
* earned: true,
* value: 5,
* index: 61,
* optionalCount: 5
* }
* }
* },
* seasonal: {
* label: "Seasonal",
* achievements: {
* habiticaDays: {
* title: "Habitica Naming Day",
* text: "Celebrated 0 Naming Days! Thanks for being a fantastic user.",
* icon: "achievement-habiticaDay",
* earned: false,
* value: 0,
* index: 72,
* optionalCount: 0
* }
* }
* },
* special: {
* label: "Special",
* achievements: {
* habitSurveys: {
* title: "Helped Habitica Grow",
* text: "Helped Habitica grow on 0 occasions, either by filling out a survey or helping with a major testing effort. Thank you!",
* icon: "achievement-tree",
* earned: false,
* value: 0,
* index: 88,
* optionalCount: 0
* }
* }
* }
* }
*
* @apiError (400) {BadRequest} MemberIdRequired The `id` param is required and must be a valid `UUID`
* @apiError (404) {NotFound} UserWithIdNotFound The `id` param did not belong to an existing member
*/
api.getMemberAchievements = {
method: 'GET',
url: '/members/:memberId/achievements',
middlewares: [],
async handler (req, res) {
req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let memberId = req.params.memberId;
let member = await User
.findById(memberId)
.select(memberFields)
.exec();
if (!member) throw new NotFound(res.t('userWithIDNotFound', {userId: memberId}));
let achievsObject = achievements.getAchievementsForProfile(member, req.language);
res.respond(200, achievsObject);
},
};
// Return a request handler for getMembersForGroup / getInvitesForGroup / getMembersForChallenge
// type is `invites` or `members`
function _getMembersForItem (type) {

View File

@@ -1,268 +1,31 @@
if mobile
.item.item-divider=env.t('achievements')
mixin simpleAchiev(achiev)
- var popoverHtml = '<div class="{{earnedClass}}">{{achiev.title}}<hr>{{achiev.text}}</div>';
div(ng-init='earnedClass = achiev.earned ? "" : "muted"',
data-popover-html='#{popoverHtml}',
popover-placement='{{achievPopoverPlacement}}',
popover-append-to-body='{{achievAppendToBody}}')&attributes(attributes)
button.pet-button(popover-trigger='mouseenter',
data-popover-html='#{popoverHtml}',
popover-placement='{{achievPopoverPlacement}}',
popover-append-to-body='{{achievAppendToBody}}')
div(ng-if='::profile.achievements.habitSurveys || user._id == profile._id')
.achievement.achievement-tree(ng-if='::profile.achievements.habitSurveys')
div(ng-class='::{muted: !profile.achievements.habitSurveys}')
h5=env.t('helped')
small(ng-if='::profile.achievements.habitSurveys > 1')
=env.t('surveysMultiple', {surveys: "{{::profile.achievements.habitSurveys}}"})
small(ng-if='::!(profile.achievements.habitSurveys > 1)')
=env.t('surveysSingle')
hr
.achievement(ng-class='achiev.icon + "2x"', ng-if='achiev.earned')
.counter.badge.badge-info.stack-count(ng-if='(achiev.optionalCount)')
{{::achiev.optionalCount}}
.achievement(class='achievement-unearned2x', ng-if='!(achiev.earned)')
div(ng-if='::profile.backer.npc')
.achievement.achievement-helm
h5
span.label.label-npc
| {{::profile.backer.npc}}
=env.t('npc')
small=env.t('npcAchievementText')
hr
div(ng-if='::profile.contributor.level || user._id == profile._id')
.achievement.achievement-boot(ng-if='::profile.contributor.level')
div(ng-class='::{muted: !profile.contributor.level}')
h5
span.label.label-default(ng-if='::profile.contributor.level', class='label-contributor-{{::profile.contributor.level}}') {{::contribText(profile.contributor, profile.backer)}}
span.label.label-default(ng-if='::!profile.contributor.level')=env.t('contribName')
small
=env.t('contribText')
|&nbsp;
a(href=env.t('conLearnURL'), target='_blank')
=env.t('readMore')
| .
hr
.container-fluid
.row
.col-md-12(ng-repeat='(key,cat) in achievements', ng-init='heading=env.t(key+"Achievs")')
h4 {{heading}}
menu.pets.inventory-list(type='list')
li.customize-menu
menu
div(ng-repeat='achiev in cat.achievements | toArray | orderBy: "index"')
+simpleAchiev('achiev')
div(ng-if='::profile.backer.tier')
.achievement.achievement-heart
h5=env.t('kickstartName', {tier: "{{::profile.backer.tier}}"})
small=env.t('kickstartText')
hr
hr
div(ng-if='::profile.achievements.streak || user._id == profile._id')
.achievement.achievement-thermometer(ng-if='::profile.achievements.streak')
div(ng-class='::{muted: !profile.achievements.streak}')
h5(ng-if='::profile.achievements.streak > 1 || !profile.achievements.streak')
| {{::profile.achievements.streak || 0 }}&nbsp;
=env.t('streakName')
small(ng-if='::profile.achievements.streak > 1 || !profile.achievements.streak')=env.t('streakText', {streaks: "{{::profile.achievements.streak || 0 }}"})
h5(ng-if='::profile.achievements.streak == 1')
=env.t('streakSingular')
small(ng-if='::profile.achievements.streak == 1')=env.t('streakSingularText')
hr
div(ng-if='::profile.achievements.perfect || user._id == profile._id')
.achievement.achievement-perfect(ng-if='::profile.achievements.perfect')
div(ng-class='::{muted: !profile.achievements.perfect}')
h5(ng-if='::profile.achievements.perfect > 1 || !profile.achievements.perfect')
| {{::profile.achievements.perfect || 0 }}&nbsp;
=env.t('perfectName')
small(ng-if='::profile.achievements.perfect > 1 || !profile.achievements.perfect')=env.t('perfectText', {perfects: "{{::profile.achievements.perfect || 0 }}"})
h5(ng-if='::profile.achievements.perfect == 1')
=env.t('perfectSingular')
small(ng-if='::profile.achievements.perfect == 1')=env.t('perfectSingularText')
hr
- var ultimateGearCheck = 'profile.achievements.ultimateGearSets.healer || profile.achievements.ultimateGearSets.wizard || profile.achievements.ultimateGearSets.rogue || profile.achievements.ultimateGearSets.warrior'
div(ng-if='(user._id == profile._id) || #{ultimateGearCheck}')
.achievement.achievement-armor(ng-if='#{ultimateGearCheck}')
div(ng-class='::{muted: !(#{ultimateGearCheck})}')
h5=env.t('ultimGearName')
small=env.t('ultimGearText')
table.multi-achievement
tr
td(ng-if='::profile.achievements.ultimateGearSets.healer').multi-achievement
.achievement-ultimate-healer.multi-achievement
=env.t('healer')
td(ng-if='::profile.achievements.ultimateGearSets.wizard').multi-achievement
.achievement-ultimate-mage.multi-achievement
=env.t('mage')
td(ng-if='::profile.achievements.ultimateGearSets.rogue').multi-achievement
.achievement-ultimate-rogue.multi-achievement
=env.t('rogue')
td(ng-if='::profile.achievements.ultimateGearSets.warrior').multi-achievement
.achievement-ultimate-warrior.multi-achievement
=env.t('warrior')
hr
div(ng-if='::profile.achievements.partyUp || user._id == profile._id')
.achievement.achievement-partyUp(ng-if='::profile.achievements.partyUp')
div(ng-class='::{muted: !profile.achievements.partyUp}')
h5=env.t('partyUpName')
small=env.t('partyUpAchievement')
hr
div(ng-if='::profile.achievements.partyOn || user._id == profile._id')
.achievement.achievement-partyOn(ng-if='::profile.achievements.partyOn')
div(ng-class='::{muted: !profile.achievements.partyOn}')
h5=env.t('partyOnName')
small=env.t('partyOnAchievement')
hr
div(ng-if='::profile.achievements.beastMaster || user._id == profile._id')
.achievement.achievement-rat(ng-if='::profile.achievements.beastMaster')
div(ng-class='::{muted: !profile.achievements.beastMaster}')
h5=env.t('beastMasterName')
small=env.t('beastMasterText')
small(ng-if='::profile.achievements.beastMasterCount')
=env.t('beastMasterText2', {count: "{{::profile.achievements.beastMasterCount}}"})
hr
div(ng-if='::profile.achievements.mountMaster || user._id == profile._id')
.achievement.achievement-wolf(ng-if='::profile.achievements.mountMaster')
div(ng-class='::{muted: !profile.achievements.mountMaster}')
h5=env.t('mountMasterName')
small=env.t('mountMasterText')
small(ng-if='::profile.achievements.mountMasterCount')
=env.t('mountMasterText2', {count: "{{::profile.achievements.mountMasterCount}}"})
hr
div(ng-if='::profile.achievements.triadBingo || user._id == profile._id')
.achievement.achievement-triadbingo(ng-if='::profile.achievements.triadBingo')
div(ng-class='::{muted: !profile.achievements.triadBingo}')
h5=env.t('triadBingoName')
small=env.t('triadBingoText')
small(ng-if='::profile.achievements.triadBingoCount')
=env.t('triadBingoText2', {count: "{{::profile.achievements.triadBingoCount}}"})
hr
div(ng-if='::profile.achievements.rebirths')
.achievement.achievement-sun
h5(ng-if='::profile.achievements.rebirths == 1')=env.t('rebirthBegan')
h5(ng-if='::profile.achievements.rebirths > 1')
=env.t('rebirthText', {rebirths: "{{::profile.achievements.rebirths}}"})
small(ng-if='::profile.achievements.rebirthLevel < 100')
=env.t('rebirthOrb')
|&nbsp;{{::profile.achievements.rebirthLevel}}.
small(ng-if='::profile.achievements.rebirthLevel >= 100')
=env.t('rebirthOrb100')
hr
div(ng-if='::profile.achievements.veteran')
.achievement.achievement-cake
div(ng-if='::profile.achievements.veteran')
h5=env.t('veteran')
small=env.t('veteranText')
hr
div(ng-if='::profile.achievements.originalUser')
.achievement.achievement-alpha
div(ng-if='::profile.achievements.originalUser')
h5=env.t('originalUser')
small!=env.t('originalUserText')
hr
div(ng-if='::profile.achievements.challenges.length || user._id == profile._id')
// This is a very strange icon to use. revisit
.achievement.achievement-karaoke(ng-if='::profile.achievements.challenges.length')
div(ng-class='::{muted: !profile.achievements.challenges.length}')
h5=env.t('challengeWinner')
table.table.table-striped
tr(ng-repeat='chal in profile.achievements.challenges track by $index')
td: markdown(text='::chal')
hr
div(ng-if='::profile.achievements.quests || user._id == profile._id')
.achievement.achievement-alien(ng-if='::profile.achievements.quests')
div(ng-class='::{muted: !profile.achievements.quests}')
h5=env.t('completedQuests')
table.table.table-striped
tr(ng-repeat='(k,v) in profile.achievements.quests')
td {{::Content.quests[k].text()}}
td x{{::v}}
hr
div(ng-if='::profile.achievements.snowball')
.achievement.achievement-snowball
h5=env.t('annoyingFriends')
small
=env.t('annoyingFriendsText', {snowballs: "{{::profile.achievements.snowball}}"})
hr
div(ng-if='::profile.achievements.spookySparkles')
.achievement.achievement-spookySparkles
h5=env.t('alarmingFriends')
small
=env.t('alarmingFriendsText', {spookySparkles: "{{::profile.achievements.spookySparkles}}"})
hr
div(ng-if='::profile.achievements.shinySeed')
.achievement.achievement-shinySeed
h5=env.t('agriculturalFriends')
small
=env.t('agriculturalFriendsText', {seeds: "{{::profile.achievements.shinySeed}}"})
hr
div(ng-if='::profile.achievements.seafoam')
.achievement.achievement-seafoam
h5=env.t('aquaticFriends')
small
=env.t('aquaticFriendsText', {seafoam: "{{::profile.achievements.seafoam}}"})
hr
div(ng-if='::profile.achievements.habiticaDays')
.achievement.achievement-habiticaDay
h5=env.t('habiticaDay')
small(ng-if='::profile.achievements.habiticaDays == 1')
=env.t('habiticaDaySingularText')
small(ng-if='::profile.achievements.habiticaDays > 1')
=env.t('habiticaDayPluralText', {number: "{{::profile.achievements.habiticaDays}}"})
hr
div(ng-if='::profile.achievements.habitBirthdays')
.achievement.achievement-habitBirthday
h5=env.t('habitBirthday')
small(ng-if='::profile.achievements.habitBirthdays == 1')
=env.t('habitBirthdayText')
small(ng-if='::profile.achievements.habitBirthdays > 1')
=env.t('habitBirthdayPluralText', {number: "{{::profile.achievements.habitBirthdays}}"})
hr
div(ng-if='::profile.achievements.quests.dilatory')
.achievement.achievement-dilatory
h5=env.t('achievementDilatory')
small
=env.t('achievementDilatoryText')
hr
div(ng-if='::profile.achievements.quests.stressbeast')
.achievement.achievement-stoikalm
h5=env.t('achievementStressbeast')
small
=env.t('achievementStressbeastText')
hr
div(ng-if='::profile.achievements.quests.burnout')
.achievement.achievement-burnout
h5=env.t('achievementBurnout')
small
=env.t('achievementBurnoutText')
hr
div(ng-if='::profile.achievements.quests.bewilder')
.achievement.achievement-bewilder
h5=env.t('achievementBewilder')
small
=env.t('achievementBewilderText')
hr
div(ng-if='::profile.achievements.costumeContests')
.achievement.achievement-costumeContest
h5=env.t('costumeContest')
small(ng-if='::profile.achievements.costumeContests === 1')
!=env.t('costumeContestText')
small(ng-if='::profile.achievements.costumeContests > 1')
!=env.t('costumeContestTextPlural', {number: "{{::profile.achievements.costumeContests}}"})
hr
each card in ['greeting', 'thankyou', 'nye', 'valentine', 'birthday']
div(ng-if='::profile.achievements.#{card}')
div(class='achievement achievement-#{card}')
h5=env.t(card + 'CardAchievementTitle')
small=env.t(card + 'CardAchievementText', {cards: "{{::profile.achievements." + card + "}}"})
hr
include ./achievs/challenges
include ./achievs/quests

View File

@@ -0,0 +1,9 @@
div
// This is a very strange icon to use. revisit
.achievement.achievement-karaoke(ng-if='::profile.achievements.challenges.length')
div(ng-class='::{muted: !profile.achievements.challenges.length}')
h5=env.t('challengeWinner')
table.table.table-striped
tr(ng-repeat='chal in profile.achievements.challenges track by $index')
td: markdown(text='::chal')
hr

View File

@@ -0,0 +1,9 @@
div
.achievement.achievement-alien(ng-if='::profile.achievements.quests')
div(ng-class='::{muted: !profile.achievements.quests}')
h5=env.t('completedQuests')
table.table.table-striped
tr(ng-repeat='(k,v) in profile.achievements.quests')
td {{::Content.quests[k].text()}}
td x{{::v}}
hr