Merge branch 'release' into develop

This commit is contained in:
SabreCat 2023-10-17 17:53:22 -05:00
commit 57fe1d6b22
20 changed files with 213 additions and 19 deletions

View file

@ -0,0 +1,118 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20231017_pet_group_achievements';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {
migration: MIGRATION_NAME,
};
if (user && user.items && user.items.pets) {
const pets = user.items.pets;
if (pets['Armadillo-Base']
&& pets['Armadillo-CottonCandyBlue']
&& pets['Armadillo-CottonCandyPink']
&& pets['Armadillo-Desert']
&& pets['Armadillo-Golden']
&& pets['Armadillo-Red']
&& pets['Armadillo-Shade']
&& pets['Armadillo-Skeleton']
&& pets['Armadillo-White']
&& pets['Armadillo-Zombie']
&& pets['Cactus-Base']
&& pets['Cactus-CottonCandyBlue']
&& pets['Cactus-CottonCandyPink']
&& pets['Cactus-Desert']
&& pets['Cactus-Golden']
&& pets['Cactus-Red']
&& pets['Cactus-Shade']
&& pets['Cactus-Skeleton']
&& pets['Cactus-White']
&& pets['Cactus-Zombie']
&& pets['Fox-Base']
&& pets['Fox-CottonCandyBlue']
&& pets['Fox-CottonCandyPink']
&& pets['Fox-Desert']
&& pets['Fox-Golden']
&& pets['Fox-Red']
&& pets['Fox-Shade']
&& pets['Fox-Skeleton']
&& pets['Fox-White']
&& pets['Fox-Zombie']
&& pets['Frog-Base']
&& pets['Frog-CottonCandyBlue']
&& pets['Frog-CottonCandyPink']
&& pets['Frog-Desert']
&& pets['Frog-Golden']
&& pets['Frog-Red']
&& pets['Frog-Shade']
&& pets['Frog-Skeleton']
&& pets['Frog-White']
&& pets['Frog-Zombie']
&& pets['Snake-Base']
&& pets['Snake-CottonCandyBlue']
&& pets['Snake-CottonCandyPink']
&& pets['Snake-Desert']
&& pets['Snake-Golden']
&& pets['Snake-Red']
&& pets['Snake-Shade']
&& pets['Snake-Skeleton']
&& pets['Snake-White']
&& pets['Snake-Zombie']
&& pets['Spider-Base']
&& pets['Spider-CottonCandyBlue']
&& pets['Spider-CottonCandyPink']
&& pets['Spider-Desert']
&& pets['Spider-Golden']
&& pets['Spider-Red']
&& pets['Spider-Shade']
&& pets['Spider-Skeleton']
&& pets['Spider-White']
&& pets['Spider-Zombie']) {
set['achievements.duneBuddy'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.updateOne({ _id: user._id }, { $set: set }).exec();
}
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-09-16') },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

2
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "habitica",
"version": "5.7.0",
"version": "5.9.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View file

@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.7.0",
"version": "5.9.1",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",

View file

@ -153,6 +153,11 @@
width: 60px;
height: 64px;
}
.achievement-duneBuddy2x {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/achievement-duneBuddy2x.png');
width: 68px;
height: 68px;
}
.achievement-dustDevil2x {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/achievement-dustDevil2x.png');
width: 48px;

View file

@ -91,6 +91,7 @@ export default {
this.close();
},
hide () {
this.$store.dispatch('user:statSync');
this.$store.dispatch('user:set', { 'party.quest.completed': '' });
},
},

View file

@ -454,7 +454,7 @@
>
<div
:id="achievKey + '-achievement'"
class="box achievement-container"
class="box achievement-container d-flex justify-content-center align-items-middle"
:class="{'achievement-unearned': !achievement.earned}"
>
<b-popover
@ -472,7 +472,7 @@
</b-popover>
<div
v-if="achievement.earned"
class="achievement"
class="achievement m-auto"
:class="achievement.icon + '2x'"
>
<div
@ -484,7 +484,7 @@
</div>
<div
v-if="!achievement.earned"
class="achievement achievement-unearned achievement-unearned2x"
class="achievement achievement-unearned achievement-unearned2x m-auto"
></div>
</div>
</div>
@ -922,7 +922,6 @@
background: $white;
margin: 0 auto;
margin-bottom: 16px;
padding-top: 20px;
}
hr {

View file

@ -20,7 +20,7 @@
>
<div
:id="achievKey + '-achievement'"
class="box achievement-container"
class="box achievement-container d-flex justify-content-center align-items-middle"
:class="{'achievement-unearned': !achievement.earned}"
>
<b-popover
@ -38,7 +38,7 @@
</b-popover>
<div
v-if="achievement.earned"
class="achievement"
class="achievement m-auto"
:class="achievement.icon + '2x'"
>
<div
@ -142,7 +142,6 @@
background: $white;
margin: 0 auto;
margin-bottom: 16px;
padding-top: 20px;
}
hr {

View file

@ -33,6 +33,7 @@ export const NotificationMixins = {
this.notify(`${this.sign(val)}${val}`, 'damage');
},
exp (val) {
if (val === 0) return;
const message = getXPMessage(val);
this.notify(message, 'xp', 'glyphicon glyphicon-star', this.sign(val));
},

View file

@ -148,7 +148,7 @@ import svgGold from '@/assets/svg/gold.svg';
import level from '@/assets/svg/level.svg';
import streakIcon from '@/assets/svg/streak.svg';
import { mapState } from '@/libs/store';
import { MAX_LEVEL_HARD_CAP } from '../../../../../common/script/constants';
import { MAX_LEVEL_HARD_CAP, MAX_FIELD_HARD_CAP } from '../../../../../common/script/constants';
export default {
components: { SaveCancelButtons },
@ -231,10 +231,6 @@ export default {
return;
}
if (this.restoreValues.lvl > MAX_LEVEL_HARD_CAP) {
this.restoreValues.lvl = MAX_LEVEL_HARD_CAP;
}
const userChangedLevel = this.restoreValues.lvl !== this.user.stats.lvl;
const userDidNotChangeExp = this.restoreValues.exp === this.user.stats.exp;
if (userChangedLevel && userDidNotChangeExp) {
@ -265,6 +261,9 @@ export default {
) {
this.restoreValues[stat] = this.user.stats[stat];
valid = false;
} else if (this.restoreValues[stat] > MAX_FIELD_HARD_CAP) {
this.restoreValues[stat] = MAX_FIELD_HARD_CAP;
valid = false;
}
}
@ -274,6 +273,9 @@ export default {
|| inputLevel < 1) {
this.restoreValues.lvl = this.user.stats.lvl;
valid = false;
} else if (inputLevel > MAX_LEVEL_HARD_CAP) {
this.restoreValues.lvl = MAX_LEVEL_HARD_CAP;
valid = false;
}
const inputStreak = Number(this.restoreValues.streak);

View file

@ -8,6 +8,7 @@ import disableClassesOp from '@/../../common/script/ops/disableClasses';
import openMysteryItemOp from '@/../../common/script/ops/openMysteryItem';
import { unEquipByType } from '../../../../common/script/ops/unequip';
import markPMSRead from '../../../../common/script/ops/markPMSRead';
import updateStats from '../../../../common/script/fns/updateStats';
export function fetch (store, options = {}) { // eslint-disable-line no-shadow
return loadAsyncResource({
@ -179,6 +180,10 @@ export async function getPurchaseHistory () {
return response.data.data;
}
export function statSync (store) {
updateStats(store.state.user.data, store.state.user.data.stats);
return axios.post('/api/v4/user/stat-sync');
}
export function newPrivateMessageTo (store, params) {
const { member } = params;

View file

@ -153,5 +153,8 @@
"achievementDinosaurDynastyModalText": "You collected all the bird and dinosaur pets!",
"achievementBonelessBoss": "Boneless Boss",
"achievementBonelessBossText": "Has hatched all standard colors of invertebrate pets: Beetle, Butterfly, Cuttlefish, Nudibranch, Octopus, Snail, and Spider!",
"achievementBonelessBossModalText": "You collected all the invertebrate pets!"
"achievementBonelessBossModalText": "You collected all the invertebrate pets!",
"achievementDuneBuddy": "Dune Buddy",
"achievementDuneBuddyText": "Has hatched all standard colors of desert dwelling pets: Armadillo, Cactus, Fox, Frog, Snake, and Spider!",
"achievementDuneBuddyNotes": "You collected all the desert dwelling pets!"
}

View file

@ -2,6 +2,7 @@ export const MAX_HEALTH = 50;
export const MAX_LEVEL = 100;
export const MAX_STAT_POINTS = MAX_LEVEL;
export const MAX_LEVEL_HARD_CAP = 9999;
export const MAX_FIELD_HARD_CAP = 99999999;
export const ATTRIBUTES = ['str', 'int', 'con', 'per'];
export const MAX_INCENTIVES = 500;

View file

@ -193,6 +193,11 @@ const animalSetAchievs = {
titleKey: 'achievementDomesticated',
textKey: 'achievementDomesticatedText',
},
duneBuddy: {
icon: 'achievement-duneBuddy',
titleKey: 'achievementDuneBuddy',
textKey: 'achievementDuneBuddyText',
},
plantParent: {
icon: 'achievement-plantParent',
titleKey: 'achievementPlantParent',

View file

@ -72,6 +72,19 @@ const ANIMAL_SET_ACHIEVEMENTS = {
achievementKey: 'domesticated',
notificationType: 'ACHIEVEMENT_ANIMAL_SET',
},
duneBuddy: {
type: 'pet',
species: [
'Armadillo',
'Cactus',
'Fox',
'Frog',
'Snake',
'Spider',
],
achievementKey: 'duneBuddy',
notificationType: 'ACHIEVEMENT_ANIMAL_SET',
},
plantParent: {
type: 'pet',
species: [

View file

@ -10,14 +10,14 @@ const gemsPromo = {
export const EVENTS = {
noEvent: {
start: '2023-11-01T00:00-04:00',
end: '2023-12-21T08:00-04:00',
start: '2023-11-01T00:00-05:00',
end: '2023-12-21T08:00-05:00',
season: 'normal',
npcImageSuffix: '',
},
spooky_extra_gems: {
start: '2022-10-24T08:00-04:00',
end: '2022-10-31T23:59-04:00',
start: '2023-10-24T08:00-04:00',
end: '2023-10-31T23:59-04:00',
gemsPromo,
},
bundle202310: {

View file

@ -11,6 +11,7 @@ import {
MAX_INCENTIVES,
MAX_LEVEL,
MAX_LEVEL_HARD_CAP,
MAX_FIELD_HARD_CAP,
MAX_STAT_POINTS,
MAX_SUMMARY_SIZE_FOR_CHALLENGES,
MAX_SUMMARY_SIZE_FOR_GUILDS,
@ -124,6 +125,7 @@ api.constants = {
MAX_MESSAGE_LENGTH,
MAX_GIFT_MESSAGE_LENGTH,
MAX_LEVEL_HARD_CAP,
MAX_FIELD_HARD_CAP,
};
// TODO Move these under api.constants
api.maxLevel = MAX_LEVEL;

View file

@ -223,6 +223,7 @@ function _getBasicAchievements (user, language) {
_addSimple(result, user, { path: 'plantParent', language });
_addSimple(result, user, { path: 'dinosaurDynasty', language });
_addSimple(result, user, { path: 'bonelessBoss', language });
_addSimple(result, user, { path: 'duneBuddy', language });
_addSimpleWithMasterCount(result, user, { path: 'beastMaster', language });
_addSimpleWithMasterCount(result, user, { path: 'mountMaster', language });

View file

@ -1775,4 +1775,26 @@ api.movePinnedItem = {
},
};
/**
* @api {post} /api/v3/user/stat-sync
* Request a refresh of user stats, including processing of pending level-ups
* @apiName StatSync
* @apiGroup User
*
* @apiSuccess {Object} data The user object
*/
api.statSync = {
method: 'POST',
middlewares: [authWithHeaders()],
url: '/user/stat-sync',
async handler (req, res) {
const { user } = res.locals;
common.fns.updateStats(user, user.stats);
await user.save();
res.respond(200, user);
},
};
export default api;

View file

@ -362,6 +362,8 @@ schema.pre('save', true, function preSaveUser (next, done) {
}
}
// Enforce min/max values without displaying schema errors to end user
if (this.isDirectSelected('preferences')) {
if (
_.isNaN(this.preferences.dayStart)
@ -372,6 +374,20 @@ schema.pre('save', true, function preSaveUser (next, done) {
}
}
if (this.isSelected('stats')) {
const statMaximum = common.constants.MAX_FIELD_HARD_CAP;
const levelMaximum = common.constants.MAX_LEVEL_HARD_CAP;
_.each(['hp', 'mp', 'exp', 'gp'], stat => {
if (this.stats[stat] > statMaximum) {
this.stats[stat] = statMaximum;
}
});
if (this.stats.lvl > levelMaximum) {
this.stats.lvl = levelMaximum;
}
}
// our own version incrementer
if (this.isDirectSelected('_v')) {
if (_.isNaN(this._v) || !_.isNumber(this._v)) this._v = 0;

View file

@ -156,6 +156,7 @@ export const UserSchema = new Schema({
plantParent: Boolean,
dinosaurDynasty: Boolean,
bonelessBoss: Boolean,
duneBuddy: Boolean,
// Onboarding Guide
createdTask: Boolean,
completedTask: Boolean,