Merge branch 'release' into develop
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-console */
|
||||
import { sendTxn } from '../../../website/server/libs/email';
|
||||
import { sendTxn } from '../../website/server/libs/email';
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
import moment from 'moment';
|
||||
import nconf from 'nconf';
|
||||
|
||||
@@ -1,67 +1,24 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = 'full-stable';
|
||||
import each from 'lodash/each';
|
||||
import keys from 'lodash/keys';
|
||||
import content from '../../website/common/script/content/index';
|
||||
const migrationName = 'full-stable.js';
|
||||
const authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
|
||||
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
/*
|
||||
* Award users every extant pet and mount
|
||||
*/
|
||||
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
|
||||
let monk = require('monk');
|
||||
let dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
function processUsers (lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
let query = {
|
||||
'profile.name': 'SabreCat',
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId,
|
||||
};
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [
|
||||
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
return exiting(1, `ERROR! ${ err}`);
|
||||
});
|
||||
}
|
||||
|
||||
let progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
let userPromises = users.map(updateUser);
|
||||
let lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(() => {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
let set = {
|
||||
migration: migrationName,
|
||||
};
|
||||
|
||||
const set = {};
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
each(keys(content.pets), (pet) => {
|
||||
set[`items.pets.${pet}`] = 5;
|
||||
@@ -88,30 +45,40 @@ function updateUser (user) {
|
||||
set[`items.mounts.${mount}`] = true;
|
||||
});
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set: set});
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
if (user._id === authorUuid) console.warn(`${authorName } processed`);
|
||||
|
||||
return await User.update({_id: user._id}, {$set: set}).exec();
|
||||
}
|
||||
|
||||
function displayData () {
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.local.username': 'olson22',
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 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`);
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting (code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) {
|
||||
msg = 'ERROR!';
|
||||
}
|
||||
if (msg) {
|
||||
if (code) {
|
||||
console.error(msg);
|
||||
break;
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
process.exit(code);
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
|
||||
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "4.90.4",
|
||||
"version": "4.91.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.90.4",
|
||||
"version": "4.91.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@google-cloud/trace-agent": "^3.6.0",
|
||||
|
||||
@@ -53,7 +53,7 @@ async function _deleteHabiticaData (user, email) {
|
||||
|
||||
if (response) {
|
||||
console.log(`${response.status} ${response.statusText}`);
|
||||
if (response.status === 200) console.log(`${user._id} removed. Last login: ${user.auth.timestamps.loggedin}`);
|
||||
if (response.status === 200) console.log(`${user._id} (${email}) removed. Last login: ${user.auth.timestamps.loggedin}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -335,14 +335,13 @@ describe('analyticsService', () => {
|
||||
let data, itemSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
Visitor.prototype.event.yields();
|
||||
|
||||
itemSpy = sandbox.stub().returnsThis();
|
||||
|
||||
Visitor.prototype.event.returns({
|
||||
send: sandbox.stub(),
|
||||
});
|
||||
Visitor.prototype.transaction.returns({
|
||||
item: itemSpy,
|
||||
send: sandbox.stub().returnsThis(),
|
||||
send: sandbox.stub().yields(),
|
||||
});
|
||||
|
||||
data = {
|
||||
|
||||
@@ -1,72 +1,60 @@
|
||||
.promo_april_fools_2019 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -719px -767px;
|
||||
background-position: -445px -163px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_armoire_backgrounds_201903 {
|
||||
.promo_armoire_backgrounds_201904 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -933px;
|
||||
background-position: 0px -337px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_beffymaroo_wondercon {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px 0px;
|
||||
width: 718px;
|
||||
height: 932px;
|
||||
}
|
||||
.promo_celestial_rainbow_potions {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -933px;
|
||||
background-position: -424px -337px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_classes_spring2019 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -719px -604px;
|
||||
background-position: -445px 0px;
|
||||
width: 432px;
|
||||
height: 162px;
|
||||
}
|
||||
.promo_egg_hunt {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -1160px 0px;
|
||||
background-position: 0px -485px;
|
||||
width: 354px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_mystery_201903 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -1160px -148px;
|
||||
background-position: -355px -485px;
|
||||
width: 351px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_seasonalshop_spring {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -1160px -492px;
|
||||
background-position: -707px -485px;
|
||||
width: 162px;
|
||||
height: 138px;
|
||||
}
|
||||
.promo_shiny_seeds {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -633px;
|
||||
width: 351px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -1401px -296px;
|
||||
background-position: -352px -633px;
|
||||
width: 96px;
|
||||
height: 69px;
|
||||
}
|
||||
.scene_dailies {
|
||||
.scene_hat_guild {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -719px -327px;
|
||||
width: 327px;
|
||||
height: 276px;
|
||||
}
|
||||
.scene_tavern {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -719px 0px;
|
||||
width: 440px;
|
||||
height: 326px;
|
||||
}
|
||||
.scene_todos {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -1160px -296px;
|
||||
width: 240px;
|
||||
height: 195px;
|
||||
background-position: 0px 0px;
|
||||
width: 444px;
|
||||
height: 336px;
|
||||
}
|
||||
|
||||
BIN
website/client/assets/images/Pet_HatchingPotion_Veggie.gif
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 983 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 557 KiB After Width: | Height: | Size: 556 KiB |
|
Before Width: | Height: | Size: 562 KiB After Width: | Height: | Size: 570 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 344 KiB |
|
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 322 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 116 KiB |
@@ -481,5 +481,13 @@
|
||||
"backgroundFieldWithColoredEggsText": "Field with Colored Eggs",
|
||||
"backgroundFieldWithColoredEggsNotes": "Hunt for springtime treasure in a Field with Colored Eggs.",
|
||||
"backgroundFlowerMarketText": "Flower Market",
|
||||
"backgroundFlowerMarketNotes": "Find the perfect colors for bouquet or garden in a Flower Market."
|
||||
"backgroundFlowerMarketNotes": "Find the perfect colors for bouquet or garden in a Flower Market.",
|
||||
|
||||
"backgrounds042019": "SET 59: Released April 2019",
|
||||
"backgroundBirchForestText": "Birch Forest",
|
||||
"backgroundBirchForestNotes": "Dally in a peaceful Birch Forest.",
|
||||
"backgroundHalflingsHouseText": "Halfling's House",
|
||||
"backgroundHalflingsHouseNotes": "Visit a charming Halfling's House.",
|
||||
"backgroundBlossomingDesertText": "Blossoming Desert",
|
||||
"backgroundBlossomingDesertNotes": "Witness a rare superbloom in the Blossoming Desert."
|
||||
}
|
||||
|
||||
@@ -418,6 +418,10 @@
|
||||
"weaponArmoireChefsSpoonNotes": "Raise it as you release your battle cry: “SPOOOON!!” Increases Intelligence by <%= int %>. Enchanted Armoire: Chef Set (Item 3 of 4).",
|
||||
"weaponArmoireVernalTaperText": "Vernal Taper",
|
||||
"weaponArmoireVernalTaperNotes": "The days are getting longer, but this candle will help you find your way before sunrise. Increases Constitution by <%= con %>. Enchanted Armoire: Vernal Vestments Set (Item 3 of 3).",
|
||||
"weaponArmoireJugglingBallsText": "Juggling Balls",
|
||||
"weaponArmoireJugglingBallsNotes": "Habiticans are master multi-taskers, so you should have no trouble keeping all these balls in the air! Increases Intelligence by <%= int %>. Enchanted Armoire: Independent Item.",
|
||||
"weaponArmoireSlingshotText": "Slingshot",
|
||||
"weaponArmoireSlingshotNotes": "Take aim at your red Dailies! Increases Strength by <%= str %>. Enchanted Armoire: Independent Item.",
|
||||
|
||||
"armor": "armor",
|
||||
"armorCapitalized": "Armor",
|
||||
@@ -1406,6 +1410,8 @@
|
||||
"headArmoireToqueBlancheNotes": "According to legend, the number of folds in this hat indicate the number of ways you know how to cook an egg! Is it accurate? Increases Perception by <%= per %>. Enchanted Armoire: Chef Set (Item 1 of 4).",
|
||||
"headArmoireVernalHenninText": "Vernal Hennin",
|
||||
"headArmoireVernalHenninNotes": "More than just a pretty hat, this conical chapeau can also hold a rolled-up To-Do list inside. Increases Perception by <%= per %>. Enchanted Armoire: Vernal Vestments Set (Item 1 of 3).",
|
||||
"headArmoireTricornHatText": "Tricorn Hat",
|
||||
"headArmoireTricornHatNotes": "Become a revolutionary jokester! Increases Perception by <%= per %>. Enchanted Armoire: Independent Item.",
|
||||
|
||||
"offhand": "off-hand item",
|
||||
"offhandCapitalized": "Off-Hand Item",
|
||||
|
||||
@@ -815,6 +815,20 @@ let backgrounds = {
|
||||
notes: t('backgroundFlowerMarketNotes'),
|
||||
},
|
||||
},
|
||||
backgrounds042019: {
|
||||
halflings_house: {
|
||||
text: t('backgroundHalflingsHouseText'),
|
||||
notes: t('backgroundHalflingsHouseNotes'),
|
||||
},
|
||||
blossoming_desert: {
|
||||
text: t('backgroundBlossomingDesertText'),
|
||||
notes: t('backgroundBlossomingDesertNotes'),
|
||||
},
|
||||
birch_forest: {
|
||||
text: t('backgroundBirchForestText'),
|
||||
notes: t('backgroundBirchForestNotes'),
|
||||
},
|
||||
},
|
||||
incentiveBackgrounds: {
|
||||
violet: {
|
||||
text: t('backgroundVioletText'),
|
||||
|
||||
@@ -924,6 +924,13 @@ let head = {
|
||||
set: 'vernalVestments',
|
||||
canOwn: ownsItem('head_armoire_vernalHennin'),
|
||||
},
|
||||
tricornHat: {
|
||||
text: t('headArmoireTricornHatText'),
|
||||
notes: t('headArmoireTricornHatNotes', { per: 10 }),
|
||||
value: 100,
|
||||
per: 10,
|
||||
canOwn: ownsItem('head_armoire_tricornHat'),
|
||||
},
|
||||
};
|
||||
|
||||
let shield = {
|
||||
@@ -1553,6 +1560,20 @@ let weapon = {
|
||||
set: 'vernalVestments',
|
||||
canOwn: ownsItem('weapon_armoire_vernalTaper'),
|
||||
},
|
||||
jugglingBalls: {
|
||||
text: t('weaponArmoireJugglingBallsText'),
|
||||
notes: t('weaponArmoireJugglingBallsNotes', { int: 10 }),
|
||||
value: 100,
|
||||
int: 10,
|
||||
canOwn: ownsItem('weapon_armoire_jugglingBalls'),
|
||||
},
|
||||
slingshot: {
|
||||
text: t('weaponArmoireSlingshotText'),
|
||||
notes: t('weaponArmoireSlingshotNotes', { str: 10 }),
|
||||
value: 100,
|
||||
str: 10,
|
||||
canOwn: ownsItem('weapon_armoire_slingshot'),
|
||||
},
|
||||
};
|
||||
|
||||
let armoireSet = {
|
||||
|
||||
@@ -8,12 +8,12 @@ const featuredItems = {
|
||||
path: 'armoire',
|
||||
},
|
||||
{
|
||||
type: 'hatchingPotions',
|
||||
path: 'hatchingPotions.Golden',
|
||||
type: 'premiumHatchingPotion',
|
||||
path: 'premiumHatchingPotions.Celestial',
|
||||
},
|
||||
{
|
||||
type: 'eggs',
|
||||
path: 'eggs.PandaCub',
|
||||
type: 'premiumHatchingPotion',
|
||||
path: 'premiumHatchingPotions.Rainbow',
|
||||
},
|
||||
{
|
||||
type: 'card',
|
||||
|
||||
@@ -19,6 +19,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
availableSpells: [
|
||||
'shinySeed',
|
||||
],
|
||||
|
||||
availableQuests: [
|
||||
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 423 B |
|
After Width: | Height: | Size: 386 B |
|
After Width: | Height: | Size: 614 B |
|
After Width: | Height: | Size: 333 B |
|
After Width: | Height: | Size: 696 B |
|
After Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 915 B After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 711 KiB |
BIN
website/raw_sprites/spritesmith_large/promo_shiny_seeds.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
BIN
website/raw_sprites/spritesmith_large/scene_hat_guild.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
@@ -3,7 +3,7 @@ import { authWithHeaders } from '../../middlewares/auth';
|
||||
let api = {};
|
||||
|
||||
// @TODO export this const, cannot export it from here because only routes are exported from controllers
|
||||
const LAST_ANNOUNCEMENT_TITLE = 'THE APRIL FOOL STRIKES AGAIN!';
|
||||
const LAST_ANNOUNCEMENT_TITLE = 'NEW BACKGROUNDS AND ARMOIRE ITEMS, MONTHLY CHALLENGES, AND SHINY SEEDS';
|
||||
const worldDmg = { // @TODO
|
||||
bailey: false,
|
||||
};
|
||||
@@ -30,21 +30,30 @@ api.getNews = {
|
||||
<div class="mr-3 ${baileyClass}"></div>
|
||||
<div class="media-body">
|
||||
<h1 class="align-self-center">${res.t('newStuff')}</h1>
|
||||
<h2>4/1/2019 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
|
||||
<h2>4/2/2019 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="promo_april_fools_2019 center-block"></div>
|
||||
<h3>Fruit and Veggie Pets and NPCs</h3>
|
||||
<p>The April Fool has appeared, and he's got a farmer's market's worth of produce in tow.</p>
|
||||
<p>"HAHA!" he cries, as a dragonfruit bounces along beside him. "I've always thought good humor should be healthful and nourishing, so I've gone back to my roots, if you will, to bring some plant-powered goodness into Habitica once again!"</p>
|
||||
<p>"He's replaced all our equipped pets with fruits and vegetables!" says QuartzFox, gently patting a contented-looking tomato. "Although to be fair, they are very cute fruits and vegetables!"</p>
|
||||
<p>Equipping different pets will show different fruits and veggies. Have fun discovering them all!</p>
|
||||
<p>The NPCs have also been turned into their fruit and vegetable forms as a tribute to Habitica's <a href="https://habitica.fandom.com/wiki/April_Fools'_Day_2014" target='_blank'>very first April Fool's prank back in 2014</a>! Go check them out.</p>
|
||||
<h3>Special April Fool's Social Media Challenge!</h3>
|
||||
<p>For even more fun, check out the <a href='/challenges/b0337534-ec69-4269-8cc6-f74c91881451'>official Challenge</a> posted especially for today! Share your avatar featuring your new fruit and veggie pet on social media between now and April 3, and you'll have a chance to win gems and have your avatar featured on the Habitica Blog!</p>
|
||||
<div class="small mb-3">by Beffymaroo, SabreCat, Piyo, Viirus, and Lemoness</div>
|
||||
<div class="npc_aprilFool center-block"></div>
|
||||
<div class="promo_armoire_backgrounds_201904 center-block"></div>
|
||||
<h3>April Backgrounds and Armoire Items</h3>
|
||||
<p>We’ve added three new backgrounds to the Background Shop! Now your avatar can visit a Halfling's House, dally through a peaceful Birch Forest, and take in the Superbloom in the Blossoming Desert. Check them out under User Icon > Backgrounds!</p>
|
||||
<p>Plus, there’s new Gold-purchasable equipment in the Enchanted Armoire, including some fun joke props in honor of April Fool's Day! Better work hard on your real-life tasks to earn all the pieces! Enjoy :)</p>
|
||||
<div class="small mb-3">by Vikte, QuartzFox, Katy133, GeraldThePixel, and Gully</div>
|
||||
<div class="scene_hat_guild center-block"></div>
|
||||
<h3>April 2019 Resolution Success Challenge and New Take This Challenge</h3>
|
||||
<p>The Habitica team has launched a special official Challenge series hosted in the <a href='/groups/guild/6e6a8bd3-9f5f-4351-9188-9f11fcd80a99' target='_blank'>Official New Year's Resolution Guild</a>. These Challenges are designed to help you build and maintain goals that are destined for success and then stick with them as the year progresses. For this month's Challenge, <a href='/challenges/ae4a6ab8-e4c7-46fb-ba48-a5f05610a55d'>Gather Your Party</a>, we're focusing on finding encouraging allies to help you gain accountability for your goals! It has a 15 Gem prize, which will be awarded to five lucky winners on May 1st.</p>
|
||||
<p>Congratulations to the winners of March's Challenge, DcryptMart, LONEW0LF, Elcaracol, DungeonMasterful, and 7NationTpr!</p>
|
||||
<div class="promo_take_this center-block"></div>
|
||||
<p>The next Take This Challenge has also launched, "<a href='/challenges/5712376e-89f1-4f8b-89eb-8f94026d0da9'>Harder, Faster, Stronger!</a>", with a focus on setting and meeting physical activity goals. Be sure to check it out to earn additional pieces of the Take This armor set!</p>
|
||||
<p><a href='http://www.takethis.org/' target='_blank'>Take This</a> is a nonprofit that seeks to inform the gamer community about mental health issues, to provide education about mental disorders and mental illness prevention, and to reduce the stigma of mental illness.</p>
|
||||
<p>Congratulations to the winners of the last Take This Challenge, "Do One Thing Well!": grand prize winner Денис Кадников, and runners-up addone, alihenri, Hemogoblin3991, Kalu_Ienvru, and gabriellamara! Plus, all participants in that Challenge have received a piece of the <a href='http://habitica.wikia.com/wiki/Event_Item_Sequences#Take_This_Armor_Set' target='_blank'>Take This item set</a> if they hadn't completed it already. It is located in your Rewards column. Enjoy!</p>
|
||||
<div class="small mb-3">by Doctor B, the Take This team, Lemoness, Beffymaroo, and SabreCat</div>
|
||||
<div class="promo_shiny_seeds center-block"></div>
|
||||
<h3>Shiny Seeds</h3>
|
||||
<p>Throw a Shiny Seed at your friends and they will turn into a cheerful flower until their next cron! You can buy the Seeds in the <a href='/shops/seasonal'>Seasonal Shop</a> for Gold. Plus, if you get transformed by a Shiny Seed, you'll receive the Agricultural Friends badge!</p>
|
||||
<p>Don't want to be a flower? Just buy some Petal-Free Potion from your Rewards column to reverse it.</p>
|
||||
<p>Shiny Seeds will be available in the <a href='/shops/seasonal'>Seasonal Shop</a> until April 30th!</p>
|
||||
<div class="small mb-3">by Lemoness</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
toArray,
|
||||
} from 'lodash';
|
||||
import { content as Content } from '../../common';
|
||||
import logger from './logger';
|
||||
|
||||
const AMPLITUDE_TOKEN = nconf.get('AMPLITUDE_KEY');
|
||||
const GA_TOKEN = nconf.get('GA_ID');
|
||||
@@ -27,7 +28,7 @@ if (AMPLITUDE_TOKEN) amplitude = new Amplitude(AMPLITUDE_TOKEN);
|
||||
|
||||
let ga = googleAnalytics(GA_TOKEN);
|
||||
|
||||
let _lookUpItemName = (itemKey) => {
|
||||
function _lookUpItemName (itemKey) {
|
||||
if (!itemKey) return;
|
||||
|
||||
let gear = Content.gear.flat[itemKey];
|
||||
@@ -54,9 +55,9 @@ let _lookUpItemName = (itemKey) => {
|
||||
}
|
||||
|
||||
return itemName;
|
||||
};
|
||||
}
|
||||
|
||||
let _formatUserData = (user) => {
|
||||
function _formatUserData (user) {
|
||||
let properties = {};
|
||||
|
||||
if (user.stats) {
|
||||
@@ -102,9 +103,9 @@ let _formatUserData = (user) => {
|
||||
}
|
||||
|
||||
return properties;
|
||||
};
|
||||
}
|
||||
|
||||
let _formatPlatformForAmplitude = (platform) => {
|
||||
function _formatPlatformForAmplitude (platform) {
|
||||
if (!platform) {
|
||||
return 'Unknown';
|
||||
}
|
||||
@@ -114,9 +115,9 @@ let _formatPlatformForAmplitude = (platform) => {
|
||||
}
|
||||
|
||||
return '3rd Party';
|
||||
};
|
||||
}
|
||||
|
||||
let _formatUserAgentForAmplitude = (platform, agentString) => {
|
||||
function _formatUserAgentForAmplitude (platform, agentString) {
|
||||
if (!agentString) {
|
||||
return 'Unknown';
|
||||
}
|
||||
@@ -135,14 +136,18 @@ let _formatUserAgentForAmplitude = (platform, agentString) => {
|
||||
}
|
||||
|
||||
return formattedAgent;
|
||||
};
|
||||
}
|
||||
|
||||
let _formatDataForAmplitude = (data) => {
|
||||
function _formatUUIDForAmplitude (uuid) {
|
||||
return uuid || 'no-user-id-was-provided';
|
||||
}
|
||||
|
||||
function _formatDataForAmplitude (data) {
|
||||
let event_properties = omit(data, AMPLITUDE_PROPERTIES_TO_SCRUB);
|
||||
let platform = _formatPlatformForAmplitude(data.headers && data.headers['x-client']);
|
||||
let agent = _formatUserAgentForAmplitude(platform, data.headers && data.headers['user-agent']);
|
||||
let ampData = {
|
||||
user_id: data.uuid || 'no-user-id-was-provided',
|
||||
user_id: _formatUUIDForAmplitude(data.uuid),
|
||||
platform,
|
||||
os_name: agent.name,
|
||||
os_version: agent.version,
|
||||
@@ -159,21 +164,19 @@ let _formatDataForAmplitude = (data) => {
|
||||
ampData.event_properties.itemName = itemName;
|
||||
}
|
||||
return ampData;
|
||||
};
|
||||
}
|
||||
|
||||
let _sendDataToAmplitude = (eventType, data) => {
|
||||
function _sendDataToAmplitude (eventType, data) {
|
||||
let amplitudeData = _formatDataForAmplitude(data);
|
||||
|
||||
amplitudeData.event_type = eventType;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
amplitude.track(amplitudeData)
|
||||
.then(resolve)
|
||||
.catch(() => reject('Error while sending data to Amplitude.'));
|
||||
});
|
||||
};
|
||||
return amplitude
|
||||
.track(amplitudeData)
|
||||
.catch(err => logger.error(err, 'Error while sending data to Amplitude.'));
|
||||
}
|
||||
|
||||
let _generateLabelForGoogleAnalytics = (data) => {
|
||||
function _generateLabelForGoogleAnalytics (data) {
|
||||
let label;
|
||||
|
||||
each(GA_POSSIBLE_LABELS, (key) => {
|
||||
@@ -184,9 +187,9 @@ let _generateLabelForGoogleAnalytics = (data) => {
|
||||
});
|
||||
|
||||
return label;
|
||||
};
|
||||
}
|
||||
|
||||
let _generateValueForGoogleAnalytics = (data) => {
|
||||
function _generateValueForGoogleAnalytics (data) {
|
||||
let value;
|
||||
|
||||
each(GA_POSSIBLE_VALUES, (key) => {
|
||||
@@ -197,9 +200,9 @@ let _generateValueForGoogleAnalytics = (data) => {
|
||||
});
|
||||
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
let _sendDataToGoogle = (eventType, data) => {
|
||||
function _sendDataToGoogle (eventType, data) {
|
||||
let eventData = {
|
||||
ec: data.gaCategory || data.category || 'behavior',
|
||||
ea: eventType,
|
||||
@@ -217,28 +220,28 @@ let _sendDataToGoogle = (eventType, data) => {
|
||||
eventData.ev = value;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
ga.event(eventData, (err) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
let _sendPurchaseDataToAmplitude = (data) => {
|
||||
return promise.catch(err => logger.error(err, 'Error while sending data to Google Analytics.'));
|
||||
}
|
||||
|
||||
function _sendPurchaseDataToAmplitude (data) {
|
||||
let amplitudeData = _formatDataForAmplitude(data);
|
||||
|
||||
amplitudeData.event_type = 'purchase';
|
||||
amplitudeData.revenue = data.purchaseValue;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
amplitude.track(amplitudeData)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
return amplitude
|
||||
.track(amplitudeData)
|
||||
.catch(err => logger.error(err, 'Error while sending data to Amplitude.'));
|
||||
}
|
||||
|
||||
let _sendPurchaseDataToGoogle = (data) => {
|
||||
function _sendPurchaseDataToGoogle (data) {
|
||||
let label = data.paymentMethod;
|
||||
let type = data.purchaseType;
|
||||
let price = data.purchaseValue;
|
||||
@@ -256,38 +259,55 @@ let _sendPurchaseDataToGoogle = (data) => {
|
||||
ev: price,
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
ga.event(eventData).send();
|
||||
|
||||
ga.transaction(data.uuid, price)
|
||||
.item(price, qty, sku, itemKey, variation)
|
||||
.send();
|
||||
|
||||
const eventPromise = new Promise((resolve, reject) => {
|
||||
ga.event(eventData, (err) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
let _setOnce = (data) => {
|
||||
return amplitude.identify({
|
||||
user_properties: {
|
||||
$setOnce: data,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function track (eventType, data) {
|
||||
const transactionPromise = new Promise((resolve, reject) => {
|
||||
ga.transaction(data.uuid, price)
|
||||
.item(price, qty, sku, itemKey, variation)
|
||||
.send(err => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return Promise
|
||||
.all([eventPromise, transactionPromise])
|
||||
.catch(err => logger.error(err, 'Error while sending data to Google Analytics.'));
|
||||
}
|
||||
|
||||
function _setOnce (dataToSetOnce, uuid) {
|
||||
return amplitude
|
||||
.identify({
|
||||
user_id: _formatUUIDForAmplitude(uuid),
|
||||
user_properties: {
|
||||
$setOnce: dataToSetOnce,
|
||||
},
|
||||
})
|
||||
.catch(err => logger.error(err, 'Error while sending data to Amplitude.'));
|
||||
}
|
||||
|
||||
// There's no error handling directly here because it's handled inside _sendDataTo{Amplitude|Google}
|
||||
async function track (eventType, data) {
|
||||
let promises = [
|
||||
_sendDataToAmplitude(eventType, data),
|
||||
_sendDataToGoogle(eventType, data),
|
||||
];
|
||||
if (data.user && data.user.registeredThrough) {
|
||||
promises.push(_setOnce({registeredPlatform: data.user.registeredThrough}));
|
||||
promises.push(_setOnce({
|
||||
registeredPlatform: data.user.registeredThrough,
|
||||
}, data.uuid || data.user._id));
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
function trackPurchase (data) {
|
||||
// There's no error handling directly here because it's handled inside _sendPurchaseDataTo{Amplitude|Google}
|
||||
async function trackPurchase (data) {
|
||||
return Promise.all([
|
||||
_sendPurchaseDataToAmplitude(data),
|
||||
_sendPurchaseDataToGoogle(data),
|
||||
@@ -295,7 +315,7 @@ function trackPurchase (data) {
|
||||
}
|
||||
|
||||
// Stub for non-prod environments
|
||||
let mockAnalyticsService = {
|
||||
const mockAnalyticsService = {
|
||||
track: () => { },
|
||||
trackPurchase: () => { },
|
||||
};
|
||||
|
||||