mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-15 13:47:33 +01:00
Onboarding guide and initial achievements refactoring (#11536)
* add achievements to user * add placeholder strings * add to achievements to common script * add onboarding achievements category * add notifications * more notifications * award achievements * wip notification panel * add achievements icons and copy * do not count onboarding tasks for the created task achievement * add notes * sprites, fixes and completion status and reward * add onboarding panel * add toggle * fix toggle size * fix tests * fix typo * add notification * start adding modal * fix remove button positionin, timeout, progress bar * modal + fixes * disable broken social links from level up modal * change toggle icon color on hover * add border bottom to onboarding guide panel * add collapse animation * expanded onboarding on first open * onboarding: flip toggle colors * onboarding: show progress bar all the time * onboarding: fix panel closing on click * onboarding modal: add close icon and fix padding * wip: add migration for existing users * fix titles in guide * fix achievements copy * do not award completed task achievement when direction is down * start implementing new achievements * start migrating client * remove social links from achievements modals * prevent skipping tutorial + fix achievement notification * sync fixes * start redesign achievement modal * misc fixes to achievements, polish generic achievement modal and hatched pet modal * add special badge for onboarding * fix badge condition * modals fixes * hatched pet modal: add close icon * fix badge typo * fix justin button * new scrolling behavior for dropdowns * fix strings capitalization * add common tests * add api unit tests * add date check * achievements modal polishing * typos * add toggle for achievements categories * typo * fix test * fix edit avatar modal cannot be closed * finish migration and correct launch date * fix migration * migration fixes * fix tests
This commit is contained in:
@@ -180,6 +180,35 @@ const basicAchievs = {
|
||||
};
|
||||
Object.assign(achievementsData, basicAchievs);
|
||||
|
||||
const onboardingAchievs = {
|
||||
createdTask: {
|
||||
icon: 'achievement-createdTask',
|
||||
titleKey: 'achievementCreatedTask',
|
||||
textKey: 'achievementCreatedTaskText',
|
||||
},
|
||||
completedTask: {
|
||||
icon: 'achievement-completedTask',
|
||||
titleKey: 'achievementCompletedTask',
|
||||
textKey: 'achievementCompletedTaskText',
|
||||
},
|
||||
hatchedPet: {
|
||||
icon: 'achievement-hatchedPet',
|
||||
titleKey: 'achievementHatchedPet',
|
||||
textKey: 'achievementHatchedPetText',
|
||||
},
|
||||
fedPet: {
|
||||
icon: 'achievement-fedPet',
|
||||
titleKey: 'achievementFedPet',
|
||||
textKey: 'achievementFedPetText',
|
||||
},
|
||||
purchasedEquipment: {
|
||||
icon: 'achievement-purchasedEquipment',
|
||||
titleKey: 'achievementPurchasedEquipment',
|
||||
textKey: 'achievementPurchasedEquipmentText',
|
||||
},
|
||||
};
|
||||
Object.assign(achievementsData, onboardingAchievs);
|
||||
|
||||
const specialAchievs = {
|
||||
contributor: {
|
||||
icon: 'achievement-boot',
|
||||
|
||||
@@ -1,82 +1,29 @@
|
||||
// When using a common module from the website or the server NEVER import the module directly
|
||||
// but access it through `api` (the main common) module,
|
||||
// otherwise you would require the non transpiled version of the file in production.
|
||||
import content from './content/index';
|
||||
|
||||
import * as errors from './libs/errors';
|
||||
import i18n from './i18n';
|
||||
|
||||
import commonErrors from './errors/commonErrorMessages';
|
||||
import apiErrors from './errors/apiErrorMessages';
|
||||
|
||||
// TODO under api.libs.cron?
|
||||
import { shouldDo, daysSince, DAY_MAPPING } from './cron';
|
||||
|
||||
import {
|
||||
MAX_HEALTH,
|
||||
MAX_LEVEL,
|
||||
MAX_STAT_POINTS,
|
||||
MAX_INCENTIVES,
|
||||
TAVERN_ID,
|
||||
LARGE_GROUP_COUNT_MESSAGE_CUTOFF,
|
||||
MAX_SUMMARY_SIZE_FOR_GUILDS,
|
||||
MAX_SUMMARY_SIZE_FOR_CHALLENGES,
|
||||
MIN_SHORTNAME_SIZE_FOR_CHALLENGES,
|
||||
SUPPORTED_SOCIAL_NETWORKS,
|
||||
GUILDS_PER_PAGE,
|
||||
PARTY_LIMIT_MEMBERS,
|
||||
CHAT_FLAG_LIMIT_FOR_HIDING,
|
||||
CHAT_FLAG_FROM_MOD,
|
||||
CHAT_FLAG_FROM_SHADOW_MUTE,
|
||||
CHAT_FLAG_LIMIT_FOR_HIDING,
|
||||
GUILDS_PER_PAGE,
|
||||
LARGE_GROUP_COUNT_MESSAGE_CUTOFF,
|
||||
MAX_HEALTH,
|
||||
MAX_INCENTIVES,
|
||||
MAX_LEVEL,
|
||||
MAX_STAT_POINTS,
|
||||
MAX_SUMMARY_SIZE_FOR_CHALLENGES,
|
||||
MAX_SUMMARY_SIZE_FOR_GUILDS,
|
||||
MIN_SHORTNAME_SIZE_FOR_CHALLENGES,
|
||||
PARTY_LIMIT_MEMBERS,
|
||||
SUPPORTED_SOCIAL_NETWORKS,
|
||||
TAVERN_ID,
|
||||
} from './constants';
|
||||
|
||||
// TODO under api.libs.statHelpers?
|
||||
import * as statHelpers from './statHelpers';
|
||||
|
||||
import splitWhitespace from './libs/splitWhitespace';
|
||||
|
||||
import refPush from './libs/refPush';
|
||||
|
||||
import planGemLimits from './libs/planGemLimits';
|
||||
|
||||
import preenTodos from './libs/preenTodos';
|
||||
|
||||
import updateStore from './libs/updateStore';
|
||||
|
||||
import inAppRewards from './libs/inAppRewards';
|
||||
|
||||
import setDebuffPotionItems from './libs/setDebuffPotionItems';
|
||||
|
||||
import getDebuffPotionItems from './libs/getDebuffPotionItems';
|
||||
|
||||
import uuid from './libs/uuid';
|
||||
|
||||
import taskDefaults from './libs/taskDefaults';
|
||||
|
||||
import percent from './libs/percent';
|
||||
|
||||
import gold from './libs/gold';
|
||||
|
||||
import silver from './libs/silver';
|
||||
|
||||
import noTags from './libs/noTags';
|
||||
|
||||
import appliedTags from './libs/appliedTags';
|
||||
|
||||
import pickDeep from './libs/pickDeep';
|
||||
|
||||
import content from './content/index';
|
||||
import * as count from './count';
|
||||
|
||||
import statsComputed from './libs/statsComputed';
|
||||
|
||||
import shops from './libs/shops';
|
||||
|
||||
import achievements from './libs/achievements';
|
||||
|
||||
import randomVal from './libs/randomVal';
|
||||
|
||||
import hasClass from './libs/hasClass';
|
||||
|
||||
// TODO under api.libs.cron?
|
||||
import { daysSince, DAY_MAPPING, shouldDo } from './cron';
|
||||
import apiErrors from './errors/apiErrorMessages';
|
||||
import commonErrors from './errors/commonErrorMessages';
|
||||
import autoAllocate from './fns/autoAllocate';
|
||||
import crit from './fns/crit';
|
||||
import handleTwoHanded from './fns/handleTwoHanded';
|
||||
@@ -85,33 +32,59 @@ import randomDrop from './fns/randomDrop';
|
||||
import resetGear from './fns/resetGear';
|
||||
import ultimateGear from './fns/ultimateGear';
|
||||
import updateStats from './fns/updateStats';
|
||||
|
||||
import scoreTask from './ops/scoreTask';
|
||||
import sleep from './ops/sleep';
|
||||
import allocateNow from './ops/stats/allocateNow';
|
||||
import allocate from './ops/stats/allocate';
|
||||
import allocateBulk from './ops/stats/allocateBulk';
|
||||
import i18n from './i18n';
|
||||
import achievements from './libs/achievements';
|
||||
import appliedTags from './libs/appliedTags';
|
||||
import * as errors from './libs/errors';
|
||||
import getDebuffPotionItems from './libs/getDebuffPotionItems';
|
||||
import gold from './libs/gold';
|
||||
import hasClass from './libs/hasClass';
|
||||
import inAppRewards from './libs/inAppRewards';
|
||||
import noTags from './libs/noTags';
|
||||
import * as onboarding from './libs/onboarding';
|
||||
import percent from './libs/percent';
|
||||
import pickDeep from './libs/pickDeep';
|
||||
import planGemLimits from './libs/planGemLimits';
|
||||
import preenTodos from './libs/preenTodos';
|
||||
import randomVal from './libs/randomVal';
|
||||
import refPush from './libs/refPush';
|
||||
import setDebuffPotionItems from './libs/setDebuffPotionItems';
|
||||
import shops from './libs/shops';
|
||||
import silver from './libs/silver';
|
||||
import splitWhitespace from './libs/splitWhitespace';
|
||||
import statsComputed from './libs/statsComputed';
|
||||
import taskDefaults from './libs/taskDefaults';
|
||||
import updateStore from './libs/updateStore';
|
||||
import uuid from './libs/uuid';
|
||||
import blockUser from './ops/blockUser';
|
||||
import buy from './ops/buy/buy';
|
||||
import hatch from './ops/hatch';
|
||||
import feed from './ops/feed';
|
||||
import equip from './ops/equip';
|
||||
import changeClass from './ops/changeClass';
|
||||
import disableClasses from './ops/disableClasses';
|
||||
import readCard from './ops/readCard';
|
||||
import equip from './ops/equip';
|
||||
import feed from './ops/feed';
|
||||
import hatch from './ops/hatch';
|
||||
import markPmsRead from './ops/markPMSRead';
|
||||
import openMysteryItem from './ops/openMysteryItem';
|
||||
import releasePets from './ops/releasePets';
|
||||
import * as pinnedGearUtils from './ops/pinnedGearUtils';
|
||||
import readCard from './ops/readCard';
|
||||
import rebirth from './ops/rebirth';
|
||||
import releaseBoth from './ops/releaseBoth';
|
||||
import releaseMounts from './ops/releaseMounts';
|
||||
import updateTask from './ops/updateTask';
|
||||
import sell from './ops/sell';
|
||||
import unlock from './ops/unlock';
|
||||
import revive from './ops/revive';
|
||||
import rebirth from './ops/rebirth';
|
||||
import blockUser from './ops/blockUser';
|
||||
import releasePets from './ops/releasePets';
|
||||
import reroll from './ops/reroll';
|
||||
import reset from './ops/reset';
|
||||
import markPmsRead from './ops/markPMSRead';
|
||||
import * as pinnedGearUtils from './ops/pinnedGearUtils';
|
||||
import revive from './ops/revive';
|
||||
import scoreTask from './ops/scoreTask';
|
||||
import sell from './ops/sell';
|
||||
import sleep from './ops/sleep';
|
||||
import allocate from './ops/stats/allocate';
|
||||
import allocateBulk from './ops/stats/allocateBulk';
|
||||
import allocateNow from './ops/stats/allocateNow';
|
||||
import unlock from './ops/unlock';
|
||||
import updateTask from './ops/updateTask';
|
||||
// TODO under api.libs.statHelpers?
|
||||
import * as statHelpers from './statHelpers';
|
||||
|
||||
|
||||
const api = {};
|
||||
api.content = content;
|
||||
@@ -162,10 +135,10 @@ api.shops = shops;
|
||||
api.achievements = achievements;
|
||||
api.randomVal = randomVal;
|
||||
api.hasClass = hasClass;
|
||||
api.onboarding = onboarding;
|
||||
api.setDebuffPotionItems = setDebuffPotionItems;
|
||||
api.getDebuffPotionItems = getDebuffPotionItems;
|
||||
|
||||
|
||||
api.fns = {
|
||||
autoAllocate,
|
||||
crit,
|
||||
|
||||
@@ -241,6 +241,18 @@ function _getBasicAchievements (user, language) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function _getOnboardingAchievements (user, language) {
|
||||
const result = {};
|
||||
|
||||
_addSimple(result, user, { path: 'createdTask', language });
|
||||
_addSimple(result, user, { path: 'completedTask', language });
|
||||
_addSimple(result, user, { path: 'hatchedPet', language });
|
||||
_addSimple(result, user, { path: 'fedPet', language });
|
||||
_addSimple(result, user, { path: 'purchasedEquipment', language });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function _getSeasonalAchievements (user, language) {
|
||||
const result = {};
|
||||
|
||||
@@ -321,6 +333,10 @@ achievs.getAchievementsForProfile = function getAchievementsForProfile (user, la
|
||||
label: 'Basic',
|
||||
achievements: _getBasicAchievements(user, language),
|
||||
},
|
||||
onboarding: {
|
||||
label: 'Onboarding',
|
||||
achievements: _getOnboardingAchievements(user, language),
|
||||
},
|
||||
seasonal: {
|
||||
label: 'Seasonal',
|
||||
achievements: _getSeasonalAchievements(user, language),
|
||||
|
||||
31
website/common/script/libs/onboarding.js
Normal file
31
website/common/script/libs/onboarding.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import moment from 'moment';
|
||||
|
||||
const BEGIN_DATE = moment('2019-12-18');
|
||||
|
||||
// Only users that signed up after the BEGIN DATE should see the onboarding
|
||||
export function hasActiveOnboarding (user) {
|
||||
return BEGIN_DATE.isBefore(user.auth.timestamps.created);
|
||||
}
|
||||
|
||||
export function hasCompletedOnboarding (user) {
|
||||
return (
|
||||
user.achievements.createdTask === true
|
||||
&& user.achievements.completedTask === true
|
||||
&& user.achievements.hatchedPet === true
|
||||
&& user.achievements.fedPet === true
|
||||
&& user.achievements.purchasedEquipment === true
|
||||
);
|
||||
}
|
||||
|
||||
export function onOnboardingComplete (user) {
|
||||
// Award gold
|
||||
user.stats.gp += 100;
|
||||
}
|
||||
|
||||
// Add notification and awards (server)
|
||||
export function checkOnboardingStatus (user) {
|
||||
if (hasActiveOnboarding(user) && hasCompletedOnboarding(user) && user.addNotification) {
|
||||
user.addNotification('ONBOARDING_COMPLETE');
|
||||
onOnboardingComplete(user);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import get from 'lodash/get';
|
||||
import pick from 'lodash/pick';
|
||||
import content from '../../content/index';
|
||||
import splitWhitespace from '../../libs/splitWhitespace';
|
||||
import { checkOnboardingStatus } from '../../libs/onboarding';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
@@ -67,6 +68,11 @@ export class BuyMarketGearOperation extends AbstractGoldItemOperation { // eslin
|
||||
message = handleTwoHanded(user, item, undefined, req);
|
||||
}
|
||||
|
||||
if (!user.achievements.purchasedEquipment && user.addAchievement) {
|
||||
user.addAchievement('purchasedEquipment');
|
||||
checkOnboardingStatus(user);
|
||||
}
|
||||
|
||||
removePinnedGearAddPossibleNewOnes(user, `gear.flat.${item.key}`, item.key);
|
||||
|
||||
if (item.last) ultimateGear(user);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
NotFound,
|
||||
} from '../libs/errors';
|
||||
import errorMessage from '../libs/errorMessage';
|
||||
import { checkOnboardingStatus } from '../libs/onboarding';
|
||||
|
||||
function evolve (user, pet, req) {
|
||||
user.items.pets[pet.key] = -1;
|
||||
@@ -88,6 +89,11 @@ export default function feed (user, req = {}) {
|
||||
if (userPets[pet.key] >= 50 && !user.items.mounts[pet.key]) {
|
||||
message = evolve(user, pet, req);
|
||||
}
|
||||
|
||||
if (!user.achievements.fedPet && user.addAchievement) {
|
||||
user.addAchievement('fedPet');
|
||||
checkOnboardingStatus(user);
|
||||
}
|
||||
}
|
||||
|
||||
user.items.food[food.key] -= 1;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
NotFound,
|
||||
} from '../libs/errors';
|
||||
import errorMessage from '../libs/errorMessage';
|
||||
import { checkOnboardingStatus } from '../libs/onboarding';
|
||||
|
||||
export default function hatch (user, req = {}) {
|
||||
const egg = get(req, 'params.egg');
|
||||
@@ -49,6 +50,11 @@ export default function hatch (user, req = {}) {
|
||||
user.markModified('items.hatchingPotions');
|
||||
}
|
||||
|
||||
if (!user.achievements.hatchedPet && user.addAchievement) {
|
||||
user.addAchievement('hatchedPet');
|
||||
checkOnboardingStatus(user);
|
||||
}
|
||||
|
||||
forEach(content.animalColorAchievements, achievement => {
|
||||
if (!user.achievements[achievement.petAchievement]) {
|
||||
const petIndex = findIndex(
|
||||
|
||||
@@ -9,6 +9,7 @@ import i18n from '../i18n';
|
||||
import updateStats from '../fns/updateStats';
|
||||
import crit from '../fns/crit';
|
||||
import statsComputed from '../libs/statsComputed';
|
||||
import { checkOnboardingStatus } from '../libs/onboarding';
|
||||
|
||||
const MAX_TASK_VALUE = 21.27;
|
||||
const MIN_TASK_VALUE = -47.27;
|
||||
@@ -343,5 +344,11 @@ export default function scoreTask (options = {}, req = {}) {
|
||||
|
||||
req.yesterDailyScored = task.yesterDailyScored;
|
||||
updateStats(user, stats, req);
|
||||
|
||||
if (!user.achievements.completedTask && cron === false && direction === 'up' && user.addAchievement) {
|
||||
user.addAchievement('completedTask');
|
||||
checkOnboardingStatus(user);
|
||||
}
|
||||
|
||||
return [delta];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user