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;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.achievement .counter {
|
||||||
|
bottom: 0;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.multi-achievement {
|
.multi-achievement {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding-left: 0.5em;
|
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) {
|
$scope.$watch( function() { return Members.selectedMember; }, function (member) {
|
||||||
if(member) {
|
if(member) {
|
||||||
$scope.profile = 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;
|
var nextRewardAt = currentLoginDay.nextRewardAt;
|
||||||
return ($scope.profile.loginIncentives - previousRewardDay)/(nextRewardAt - previousRewardDay) * 100;
|
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) {
|
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){
|
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:",
|
"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!",
|
"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.",
|
"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",
|
"ultimGearName": "Ultimate Gear - <%= ultClass %>",
|
||||||
"ultimGearText": "Has upgraded to the maximum weapon and armor set for the following classes:",
|
"ultimGearText": "Has upgraded to the maximum weapon and armor set for the <%= ultClass %> class.",
|
||||||
"level": "Level",
|
"level": "Level",
|
||||||
"levelUp": "Level Up!",
|
"levelUp": "Level Up!",
|
||||||
"gainedLevel": "You gained a level!",
|
"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",
|
"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!",
|
"contribLink": "what prizes you've earned for your contribution!",
|
||||||
"contribName": "Contributor",
|
"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",
|
"readMore": "Read More",
|
||||||
"kickstartName": "Kickstarter Backer - $<%= tier %> Tier",
|
"kickstartName": "Kickstarter Backer - $<%= key %> Tier",
|
||||||
"kickstartText": "Backed the Kickstarter Project",
|
"kickstartText": "Backed the Kickstarter Project",
|
||||||
"helped": "Helped Habit Grow",
|
"helped": "Helped Habitica Grow",
|
||||||
"helpedText1": "Helped Habitica grow by filling out",
|
"helpedText1": "Helped Habitica grow by filling out",
|
||||||
"helpedText2": "this survey.",
|
"helpedText2": "this survey.",
|
||||||
"hall": "Hall of Heroes",
|
"hall": "Hall of Heroes",
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
"conLearnURL": "http://habitica.wikia.com/wiki/Contributing_to_Habitica",
|
"conLearnURL": "http://habitica.wikia.com/wiki/Contributing_to_Habitica",
|
||||||
"conRewardsURL": "http://habitica.wikia.com/wiki/Contributor_Rewards",
|
"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!",
|
"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",
|
"currentSurvey": "Current Survey",
|
||||||
"surveyWhen": "The badge will be awarded to all participants when surveys have been processed, in late March.",
|
"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>)",
|
"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",
|
"unorderedListMarkdown": "+ First item\n+ Second item\n+ Third item",
|
||||||
"code": "`code`",
|
"code": "`code`",
|
||||||
"achievements": "Achievements",
|
"achievements": "Achievements",
|
||||||
|
"basicAchievs": "Basic Achievements",
|
||||||
|
"seasonalAchievs": "Seasonal Achievements",
|
||||||
|
"specialAchievs": "Special Achievements",
|
||||||
"modalAchievement": "Achievement!",
|
"modalAchievement": "Achievement!",
|
||||||
"special": "Special",
|
"special": "Special",
|
||||||
"site": "Site",
|
"site": "Site",
|
||||||
@@ -89,15 +92,15 @@
|
|||||||
"originalUserText": "One of the <em>very</em> original early adopters. Talk about alpha tester!",
|
"originalUserText": "One of the <em>very</em> original early adopters. Talk about alpha tester!",
|
||||||
"habitBirthday": "Habitica Birthday Bash",
|
"habitBirthday": "Habitica Birthday Bash",
|
||||||
"habitBirthdayText": "Celebrated the 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",
|
"habiticaDay": "Habitica Naming Day",
|
||||||
"habiticaDaySingularText": "Celebrated Habitica's Naming Day! Thanks for being a fantastic user.",
|
"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",
|
"achievementDilatory": "Savior of Dilatory",
|
||||||
"achievementDilatoryText": "Helped defeat the Dread Drag'on of Dilatory during the 2014 Summer Splash Event!",
|
"achievementDilatoryText": "Helped defeat the Dread Drag'on of Dilatory during the 2014 Summer Splash Event!",
|
||||||
"costumeContest": "Costume Contestant",
|
"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>!",
|
"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",
|
"memberSince": "- Member since",
|
||||||
"lastLoggedIn": "- Last logged in",
|
"lastLoggedIn": "- Last logged in",
|
||||||
"notPorted": "This feature is not yet ported from the original site.",
|
"notPorted": "This feature is not yet ported from the original site.",
|
||||||
@@ -159,7 +162,7 @@
|
|||||||
"greeting2": "`waves frantically`",
|
"greeting2": "`waves frantically`",
|
||||||
"greeting3": "Hey you!",
|
"greeting3": "Hey you!",
|
||||||
"greetingCardAchievementTitle": "Cheery Chum",
|
"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",
|
"thankyouCard": "Thank-You Card",
|
||||||
"thankyouCardExplanation": "You both receive the Greatly Grateful achievement!",
|
"thankyouCardExplanation": "You both receive the Greatly Grateful achievement!",
|
||||||
"thankyouCardNotes": "Send a Thank-You card to a party member.",
|
"thankyouCardNotes": "Send a Thank-You card to a party member.",
|
||||||
@@ -168,13 +171,13 @@
|
|||||||
"thankyou2": "Sending you a thousand thanks.",
|
"thankyou2": "Sending you a thousand thanks.",
|
||||||
"thankyou3": "I'm very grateful - thank you!",
|
"thankyou3": "I'm very grateful - thank you!",
|
||||||
"thankyouCardAchievementTitle": "Greatly Grateful",
|
"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",
|
"birthdayCard": "Birthday Card",
|
||||||
"birthdayCardExplanation": "You both receive the Birthday Bonanza achievement!",
|
"birthdayCardExplanation": "You both receive the Birthday Bonanza achievement!",
|
||||||
"birthdayCardNotes": "Send a birthday card to a party member.",
|
"birthdayCardNotes": "Send a birthday card to a party member.",
|
||||||
"birthday0": "Happy birthday to you!",
|
"birthday0": "Happy birthday to you!",
|
||||||
"birthdayCardAchievementTitle": "Birthday Bonanza",
|
"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!",
|
"streakAchievement": "You earned a streak achievement!",
|
||||||
"firstStreakAchievement": "21-Day Streak",
|
"firstStreakAchievement": "21-Day Streak",
|
||||||
"streakAchievementCount": "<%= streaks %> 21-Day Streaks",
|
"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.",
|
"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",
|
"partyUpName": "Party Up",
|
||||||
"partyOnName": "Party On",
|
"partyOnName": "Party On",
|
||||||
"partyUpAchievement": "Joined a Party with another person! Have fun battling monsters and supporting each other.",
|
"partyUpText": "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!",
|
"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.",
|
"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",
|
"groupIdRequired": "\"groupId\" must be a valid UUID",
|
||||||
"groupNotFound": "Group not found or you don't have access.",
|
"groupNotFound": "Group not found or you don't have access.",
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
"seasonalEdition": "Seasonal Edition",
|
"seasonalEdition": "Seasonal Edition",
|
||||||
"winterColors": "Winter Colors",
|
"winterColors": "Winter Colors",
|
||||||
"annoyingFriends": "Annoying Friends",
|
"annoyingFriends": "Annoying Friends",
|
||||||
"annoyingFriendsText": "Got snowballed <%= snowballs %> times by party members.",
|
"annoyingFriendsText": "Got snowballed <%= count %> times by party members.",
|
||||||
"alarmingFriends": "Alarming Friends",
|
"alarmingFriends": "Alarming Friends",
|
||||||
"alarmingFriendsText": "Got spooked <%= spookySparkles %> times by party members.",
|
"alarmingFriendsText": "Got spooked <%= count %> times by party members.",
|
||||||
"agriculturalFriends": "Agricultural Friends",
|
"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",
|
"aquaticFriends": "Aquatic Friends",
|
||||||
"aquaticFriendsText": "Got splashed <%= seafoam %> times by party members.",
|
"aquaticFriendsText": "Got splashed <%= count %> times by party members.",
|
||||||
"valentineCard": "Valentine's Day Card",
|
"valentineCard": "Valentine's Day Card",
|
||||||
"valentineCardExplanation": "For enduring such a saccharine poem, you both receive the \"Adoring Friends\" badge!",
|
"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.",
|
"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.\"",
|
"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!\"",
|
"valentine3": "\"Roses are red\n\nIce Drakes are blue\n\nNo treasure is better\n\nThan time spent with you!\"",
|
||||||
"valentineCardAchievementTitle": "Adoring Friends",
|
"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",
|
"polarBear": "Polar Bear",
|
||||||
"turkey": "Turkey",
|
"turkey": "Turkey",
|
||||||
"gildedTurkey": "Gilded Turkey",
|
"gildedTurkey": "Gilded Turkey",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"nyeCardNotes": "Send a New Year's card to a party member.",
|
"nyeCardNotes": "Send a New Year's card to a party member.",
|
||||||
"seasonalItems": "Seasonal Items",
|
"seasonalItems": "Seasonal Items",
|
||||||
"nyeCardAchievementTitle": "Auld Acquaintance",
|
"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.",
|
"nye0": "Happy New Year! May you slay many a bad Habit.",
|
||||||
"nye1": "Happy New Year! May you reap many Rewards.",
|
"nye1": "Happy New Year! May you reap many Rewards.",
|
||||||
"nye2": "Happy New Year! May you earn many a Perfect Day.",
|
"nye2": "Happy New Year! May you earn many a Perfect Day.",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"npc": "NPC",
|
"npc": "NPC",
|
||||||
|
"npcAchievementName": "<%= key %> NPC",
|
||||||
"npcAchievementText": "Backed the Kickstarter project at the maximum level!",
|
"npcAchievementText": "Backed the Kickstarter project at the maximum level!",
|
||||||
"mattBoch": "Matt Boch",
|
"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!",
|
"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!",
|
"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",
|
"rebirthBegan": "Began a New Adventure",
|
||||||
"rebirthText": "Began <%= rebirths %> New Adventures",
|
"rebirthText": "Began <%= rebirths %> New Adventures",
|
||||||
"rebirthOrb": "Used an Orb of Rebirth to start over after attaining Level",
|
"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",
|
"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.",
|
"rebirthPop": "Begin a new character at Level 1 while retaining achievements, collectibles, equipment, and tasks with history.",
|
||||||
"rebirthName": "Orb of Rebirth",
|
"rebirthName": "Orb of Rebirth",
|
||||||
"reborn": "Reborn, max level <%= reLevel %>",
|
"reborn": "Reborn, max level <%= reLevel %>",
|
||||||
|
|||||||
@@ -78,12 +78,13 @@
|
|||||||
"startDate": "Start Date",
|
"startDate": "Start Date",
|
||||||
"startDateHelpTitle": "When should this task start?",
|
"startDateHelpTitle": "When should this task start?",
|
||||||
"startDateHelp": "Set the date for which this task takes effect. Will not be due on earlier days.",
|
"startDateHelp": "Set the date for which this task takes effect. Will not be due on earlier days.",
|
||||||
"streakName": "Streak Achievements",
|
"streaks": "Streak Achievements",
|
||||||
"streakText": "Has performed <%= streaks %> 21-day streaks on Dailies",
|
"streakName": "<%= count %> Streak Achievements",
|
||||||
|
"streakText": "Has performed <%= count %> 21-day streaks on Dailies",
|
||||||
"streakSingular": "Streaker",
|
"streakSingular": "Streaker",
|
||||||
"streakSingularText": "Has performed a 21-day streak on a Daily",
|
"streakSingularText": "Has performed a 21-day streak on a Daily",
|
||||||
"perfectName": "Perfect Days",
|
"perfectName": "<%= count %> 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.",
|
"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",
|
"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.",
|
"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!",
|
"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;
|
let api = module.exports;
|
||||||
|
|
||||||
|
import achievements from './achievements';
|
||||||
|
|
||||||
import mysterySets from './mystery-sets';
|
import mysterySets from './mystery-sets';
|
||||||
|
|
||||||
import eggs from './eggs';
|
import eggs from './eggs';
|
||||||
@@ -25,6 +27,8 @@ import spells from './spells';
|
|||||||
import faq from './faq';
|
import faq from './faq';
|
||||||
import loginIncentives from './loginIncentives';
|
import loginIncentives from './loginIncentives';
|
||||||
|
|
||||||
|
api.achievements = achievements;
|
||||||
|
|
||||||
api.mystery = mysterySets;
|
api.mystery = mysterySets;
|
||||||
|
|
||||||
api.itemList = ITEM_LIST;
|
api.itemList = ITEM_LIST;
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ api.statsComputed = statsComputed;
|
|||||||
import shops from './libs/shops';
|
import shops from './libs/shops';
|
||||||
api.shops = shops;
|
api.shops = shops;
|
||||||
|
|
||||||
|
import achievements from './libs/achievements';
|
||||||
|
api.achievements = achievements;
|
||||||
|
|
||||||
import randomVal from './libs/randomVal';
|
import randomVal from './libs/randomVal';
|
||||||
api.randomVal = 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';
|
} from '../../libs/email';
|
||||||
import Bluebird from 'bluebird';
|
import Bluebird from 'bluebird';
|
||||||
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
|
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
|
||||||
|
import { achievements } from '../../../../website/common/';
|
||||||
|
|
||||||
let api = {};
|
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
|
// Return a request handler for getMembersForGroup / getInvitesForGroup / getMembersForChallenge
|
||||||
// type is `invites` or `members`
|
// type is `invites` or `members`
|
||||||
function _getMembersForItem (type) {
|
function _getMembersForItem (type) {
|
||||||
|
|||||||
@@ -1,268 +1,31 @@
|
|||||||
if mobile
|
mixin simpleAchiev(achiev)
|
||||||
.item.item-divider=env.t('achievements')
|
- 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(ng-class='achiev.icon + "2x"', ng-if='achiev.earned')
|
||||||
.achievement.achievement-tree(ng-if='::profile.achievements.habitSurveys')
|
.counter.badge.badge-info.stack-count(ng-if='(achiev.optionalCount)')
|
||||||
div(ng-class='::{muted: !profile.achievements.habitSurveys}')
|
{{::achiev.optionalCount}}
|
||||||
h5=env.t('helped')
|
.achievement(class='achievement-unearned2x', ng-if='!(achiev.earned)')
|
||||||
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
|
|
||||||
|
|
||||||
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')
|
.container-fluid
|
||||||
.achievement.achievement-boot(ng-if='::profile.contributor.level')
|
.row
|
||||||
div(ng-class='::{muted: !profile.contributor.level}')
|
.col-md-12(ng-repeat='(key,cat) in achievements', ng-init='heading=env.t(key+"Achievs")')
|
||||||
h5
|
h4 {{heading}}
|
||||||
span.label.label-default(ng-if='::profile.contributor.level', class='label-contributor-{{::profile.contributor.level}}') {{::contribText(profile.contributor, profile.backer)}}
|
menu.pets.inventory-list(type='list')
|
||||||
span.label.label-default(ng-if='::!profile.contributor.level')=env.t('contribName')
|
li.customize-menu
|
||||||
small
|
menu
|
||||||
=env.t('contribText')
|
div(ng-repeat='achiev in cat.achievements | toArray | orderBy: "index"')
|
||||||
|
|
+simpleAchiev('achiev')
|
||||||
a(href=env.t('conLearnURL'), target='_blank')
|
|
||||||
=env.t('readMore')
|
|
||||||
| .
|
|
||||||
hr
|
|
||||||
|
|
||||||
div(ng-if='::profile.backer.tier')
|
hr
|
||||||
.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')
|
include ./achievs/challenges
|
||||||
.achievement.achievement-thermometer(ng-if='::profile.achievements.streak')
|
include ./achievs/quests
|
||||||
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')
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
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