mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 07:07:35 +01:00
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:
committed by
Keith Holliday
parent
97e1d75dce
commit
0817cf96e1
44
test/api/v3/integration/members/GET-achievements.test.js
Normal file
44
test/api/v3/integration/members/GET-achievements.test.js
Normal 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}),
|
||||
});
|
||||
});
|
||||
});
|
||||
348
test/common/libs/achievements.test.js
Normal file
348
test/common/libs/achievements.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 |
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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>)",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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 %>",
|
||||
|
||||
@@ -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!",
|
||||
|
||||
188
website/common/script/content/achievements.js
Normal file
188
website/common/script/content/achievements.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
306
website/common/script/libs/achievements.js
Normal file
306
website/common/script/libs/achievements.js
Normal 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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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')
|
||||
|
|
||||
a(href=env.t('conLearnURL'), target='_blank')
|
||||
=env.t('readMore')
|
||||
| .
|
||||
hr
|
||||
|
||||
div(ng-if='::profile.backer.tier')
|
||||
.achievement.achievement-heart
|
||||
h5=env.t('kickstartName', {tier: "{{::profile.backer.tier}}"})
|
||||
small=env.t('kickstartText')
|
||||
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 }}
|
||||
=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 }}
|
||||
=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')
|
||||
| {{::profile.achievements.rebirthLevel}}.
|
||||
small(ng-if='::profile.achievements.rebirthLevel >= 100')
|
||||
=env.t('rebirthOrb100')
|
||||
.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')
|
||||
|
||||
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
|
||||
|
||||
9
website/views/shared/profiles/achievs/challenges.jade
Normal file
9
website/views/shared/profiles/achievs/challenges.jade
Normal 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
|
||||
9
website/views/shared/profiles/achievs/quests.jade
Normal file
9
website/views/shared/profiles/achievs/quests.jade
Normal 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
|
||||
Reference in New Issue
Block a user