Compare commits

..

5 Commits

Author SHA1 Message Date
Phillip Thelen 23059231ce start building out more sophisticated rate limiting 2024-07-22 17:51:12 +02:00
Phillip Thelen 23163043c2 correct math 2024-07-19 11:41:16 +02:00
Phillip Thelen a18b8265a5 fix tests and add new one 2024-07-19 11:18:32 +02:00
Phillip Thelen ce1db6923b make rate limiter config names more consistent 2024-07-19 11:18:15 +02:00
Phillip Thelen 2465189fb1 Improve rate limiting 2024-07-18 18:49:58 +02:00
303 changed files with 19209 additions and 5076 deletions
+10 -20
View File
@@ -19,8 +19,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -42,8 +41,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -65,8 +63,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -89,8 +86,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -112,8 +108,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -142,8 +137,7 @@ jobs:
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -173,8 +167,7 @@ jobs:
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -204,8 +197,7 @@ jobs:
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -230,8 +222,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -255,8 +246,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
+6 -12
View File
@@ -1,20 +1,14 @@
Habitica ![Build Status](https://github.com/HabitRPG/habitica/workflows/Test/badge.svg)
Habitica ![Build Status](https://github.com/HabitRPG/habitica/workflows/Test/badge.svg) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
===============
[Habitica](https://habitica.com) is an open-source habit-building program that treats your life like a role-playing game. Level up as you succeed, lose HP as you fail, and earn Gold to buy weapons and armor!
[Habitica](https://habitica.com) is an open-source habit-building program that treats your life like a role-playing game. Level up as you succeed, lose HP as you fail, and earn money to buy weapons and armor.
**Want to contribute code to Habitica?** We're always looking for assistance on any issues in our repo with the "Help Wanted" label. The wiki pages below and the additional linked pages will tell you how to start contributing code and where you can seek further help or ask questions:
**We need more programmers!** Your assistance will be greatly appreciated. The wiki pages below and the additional pages they link to will tell you how to get started on contributing code and where you can go to seek further help or ask questions:
* [Guidance for Blacksmiths](https://habitica.fandom.com/wiki/Guidance_for_Blacksmiths) - an introduction to the technologies used and how the software is organized.
* [Setting up Habitica Locally](https://github.com/HabitRPG/habitica/wiki/Setting-Up-Habitica-for-Local-Development) - how to set up a local install of Habitica for development and testing.
**Interested in contributing to Habiticas mobile apps?** Visit the links below for our mobile repositories.
* **Android:** https://github.com/HabitRPG/habitica-android
* **iOS:** https://github.com/HabitRPG/habitica-ios
* [Setting up Habitica Locally](https://habitica.fandom.com/wiki/Setting_up_Habitica_Locally) - how to set up a local install of Habitica for development and testing on various platforms.
Habitica's code is licensed as described at https://github.com/HabitRPG/habitica/blob/develop/LICENSE
**Found a bug?** Please report it to [admin email](mailto:admin@habitica.com) rather than create an issue (an admin will advise you if a new issue is necessary; usually it is not).
**Found a bug?** Please report it to [admin email](mailto:admin@habitica.com) rather than creating an issue (an admin will advise you if a new issue is necessary; usually it is not).
**Creating a third-party tool?** Please review our [API Usage Guidelines](https://github.com/HabitRPG/habitica/wiki/API-Usage-Guidelines) to ensure that your tool is compliant and maintains the best experience for Habitica players.
**Have any questions about Habitica or contributing?** See the links in the [Habitica](https://habitica.com) website's Help menu. Theres FAQs, guides, and the option to reach out to us with any further questions!
**Have any questions about Habitica or its community?** See the links in the [habitica.com](https://habitica.com) website's Help menu or drop in to [Guilds > Tavern Chat](https://habitica.com/groups/tavern) to ask questions or chat socially!
-1
View File
@@ -37,7 +37,6 @@
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
"MONGODB_POOL_SIZE": "10",
"MONGODB_SOCKET_TIMEOUT": "20000",
"NODE_ENV": "development",
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
+2 -33
View File
@@ -42,41 +42,10 @@ function cssVarMap (sprite) {
}
}
function filterFile (file) {
if (file.relative.indexOf('Mount_Icon_') !== -1) {
return false;
}
if (file.path.indexOf('shop/') !== -1) {
return false;
}
if (file.path.indexOf('stable/eggs') !== -1) {
return false;
}
if (file.path.indexOf('stable/food') !== -1) {
return false;
}
if (file.path.indexOf('stable/potions') !== -1) {
return false;
}
if (file.relative.indexOf('shop_') === 0) {
return false;
}
if (file.relative.indexOf('icon_background') === 0) {
return false;
}
return true;
}
async function createSpritesStream (name, src) {
function createSpritesStream (name, src) {
const stream = mergeStream();
// need to import this way bc of weird dependency things
// eslint-disable-next-line global-require
const filter = require('gulp-filter');
const f = filter(filterFile);
const spriteData = gulp.src(src)
.pipe(f)
.pipe(spritesmith({
imgName: `spritesmith-${name}.png`,
cssName: `spritesmith-${name}.css`,
@@ -94,7 +63,7 @@ async function createSpritesStream (name, src) {
return stream;
}
gulp.task('sprites:main', async () => {
gulp.task('sprites:main', () => {
const mainSrc = sync('habitica-images/**/*.png');
return createSpritesStream('main', mainSrc);
});
@@ -1,12 +1,14 @@
/* eslint-disable no-console */
import { model as User } from '../../website/server/models/user';
const MIGRATION_NAME = '20230731_naming_day';
import { v4 as uuid } from 'uuid';
import { model as User } from '../../../website/server/models/user';
const MIGRATION_NAME = '20240731_naming_day';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count += 1;
count++;
let set;
let push;
@@ -113,16 +115,16 @@ async function updateUser (user) {
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (push) {
return user.updateOne({ $set: set, $inc: inc, $push: push }).exec();
return await user.updateOne({ $set: set, $inc: inc, $push: push }).exec();
} else {
return await user.updateOne({ $set: set, $inc: inc }).exec();
}
return user.updateOne({ $set: set, $inc: inc }).exec();
}
export default async function processUsers () {
const query = {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2024-07-01') },
'auth.timestamps.loggedin': { $gt: new Date('2023-07-01') },
};
const fields = {
@@ -134,7 +136,7 @@ export default async function processUsers () {
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({ _id: 1 })
.sort({_id: 1})
.select(fields)
.exec();
@@ -150,4 +152,4 @@ export default async function processUsers () {
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
}
};
@@ -1,47 +0,0 @@
/* eslint-disable no-console */
import { model as User } from '../../../website/server/models/user';
const MIGRATION_NAME = '2024_purge_invite_accepted';
const progressCount = 1000;
let count = 0;
async function updateUsers (userIds) {
count += userIds.length;
if (count % progressCount === 0) console.warn(`${count} ${userIds[0]}`);
return await User.updateMany(
{ _id: { $in: userIds } },
{ $pull: { notifications: { type: 'GROUP_INVITE_ACCEPTED' } } },
).exec();
}
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'notifications.type': 'GROUP_INVITE_ACCEPTED',
'auth.timestamps.loggedin': { $gt: new Date('2024-06-25') },
};
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({ _id: 1 })
.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],
};
}
const userIds = users.map(user => user._id);
await updateUsers(userIds); // eslint-disable-line no-await-in-loop
}
};
+13 -893
View File
File diff suppressed because it is too large Load Diff
+1 -4
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.27.4",
"version": "5.26.1",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
@@ -15,7 +15,6 @@
"amplitude": "^6.0.0",
"apidoc": "^0.54.0",
"apple-auth": "^1.0.9",
"babel-preset-env": "^1.7.0",
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"bootstrap": "^4.6.2",
@@ -36,7 +35,6 @@
"got": "^11.8.6",
"gulp": "^4.0.0",
"gulp-babel": "^8.0.0",
"gulp-filter": "^7.0.0",
"gulp-imagemin": "^7.1.0",
"gulp-nodemon": "^2.5.0",
"gulp.spritesmith": "^6.13.0",
@@ -76,7 +74,6 @@
"useragent": "^2.1.9",
"uuid": "^9.0.0",
"validator": "^13.11.0",
"webpack-bundle-analyzer": "^4.10.2",
"winston": "^3.10.0",
"winston-loggly-bulk": "^3.3.0",
"xml2js": "^0.6.2"
+21 -1
View File
@@ -10,7 +10,7 @@ import { TooManyRequests } from '../../../../website/server/libs/errors';
import { apiError } from '../../../../website/server/libs/apiError';
import logger from '../../../../website/server/libs/logger';
describe('rateLimiter middleware', () => {
describe.only('rateLimiter middleware', () => {
const pathToRateLimiter = '../../../../website/server/middlewares/rateLimiter';
let res; let req; let next; let nconfGetStub;
@@ -253,4 +253,24 @@ describe('rateLimiter middleware', () => {
'X-RateLimit-Reset': sinon.match(Date),
});
});
describe('authentication rate limiting', async () => {
it('applies cost for failed login attempts', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.path = '/api/v4/user/auth/local/login';
req.ip = 1;
await attachRateLimiter(req, res, next);
await attachRateLimiter(req, res, next);
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 28,
'X-RateLimit-Reset': sinon.match(Date),
});
});
});
});
@@ -3,7 +3,6 @@ import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { model as Group } from '../../../../../website/server/models/group';
describe('GET /groups/:groupId/chat', () => {
let user;
@@ -38,34 +37,4 @@ describe('GET /groups/:groupId/chat', () => {
});
});
});
context('public Guild', () => {
let group;
before(async () => {
({ group } = await createAndPopulateGroup({
groupDetails: {
name: 'test group',
type: 'guild',
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
chat: [
'Hello',
'Welcome to the Guild',
],
}));
// Creation API is shut down, we need to simulate an extant public group
await Group.updateOne({ _id: group._id }, { $set: { privacy: 'public' }, $unset: { 'purchased.plan': 1 } }).exec();
});
it('returns error if user attempts to fetch a sunset Guild', async () => {
await expect(user.get(`/groups/${group._id}/chat`)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('featureRetired'),
});
});
});
});
@@ -4,7 +4,6 @@ import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { model as Group } from '../../../../../website/server/models/group';
describe('POST /chat/:chatId/like', () => {
let user;
@@ -112,18 +111,4 @@ describe('POST /chat/:chatId/like', () => {
message: t('groupNotFound'),
});
});
it('does not like a message that belongs to a sunset public group', async () => {
const message = await anotherUser.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
// Creation API is shut down, we need to simulate an extant public group
await Group.updateOne({ _id: groupWithChat._id }, { $set: { privacy: 'public' }, $unset: { 'purchased.plan': 1 } }).exec();
await expect(user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('featureRetired'),
});
});
});
@@ -1,73 +0,0 @@
import nconf from 'nconf';
import {
generateUser,
createAndPopulateGroup,
} from '../../../../helpers/api-integration/v3';
describe('POST /debug/boss-rage', () => {
let user;
let nconfStub;
beforeEach(async () => {
user = await generateUser();
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconfStub.restore();
});
it('errors if user is not in a party', async () => {
await expect(user.post('/debug/boss-rage'))
.to.eventually.be.rejected.and.deep.equal({
code: 400,
error: 'BadRequest',
message: 'User not in a party.',
});
});
it('returns error when not in production mode', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
await expect(user.post('/debug/boss-rage'))
.to.eventually.be.rejected.and.deep.equal({
code: 404,
error: 'NotFound',
message: 'Not found.',
});
});
context('user is in a party', async () => {
let party;
beforeEach(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Party',
type: 'party',
},
members: 2,
});
party = group;
user = groupLeader;
});
it('increases boss rage to 50', async () => {
await user.post('/debug/boss-rage');
await party.sync();
expect(party.quest.progress.rage).to.eql(50);
});
it('increases boss rage to 100', async () => {
await user.post('/debug/boss-rage');
await user.post('/debug/boss-rage');
await party.sync();
expect(party.quest.progress.rage).to.eql(100);
});
});
});
@@ -34,11 +34,9 @@ describe('POST /debug/jump-time', () => {
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 1 })).time);
const tomorrow = new Date(today.valueOf());
tomorrow.setDate(today.getDate() + 1);
expect(newResultDate.getDate()).to.eql(tomorrow.getDate());
expect(newResultDate.getMonth()).to.eql(tomorrow.getMonth());
expect(newResultDate.getFullYear()).to.eql(tomorrow.getFullYear());
expect(newResultDate.getDate()).to.eql(today.getDate() + 1);
expect(newResultDate.getMonth()).to.eql(today.getMonth());
expect(newResultDate.getFullYear()).to.eql(today.getFullYear());
});
it('jumps back', async () => {
@@ -47,11 +45,9 @@ describe('POST /debug/jump-time', () => {
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: -1 })).time);
const yesterday = new Date(today.valueOf());
yesterday.setDate(today.getDate() - 1);
expect(newResultDate.getDate()).to.eql(yesterday.getDate());
expect(newResultDate.getMonth()).to.eql(yesterday.getMonth());
expect(newResultDate.getFullYear()).to.eql(yesterday.getFullYear());
expect(newResultDate.getDate()).to.eql(today.getDate() - 1);
expect(newResultDate.getMonth()).to.eql(today.getMonth());
expect(newResultDate.getFullYear()).to.eql(today.getFullYear());
});
it('can jump a lot', async () => {
@@ -85,6 +85,22 @@ describe('POST /group/:groupId/join', () => {
await expect(user.get('/user')).to.eventually.have.nested.property('items.quests.basilist', 1);
});
it('notifies inviting user that their invitation was accepted', async () => {
await invitedUser.post(`/groups/${guild._id}/join`);
const inviter = await user.get('/user');
const expectedData = {
headerText: t('invitationAcceptedHeader'),
bodyText: t('invitationAcceptedBody', {
username: invitedUser.auth.local.username,
groupName: guild.name,
}),
};
expect(inviter.notifications[1].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[1].data).to.eql(expectedData);
});
it('awards Joined Guild achievement', async () => {
await invitedUser.post(`/groups/${guild._id}/join`);
@@ -139,6 +155,23 @@ describe('POST /group/:groupId/join', () => {
await expect(invitedUser.get('/user')).to.eventually.have.nested.property('party._id', party._id);
});
it('notifies inviting user that their invitation was accepted', async () => {
await invitedUser.post(`/groups/${party._id}/join`);
const inviter = await user.get('/user');
const expectedData = {
headerText: t('invitationAcceptedHeader'),
bodyText: t('invitationAcceptedBody', {
username: invitedUser.auth.local.username,
groupName: party.name,
}),
};
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[0].data).to.eql(expectedData);
});
it('clears invitation from user when joining party', async () => {
await invitedUser.post(`/groups/${party._id}/join`);
@@ -123,7 +123,7 @@ describe('GET /world-state', () => {
const res = await requester().get('/world-state');
expect(res.npcImageSuffix).to.equal('fall');
expect(res.npcImageSuffix).to.equal('winter');
});
});
});
+4 -14
View File
@@ -47,9 +47,9 @@ describe('shops', () => {
describe('premium hatching potions', () => {
it('contains current scheduled premium hatching potions', async () => {
clock = sinon.useFakeTimers(new Date('2024-04-01T09:00:00.000Z'));
clock = sinon.useFakeTimers(new Date('2024-04-01'));
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.length).to.eql(3);
expect(potions.items.length).to.eql(2);
});
it('does not contain past scheduled premium hatching potions', async () => {
@@ -73,9 +73,9 @@ describe('shops', () => {
});
it('does not contain locked quest premium hatching potions', async () => {
clock = sinon.useFakeTimers(new Date('2024-04-01T09:00:00.000Z'));
clock = sinon.useFakeTimers(new Date('2024-04-01'));
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.length).to.eql(3);
expect(potions.items.length).to.eql(2);
expect(potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').length).to.eql(0);
});
@@ -341,16 +341,6 @@ describe('shops', () => {
const backgrounds = shopCategories.find(cat => cat.identifier === 'backgrounds').items;
expect(backgrounds.length).to.be.greaterThan(0);
});
it('does not add an end date to steampunk gear', () => {
const categories = shopCategories.filter(cat => cat.identifier.startsWith('30'));
categories.forEach(category => {
expect(category.end).to.not.exist;
category.items.forEach(item => {
expect(item.end).to.not.exist;
});
});
});
});
describe('customizationShop', () => {
-11
View File
@@ -233,17 +233,6 @@ describe('shared.ops.purchase', () => {
expect(user.items.hatchingPotions[key]).to.eql(1);
});
it('purchases event hatching potion', async () => {
clock.restore();
clock = sandbox.useFakeTimers(moment('2022-04-10').valueOf());
const type = 'hatchingPotions';
const key = 'Veggie';
await purchase(user, { params: { type, key } });
expect(user.items.hatchingPotions[key]).to.eql(1);
});
it('purchases hatching potion if user completed quest', async () => {
const type = 'hatchingPotions';
const key = 'Bronze';
+2 -2
View File
@@ -47,7 +47,7 @@ describe('content index', () => {
const junePets = content.petInfo;
expect(junePets['Chameleon-Base']).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-20'));
clock = sinon.useFakeTimers(new Date('2024-07-10'));
const julyPets = content.petInfo;
expect(julyPets['Chameleon-Base']).to.exist;
expect(Object.keys(junePets).length, '').to.equal(Object.keys(julyPets).length - 10);
@@ -58,7 +58,7 @@ describe('content index', () => {
const juneMounts = content.mountInfo;
expect(juneMounts['Chameleon-Base']).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-20'));
clock = sinon.useFakeTimers(new Date('2024-07-10'));
const julyMounts = content.mountInfo;
expect(julyMounts['Chameleon-Base']).to.exist;
expect(Object.keys(juneMounts).length, '').to.equal(Object.keys(julyMounts).length - 10);
+11 -72
View File
@@ -18,19 +18,12 @@ function validateMatcher (matcher, checkedDate) {
describe('Content Schedule', () => {
let switchoverTime;
let clock;
beforeEach(() => {
switchoverTime = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0;
clearCachedMatchers();
});
afterEach(() => {
if (clock) {
clock.restore();
}
});
it('assembles scheduled items on january 15th', () => {
const date = new Date('2024-01-15');
const matchers = getAllScheduleMatchingGroups(date);
@@ -112,14 +105,8 @@ describe('Content Schedule', () => {
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-05-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date if its on the release day before switchover', () => {
const date = new Date('2024-05-07T07:00:00.000+00:00');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-05-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date if its on the release day after switchover', () => {
const date = new Date('2024-05-07T09:00:00.000+00:00');
it('sets the end date if its on the release day', () => {
const date = new Date('2024-05-07T07:00:00.000Z');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-06-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
@@ -136,54 +123,12 @@ describe('Content Schedule', () => {
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date for a winter gala', () => {
const date = new Date('2024-12-22');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('uses correct date for first hours of the month', () => {
// if the date is checked before CONTENT_SWITCHOVER_TIME_OFFSET,
// it should be considered the previous month
const date = new Date('2024-05-01T02:00:00.000Z');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.petQuests.items).to.contain('snake');
expect(matchers.petQuests.items).to.not.contain('horse');
expect(matchers.timeTravelers.match('202304'), '202304').to.be.true;
expect(matchers.timeTravelers.match('202404'), '202404').to.be.false;
expect(matchers.timeTravelers.match('202305'), '202305').to.be.false;
});
it('uses correct date after switchover time', () => {
// if the date is checked after CONTENT_SWITCHOVER_TIME_OFFSET,
// it should be considered the current
const date = new Date('2024-05-01T09:00:00.000Z');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.petQuests.items).to.contain('snake');
expect(matchers.petQuests.items).to.not.contain('horse');
expect(matchers.timeTravelers.match('202304'), '202304').to.be.false;
expect(matchers.timeTravelers.match('202305'), '202305').to.be.true;
expect(matchers.timeTravelers.match('202405'), '202405').to.be.false;
});
it('uses UTC timezone', () => {
// if the date is checked after CONTENT_SWITCHOVER_TIME_OFFSET,
// it should be considered the current
clock = sinon.useFakeTimers(new Date('2024-05-01T05:00:00.000-04:00'));
const matchers = getAllScheduleMatchingGroups();
expect(matchers.petQuests.items).to.contain('snake');
expect(matchers.petQuests.items).to.not.contain('horse');
expect(matchers.timeTravelers.match('202304'), '202304').to.be.false;
expect(matchers.timeTravelers.match('202305'), '202305').to.be.true;
expect(matchers.timeTravelers.match('202405'), '202405').to.be.false;
});
it('contains content for repeating events', () => {
const date = new Date('2024-04-15');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.premiumHatchingPotions).to.exist;
expect(matchers.premiumHatchingPotions.items.length).to.equal(5);
expect(matchers.premiumHatchingPotions.items.indexOf('Veggie')).to.not.equal(-1);
expect(matchers.premiumHatchingPotions.items.length).to.equal(4);
expect(matchers.premiumHatchingPotions.items.indexOf('Garden')).to.not.equal(-1);
expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1);
});
@@ -300,33 +245,27 @@ describe('Content Schedule', () => {
it('allows sets matching the month', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202307'), '202307').to.be.true;
expect(matcher.match('202207'), '202207').to.be.true;
expect(matcher.match('202307')).to.be.true;
expect(matcher.match('202207')).to.be.true;
});
it('disallows sets not matching the month', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202306'), '202306').to.be.false;
expect(matcher.match('202402'), '202402').to.be.false;
expect(matcher.match('202306')).to.be.false;
expect(matcher.match('202402')).to.be.false;
});
it('disallows sets from current month', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202407'), '202407').to.be.false;
expect(matcher.match('202407')).to.be.false;
});
it('disallows sets from the future', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202507'), '202507').to.be.false;
});
it('matches sets released in the earlier half of the year', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202401'), '202401').to.be.true;
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('202507')).to.be.false;
});
});
});
+1 -1
View File
@@ -45,7 +45,7 @@ describe('time-travelers store', () => {
describe('on may 1st', () => {
beforeEach(() => {
date = new Date('2024-05-01T09:00:00.000Z');
date = new Date('2024-05-01');
});
it('returns the correct gear', () => {
const items = timeTravelers.timeTravelerStore(user, date);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

+282 -24
View File
@@ -27,15 +27,73 @@
</div>
</div>
</div>
<snackbars />
<router-view v-if="!isUserLoggedIn || isStaticPage" />
<user-main v-else />
<div
id="app"
:class="{
'casting-spell': castingSpell,
}"
>
<!-- <banned-account-modal /> -->
<amazon-payments-modal v-if="!isStaticPage" />
<payments-success-modal />
<sub-cancel-modal-confirm v-if="isUserLoaded" />
<sub-canceled-modal v-if="isUserLoaded" />
<bug-report-modal v-if="isUserLoaded" />
<bug-report-success-modal v-if="isUserLoaded" />
<external-link-modal />
<birthday-modal />
<snackbars />
<router-view v-if="!isUserLoggedIn || isStaticPage" />
<template v-else>
<template v-if="isUserLoaded">
<chat-banner />
<damage-paused-banner />
<gems-promo-banner />
<gift-promo-banner />
<birthday-banner />
<notifications-display />
<app-menu />
<div
class="container-fluid"
:class="{'no-margin': noMargin}"
>
<app-header />
<buyModal
:item="selectedItemToBuy || {}"
:with-pin="true"
:generic-purchase="genericPurchase(selectedItemToBuy)"
@buyPressed="customPurchase($event)"
/>
<selectMembersModal
:item="selectedSpellToBuy || {}"
:group="user.party"
@memberSelected="memberSelected($event)"
/>
<div :class="{sticky: user.preferences.stickyHeader}">
<router-view />
</div>
</div>
<app-footer v-if="!hideFooter" />
<audio
id="sound"
ref="sound"
autoplay="autoplay"
></audio>
</template>
</template>
</div>
</div>
</template>
<style lang='scss' scoped>
@import '~@/assets/scss/colors.scss';
#app {
display: flex;
flex-direction: column;
overflow-x: hidden;
}
#loading-screen-inapp {
#melior {
color: $white;
@@ -105,20 +163,68 @@
<script>
import axios from 'axios';
import { loadProgressBar } from 'axios-progress-bar';
import * as Analytics from '@/libs/analytics';
import birthdayModal from '@/components/news/birthdayModal';
import AppMenu from './components/header/menu';
import AppHeader from './components/header/index';
import ChatBanner from './components/header/banners/chatBanner';
import DamagePausedBanner from './components/header/banners/damagePaused';
import GemsPromoBanner from './components/header/banners/gemsPromo';
import GiftPromoBanner from './components/header/banners/giftPromo';
import BirthdayBanner from './components/header/banners/birthdayBanner';
import AppFooter from './components/appFooter';
import notificationsDisplay from './components/notifications';
import snackbars from './components/snackbars/notifications';
import { mapState } from '@/libs/store';
import userMain from '@/pages/user-main';
import snackbars from '@/components/snackbars/notifications';
import * as Analytics from '@/libs/analytics';
import BuyModal from './components/shops/buyModal.vue';
import SelectMembersModal from '@/components/selectMembersModal.vue';
import notifications from '@/mixins/notifications';
import { setup as setupPayments } from '@/libs/payments';
import amazonPaymentsModal from '@/components/payments/amazonModal';
import paymentsSuccessModal from '@/components/payments/successModal';
import subCancelModalConfirm from '@/components/payments/cancelModalConfirm';
import subCanceledModal from '@/components/payments/canceledModal';
import externalLinkModal from '@/components/externalLinkModal.vue';
import spellsMixin from '@/mixins/spells';
import {
CONSTANTS,
getLocalSetting,
removeLocalSetting,
} from '@/libs/userlocalManager';
const bugReportModal = () => import(/* webpackChunkName: "bug-report-modal" */'@/components/bugReportModal');
const bugReportSuccessModal = () => import(/* webpackChunkName: "bug-report-success-modal" */'@/components/bugReportSuccessModal');
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
export default {
name: 'App',
components: {
AppMenu,
AppHeader,
AppFooter,
birthdayModal,
ChatBanner,
DamagePausedBanner,
GemsPromoBanner,
GiftPromoBanner,
BirthdayBanner,
notificationsDisplay,
snackbars,
userMain,
BuyModal,
SelectMembersModal,
amazonPaymentsModal,
paymentsSuccessModal,
subCancelModalConfirm,
subCanceledModal,
bugReportModal,
bugReportSuccessModal,
externalLinkModal,
},
mixins: [notifications, spellsMixin],
data () {
return {
selectedItemToBuy: null,
@@ -132,25 +238,71 @@ export default {
};
},
computed: {
...mapState(['isUserLoggedIn', 'isUserLoaded', 'notificationsRemoved']),
...mapState(['isUserLoggedIn', 'browserTimezoneUtcOffset', 'isUserLoaded', 'notificationsRemoved']),
...mapState({ user: 'user.data' }),
isStaticPage () {
return this.$route.meta.requiresLogin === false;
},
castingSpell () {
return this.$store.state.spellOptions.castingSpell;
},
noMargin () {
return ['privateMessages'].includes(this.$route.name);
},
hideFooter () {
return ['privateMessages'].includes(this.$route.name);
},
},
created () {
// Setup listener for title
this.$store.watch(state => state.title, title => {
document.title = title;
this.$root.$on('playSound', sound => {
const theme = this.user.preferences.sound;
if (!theme || theme === 'off') {
return;
}
const file = `/static/audio/${theme}/${sound}`;
if (this.audioSuffix === null) {
this.audioSource = document.createElement('source');
if (this.$refs.sound.canPlayType('audio/ogg')) {
this.audioSuffix = '.ogg';
this.audioSource.type = 'audio/ogg';
} else {
this.audioSuffix = '.mp3';
this.audioSource.type = 'audio/mp3';
}
this.audioSource.src = file + this.audioSuffix;
this.$refs.sound.appendChild(this.audioSource);
} else {
this.audioSource.src = file + this.audioSuffix;
}
this.$refs.sound.load();
});
this.$store.watch(state => state.isUserLoaded, () => {
if (this.isUserLoaded) {
this.hideLoadingScreen();
// @TODO: I'm not sure these should be at the app level.
// Can we move these back into shop/inventory or maybe they need a lateral move?
this.$root.$on('buyModal::showItem', item => {
this.selectedItemToBuy = item;
this.$root.$emit('bv::show::modal', 'buy-modal');
});
this.$root.$on('bv::modal::hidden', event => {
if (event.componentId === 'buy-modal') {
this.$root.$emit('buyModal::hidden', this.selectedItemToBuy.key);
}
});
this.$nextTick(() => {
// Load external scripts after the app has been rendered
Analytics.load();
this.$root.$on('selectMembersModal::showItem', item => {
this.selectedSpellToBuy = item;
this.$root.$emit('bv::show::modal', 'select-member-modal');
});
// @TODO split up this file, it's too big
loadProgressBar({
showSpinner: false,
});
axios.interceptors.response.use(response => { // Set up Response interceptors
@@ -262,20 +414,79 @@ export default {
return Promise.reject(error);
});
// Setup listener for title
this.$store.watch(state => state.title, title => {
document.title = title;
});
this.$nextTick(() => {
// Load external scripts after the app has been rendered
Analytics.load();
});
if (this.isUserLoggedIn && !this.isStaticPage) {
// Load the user and the user tasks
Promise.all([
this.$store.dispatch('user:fetch'),
this.$store.dispatch('tasks:fetchUserTasks'),
]).then(() => {
this.$store.state.isUserLoaded = true;
Analytics.setUser();
Analytics.updateUser();
return axios.get(
'/api/v4/i18n/browser-script',
{
language: this.user.preferences.language,
headers: {
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
Expires: '0',
},
},
);
}).then(() => {
const i18nData = window && window['habitica-i18n'];
this.$loadLocale(i18nData);
this.hideLoadingScreen();
// Adjust the timezone offset
const browserTimezoneOffset = -this.browserTimezoneUtcOffset;
if (this.user.preferences.timezoneOffset !== browserTimezoneOffset) {
this.$store.dispatch('user:set', {
'preferences.timezoneOffset': browserTimezoneOffset,
});
}
let appState = getLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
if (appState) {
appState = JSON.parse(appState);
if (appState.paymentCompleted) {
removeLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
this.$root.$emit('habitica:payment-success', appState);
}
}
this.$nextTick(() => {
// Load external scripts after the app has been rendered
setupPayments();
});
}).catch(err => {
console.error('Impossible to fetch user. Clean up localStorage and refresh.', err); // eslint-disable-line no-console
});
} else {
this.hideLoadingScreen();
}
},
beforeDestroy () {
this.$root.$off('playSound');
this.$root.$off('buyModal::showItem');
this.$root.$off('selectMembersModal::showItem');
},
mounted () {
// Remove the index.html loading screen and now show the inapp loading
const loadingScreen = document.getElementById('loading-screen');
if (loadingScreen) document.body.removeChild(loadingScreen);
if (this.isStaticPage || !this.isUserLoggedIn) {
this.hideLoadingScreen();
}
},
methods: {
hideLoadingScreen () {
this.loading = false;
},
checkForBannedUser (error) {
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
const parseSettings = JSON.parse(AUTH_SETTINGS);
@@ -296,10 +507,57 @@ export default {
this.$store.dispatch('auth:logout', { redirectToLogin: true });
return true;
},
itemSelected (item) {
this.selectedItemToBuy = item;
},
genericPurchase (item) {
if (!item) return false;
if (['card', 'debuffPotion'].includes(item.purchaseType)) return false;
return true;
},
customPurchase (item) {
if (item.purchaseType === 'card') {
this.selectedSpellToBuy = item;
// hide the dialog
this.$root.$emit('bv::hide::modal', 'buy-modal');
// remove the dialog from our modal-stack,
// the default hidden event is delayed
this.$root.$emit('bv::modal::hidden', {
target: {
id: 'buy-modal',
},
});
this.$root.$emit('bv::show::modal', 'select-member-modal');
}
if (item.purchaseType === 'debuffPotion') {
this.castStart(item, this.user);
}
},
async memberSelected (member) {
await this.castStart(this.selectedSpellToBuy, member);
this.selectedSpellToBuy = null;
if (this.user.party._id) {
this.$store.dispatch('party:getMembers', { forceLoad: true });
}
this.$root.$emit('bv::hide::modal', 'select-member-modal');
},
hideLoadingScreen () {
this.loading = false;
},
},
};
</script>
<style src="intro.js/minified/introjs.min.css"></style>
<style src="axios-progress-bar/dist/nprogress.css"></style>
<style src="@/assets/scss/index.scss" lang="scss"></style>
<style src="@/assets/scss/sprites.scss" lang="scss"></style>
<style src="smartbanner.js/dist/smartbanner.min.css"></style>
File diff suppressed because it is too large Load Diff
@@ -23,14 +23,16 @@
{{ $t('foundNewItems') }}
</h2>
<div class="d-flex justify-content-center">
<Sprite
<div
class="item-box ml-auto mr-3"
:image-name="eggClass"
/>
<Sprite
:class="eggClass"
>
</div>
<div
class="item-box mr-auto"
:image-name="potionClass"
/>
:class="potionClass"
>
</div>
</div>
<p
v-once
@@ -101,12 +103,8 @@
<script>
import closeIcon from '@/assets/svg/close.svg';
import Sprite from '@/components/ui/sprite.vue';
export default {
components: {
Sprite,
},
data () {
return {
icons: Object.freeze({
@@ -19,10 +19,10 @@
</div>
<div class="inner-content">
<div class="achievement-background d-flex align-items-center">
<Sprite
<div
class="icon"
:image-name="achievementClass"
/>
:class="achievementClass"
></div>
</div>
<h4
class="title"
@@ -99,12 +99,8 @@
import achievements from '@/../../common/script/content/achievements';
import { mapState } from '@/libs/store';
import svgClose from '@/assets/svg/close.svg';
import Sprite from '@/components/ui/sprite.vue';
export default {
components: {
Sprite,
},
props: ['data'],
data () {
return {
@@ -399,10 +399,6 @@
tooltip="+1000 to boss quests. 300 items to collection quests"
@click="addQuestProgress()"
>Quest Progress Up</a>
<a
class="btn btn-secondary"
@click="bossRage()"
>+ Boss Rage 😡</a>
<a
class="btn btn-secondary"
@click="makeAdmin()"
@@ -964,10 +960,6 @@ export default {
// @TODO: Notification.text('Quest progress increased');
// @TODO: User.sync();
},
async bossRage () {
await axios.post('/api/v4/debug/boss-rage');
},
async makeAdmin () {
await axios.post('/api/v4/debug/make-admin');
// @TODO: Notification.text('You are now an admin!
@@ -224,7 +224,7 @@
<script>
import hello from 'hellojs';
import debounce from 'lodash/debounce';
import isEmail from 'validator/es/lib/isEmail';
import isEmail from 'validator/lib/isEmail';
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import { setUpAxios, buildAppleAuthUrl } from '@/libs/auth';
import googleIcon from '@/assets/svg/google.svg';
@@ -607,7 +607,7 @@
import axios from 'axios';
import hello from 'hellojs';
import debounce from 'lodash/debounce';
import isEmail from 'validator/es/lib/isEmail';
import isEmail from 'validator/lib/isEmail';
import DOMPurify from 'dompurify';
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import { buildAppleAuthUrl } from '../../libs/auth';
@@ -6,7 +6,6 @@
<div
v-for="option in items"
:key="option.key"
:id="option.imageName"
class="outer-option-background"
:class="{
premium: Boolean(option.gem),
@@ -15,28 +14,18 @@
hide: option.hide }"
@click="option.click(option)"
>
<b-popover
:target="option.imageName"
triggers="hover focus"
placement="bottom"
:prevent-overflow="false"
>
<strong> {{ option.text }} </strong>
</b-popover>
<div class="option">
<Sprite
v-if="!option.none"
class="sprite"
:prefix="option.isGear ? 'shop' : 'icon'"
:imageName="option.imageName"
:image-name="option.imageName"
/>
<div
class="sprite customize-option"
:class="option.class"
>
<div
v-else
v-if="option.none"
class="redline-outer"
>
<div class="redline"></div>
</div>
</div>
</div>
</div>
</div>
@@ -46,12 +35,8 @@
import gem from '@/assets/svg/gem.svg';
import gold from '@/assets/svg/gold.svg';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
import Sprite from '@/components/ui/sprite.vue';
export default {
components: {
Sprite,
},
mixins: [
avatarEditorUtilities,
],
@@ -90,7 +75,7 @@ export default {
cursor: pointer;
&.premium {
height: 120px;
height: 112px;
width: 96px;
margin-left: 8px;
margin-right: 8px;
@@ -107,9 +92,21 @@ export default {
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
background-color: $white;
.sprite.customize-option.shirt {
margin-left: -3px !important;
// otherwise its overriden by the .outer-option-background:not(.none) { rules
}
.sprite.customize-option.skin {
margin-left: -8px !important;
// otherwise its overriden by the .outer-option-background:not(.none) { rules
}
.option {
border: none;
border-radius: 2px;
padding-left: 6px;
padding-top: 4px;
}
&:hover {
@@ -135,14 +132,14 @@ export default {
}
.redline-outer {
height: 68px;
width: 68px;
height: 60px;
width: 60px;
position: absolute;
bottom: 0;
margin: 0 auto 0 0;
.redline {
width: 68px;
width: 60px;
height: 4px;
display: block;
background: red;
@@ -151,6 +148,7 @@ export default {
top: 0;
margin-top: 30px;
margin-bottom: 20px;
margin-left: -1px;
}
}
}
@@ -166,9 +164,10 @@ export default {
}
.option {
vertical-align: bottom;
height: 76px;
width: 76px;
height: 64px;
width: 64px;
margin: 12px 8px;
border: 4px solid transparent;
border-radius: 10px;
position: relative;
@@ -183,6 +182,44 @@ export default {
.sprite.customize-option {
margin-top: 0;
margin-left: 0;
&.skin {
margin-top: -4px;
margin-left: -4px;
}
&.chair {
margin-left: -1px;
margin-top: -1px;
&.button_chair_black {
// different sprite margin?
margin-top: -3px;
}
&.handleless {
margin-left: -5px;
margin-top: -5px;
}
}
&.color, &.bangs, &.beard, &.flower, &.mustache {
background-position-x: -6px;
background-position-y: -12px;
}
&.hair.base {
background-position-x: -6px;
background-position-y: -4px;
}
&.headAccessory {
margin-top: 0;
margin-left: -4px;
}
&.headband {
margin-top: -6px;
margin-left: -27px;
}
}
}
</style>
@@ -75,7 +75,6 @@
<script>
import appearance from '@/../../common/script/content/appearance';
import upperFirst from 'lodash/upperFirst';
import { subPageMixin } from '../../mixins/subPage';
import { userStateMixin } from '../../mixins/userState';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
@@ -83,6 +82,9 @@ import customizeBanner from './customize-banner';
import customizeOptions from './customize-options';
import subMenu from './sub-menu';
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
export default {
components: {
customizeBanner,
@@ -104,6 +106,17 @@ export default {
headAccessory: ['bearEars', 'cactusEars', 'foxEars', 'lionEars', 'pandaEars', 'pigEars', 'tigerEars', 'wolfEars'],
},
chairKeys: ['none', 'black', 'blue', 'green', 'pink', 'red', 'yellow', 'handleless_black', 'handleless_blue', 'handleless_green', 'handleless_pink', 'handleless_red', 'handleless_yellow'],
specialShirtKeys,
items: [
{
id: 'size',
label: this.$t('size'),
},
{
id: 'shirt',
label: this.$t('shirt'),
},
],
};
},
computed: {
@@ -154,7 +167,6 @@ export default {
];
const noneOption = this.createGearItem(0, 'eyewear', 'base');
noneOption.none = true;
noneOption.text = this.$t('none');
const options = [
noneOption,
];
@@ -166,36 +178,42 @@ export default {
option.active = this.user.preferences.costume
? this.user.items.gear.costume.eyewear === newKey
: this.user.items.gear.equipped.eyewear === newKey;
option.imageName = `eyewear_special_${key}`;
option.isGear = true;
option.class = `eyewear_special_${key}`;
option.click = () => {
const type = this.user.preferences.costume ? 'costume' : 'equipped';
return this.equip(newKey, type);
};
option.text = this.$t(`eyewearSpecial${upperFirst(key)}Text`);
options.push(option);
}
return options;
},
freeShirts () {
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
},
specialShirts () {
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.specialShirtKeys;
const options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
return options;
},
headbands () {
const keys = ['blackHeadband', 'blueHeadband', 'greenHeadband', 'pinkHeadband', 'redHeadband', 'whiteHeadband', 'yellowHeadband'];
const noneOption = this.createGearItem(0, 'headAccessory', 'base');
const noneOption = this.createGearItem(0, 'headAccessory', 'base', 'headband');
noneOption.none = true;
noneOption.text = this.$t('none');
const options = [
noneOption,
];
for (const key of keys) {
const option = this.createGearItem(key, 'headAccessory', 'special');
const option = this.createGearItem(key, 'headAccessory', 'special', 'headband');
const newKey = `headAccessory_special_${key}`;
option.click = () => {
const type = this.user.preferences.costume ? 'costume' : 'equipped';
return this.equip(newKey, type);
};
option.text = this.$t(`headAccessory${upperFirst(key)}Text`);
options.push(option);
}
@@ -209,9 +227,8 @@ export default {
option.none = true;
}
option.active = this.user.preferences.chair === key;
option.imageName = `chair_${key}`;
option.class = `button_chair_${key} chair ${key.includes('handleless_') ? 'handleless' : ''}`;
option.click = () => this.set({ 'preferences.chair': key });
option.text = appearance.chair[key].text();
return option;
});
return options;
@@ -225,11 +242,8 @@ export default {
option.none = true;
}
option.active = this.user.preferences.hair.flower === key;
if (key !== 0) {
option.imageName = `hair_flower_${key}`;
}
option.class = `icon_hair_flower_${key} flower`;
option.click = () => this.set({ 'preferences.hair.flower': key });
option.text = appearance.hair.flower[key].text();
return option;
});
return options;
@@ -257,7 +271,6 @@ export default {
const noneOption = this.createGearItem(0, category, 'base', category);
noneOption.none = true;
noneOption.text = this.$t('none');
const options = [
noneOption,
];
@@ -271,15 +284,10 @@ export default {
option.active = this.user.preferences.costume
? this.user.items.gear.costume[category] === newKey
: this.user.items.gear.equipped[category] === newKey;
option.class = `headAccessory_special_${option.key} ${category}`;
if (category === 'back') {
option.text = this.$t(`back${upperFirst(key)}Text`);
option.imageName = `back_special_${option.key}`;
} else {
option.text = this.$t(`headAccessory${upperFirst(key)}Text`);
option.imageName = `headAccessory_special_${option.key}`;
option.class = `icon_back_special_${option.key} back`;
}
option.isGear = true;
option.click = () => {
const type = this.user.preferences.costume ? 'costume' : 'equipped';
return this.equip(newKey, type);
@@ -295,7 +303,7 @@ export default {
return keys.join(',');
},
createGearItem (key, gearType, subGearType) {
createGearItem (key, gearType, subGearType, additionalClass) {
const newKey = `${gearType}_${subGearType ? `${subGearType}_` : ''}${key}`;
const option = {};
option.key = key;
@@ -303,7 +311,6 @@ export default {
const currentlyEquippedValue = this.user.items.gear[visibleGearType][gearType];
option.active = currentlyEquippedValue === newKey;
option.isGear = true;
if (key === 0) {
// if key is the "none" option check if a property
@@ -311,7 +318,7 @@ export default {
option.active = option.active || !currentlyEquippedValue;
}
option.imageName = `${newKey}`;
option.class = `${newKey} ${additionalClass}`;
option.click = () => {
const type = this.user.preferences.costume ? 'costume' : 'equipped';
const currentlyEquipped = this.user.items.gear[type][gearType];
@@ -167,7 +167,7 @@ label {
<script>
import axios from 'axios';
import isEmail from 'validator/es/lib/isEmail';
import isEmail from 'validator/lib/isEmail';
import closeX from '@/components/ui/closeX';
import { mapState } from '@/libs/store';
import { MODALS } from '@/libs/consts';
+10 -13
View File
@@ -220,10 +220,10 @@
:class="{selected: bg.key === user.preferences.background}"
@click="unlock('background.' + bg.key)"
>
<Sprite
<div
class="background"
:image-name="`icon_background_${bg.key}`"
/>
:class="`icon_background_${bg.key}`"
></div>
<b-popover
:target="bg.key"
triggers="hover focus"
@@ -254,10 +254,10 @@
:class="{selected: bg.key === user.preferences.background}"
@click="unlock('background.' + bg.key)"
>
<Sprite
<div
class="background"
:image-name="`icon_background_${bg.key}`"
/>
:class="`icon_background_${bg.key}`"
></div>
<b-popover
:target="bg.key"
triggers="hover focus"
@@ -286,10 +286,10 @@
:class="{selected: bg.key === user.preferences.background}"
@click="unlock('background.' + bg.key)"
>
<Sprite
<div
class="background"
:image-name="`icon_background_${bg.key}`"
/>
:class="`icon_background_${bg.key}`"
></div>
<b-popover
:target="bg.key"
triggers="hover focus"
@@ -818,10 +818,9 @@
.background {
border-radius: 4px;
object-position: -4px -4px;
object-fit: none;
width: 60px;
height: 60px;
background-position: -4px -4px;
}
.deselect {
@@ -1014,7 +1013,6 @@ import arrowRight from '@/assets/svg/arrow_right.svg';
import arrowLeft from '@/assets/svg/arrow_left.svg';
import svgClose from '@/assets/svg/close.svg';
import { avatarEditorUtilities } from '../mixins/avatarEditUtilities';
import Sprite from './ui/sprite';
export default {
components: {
@@ -1026,7 +1024,6 @@ export default {
hairSettings,
skinSettings,
usernameForm,
Sprite,
},
mixins: [guide, notifications, avatarEditorUtilities],
data () {
@@ -122,8 +122,8 @@
<script>
import clone from 'lodash/clone';
import debounce from 'lodash/debounce';
import isEmail from 'validator/es/lib/isEmail';
import isUUID from 'validator/es/lib/isUUID';
import isEmail from 'validator/lib/isEmail';
import isUUID from 'validator/lib/isUUID';
import { mapState } from '@/libs/store';
import notifications from '@/mixins/notifications';
import positiveIcon from '@/assets/svg/positive.svg';
+189 -194
View File
@@ -16,10 +16,10 @@
class="brand"
aria-label="Habitica"
>
<router-link to="/">
<router-link to="/">
<div
class="logo svg-icon svg color gryphon pl-2 mr-3"
v-html="icons.melior"
class="logo svg-icon svg color gryphon"
v-html="icons.melior"
></div>
<div class="svg-icon"></div>
</router-link>
@@ -349,15 +349,15 @@
>
<div
v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')"
class="top-menu-icon svg-icon mr-1"
class="top-menu-icon svg-icon"
v-html="icons.hourglasses"
></div>
<span>{{ userHourglasses }}</span>
</div>
<div class="item-with-icon gem">
<div class="item-with-icon">
<a
v-b-tooltip.hover.bottom="$t('gems')"
class="top-menu-icon svg-icon gem mr-2"
class="top-menu-icon svg-icon gem"
:aria-label="$t('gems')"
href="#buy-gems"
@click.prevent="showBuyGemsModal()"
@@ -368,7 +368,7 @@
<div class="item-with-icon gold">
<div
v-b-tooltip.hover.bottom="$t('gold')"
class="top-menu-icon svg-icon mr-2"
class="top-menu-icon svg-icon"
:aria-label="$t('gold')"
v-html="icons.gold"
></div>
@@ -409,180 +409,6 @@ body.modal-open #habitica-menu {
@import '~@/assets/scss/utils.scss';
@import '~@/assets/scss/variables.scss';
.menu-toggle {
border: none;
}
#menu_collapse {
display: flex;
justify-content: space-between;
}
.topbar {
z-index: 1080;
background: $purple-100 url(~@/assets/svg/for-css/bits.svg) right top no-repeat;
min-height: 56px;
box-shadow: 0 1px 2px 0 rgba($black, 0.24);
a {
color: white !important;
}
}
.logo {
color: $white;
height: 32px;
object-fit: contain;
width: 32px;
}
.quick-menu {
display: flex;
margin-left: auto;
}
.currency-tray {
display: flex;
}
.topbar-item {
font-size: 16px;
color: $white !important;
font-weight: bold;
transition: none;
.topbar-dropdown {
overflow: hidden;
max-height: 0;
.topbar-dropdown-item {
line-height: 1.5;
font-size: 16px;
}
}
>a {
padding: .8em 1em !important;
}
&.down {
color: $white !important;
background: $purple-200;
.topbar-dropdown {
margin-top: 0; // Remove gap between navbar and drop-down.
background: $purple-200;
border-radius: 0px;
border: none;
box-shadow: none;
padding: 0px;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
.topbar-dropdown-item {
font-size: 16px;
box-shadow: none;
color: $white;
border: none;
line-height: 1.5;
display: list-item;
&.active {
background: $purple-300;
}
&:hover {
background: $purple-300;
text-decoration: none;
&:last-child {
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
}
}
}
}
}
}
.dropdown + .dropdown {
margin-left: 0px;
}
.item-with-icon {
color: $white;
font-size: 16px;
font-weight: normal;
white-space: nowrap;
span {
font-weight: bold;
}
&.gem {
margin-left: 12px;
}
&.gold {
margin-left: 12px;
margin-right: 36px;
}
&:focus ::v-deep .top-menu-icon.svg-icon,
&:hover ::v-deep .top-menu-icon.svg-icon {
color: $white;
}
& ::v-deep .top-menu-icon.svg-icon {
color: $header-color;
vertical-align: bottom;
display: inline-block;
width: 24px;
height: 24px;
margin-right: 12px;
margin-left: 12px;
}
}
a.item-with-icon:focus {
outline: none;
}
@keyframes rotateGemColors {
/* Gems are green by default, so we rotate through ROYGBIV starting with green. */
20% {
fill: #46A7D9; /* Blue */
}
40% {
fill: #925CF3; /* Purple */
}
60% {
fill: #DE3F3F; /* Red */
}
80% {
fill: #FA8537; /* Orange */
}
100% {
fill: #FFB445; /* Yellow */
}
}
.gem:hover {
cursor: pointer;
& ::v-deep path:nth-child(1) {
animation: rotateGemColors 3s linear infinite alternate;
}
}
.message-count.top-count {
background-color: $red-50;
position: absolute;
right: 0;
top: -0.5em;
padding: .2em;
}
@media only screen and (max-width: 1200px) {
.chevron {
display: none
@@ -590,13 +416,12 @@ body.modal-open #habitica-menu {
.gryphon {
background-size: cover;
color: $white;
height: 32px;
color: $white;
margin: 0 auto;
top: -10px;
padding-left: 8px;
position: relative;
width: 32px;
top: -10px;
position: relative;
}
.logo {
@@ -720,23 +545,193 @@ body.modal-open #habitica-menu {
.desktop-only {
display: none !important;
}
}
.navbar-toggler {
padding-left: 8px;
padding-right: 8px;
.menu-toggle {
border: none;
}
#menu_collapse {
display: flex;
justify-content: space-between;
}
.topbar {
z-index: 1080;
background: $purple-100 url(~@/assets/svg/for-css/bits.svg) right top no-repeat;
min-height: 56px;
box-shadow: 0 1px 2px 0 rgba($black, 0.24);
a {
color: white !important;
}
}
.logo {
color: $white;
height: 32px;
object-fit: contain;
width: 32px;
}
.quick-menu {
display: flex;
margin-left: auto;
}
.currency-tray {
display: flex;
}
.topbar-item {
font-size: 16px;
color: $white !important;
font-weight: bold;
transition: none;
.topbar-dropdown {
overflow: hidden;
max-height: 0;
.topbar-dropdown-item {
line-height: 1.5;
font-size: 16px;
}
}
.item-with-icon {
margin-left: 0px;
margin-right: 16px;
>a {
padding: .8em 1em !important;
}
& ::v-deep .top-menu-icon.svg-icon {
margin-right: 0px;
margin-left: 0px;
&.down {
color: $white !important;
background: $purple-200;
.topbar-dropdown {
margin-top: 0; // Remove gap between navbar and drop-down.
background: $purple-200;
border-radius: 0px;
border: none;
box-shadow: none;
padding: 0px;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
.topbar-dropdown-item {
font-size: 16px;
box-shadow: none;
color: $white;
border: none;
line-height: 1.5;
display: list-item;
&.active {
background: $purple-300;
}
&:hover {
background: $purple-300;
text-decoration: none;
&:last-child {
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
}
}
}
}
}
}
.dropdown + .dropdown {
margin-left: 0px;
}
.item-with-icon {
color: $white;
font-size: 16px;
font-weight: normal;
white-space: nowrap;
span {
font-weight: bold;
}
&.gold {
margin-right: 24px;
}
&:focus ::v-deep .top-menu-icon.svg-icon,
&:hover ::v-deep .top-menu-icon.svg-icon {
color: $white;
}
& ::v-deep .top-menu-icon.svg-icon {
color: $header-color;
vertical-align: bottom;
display: inline-block;
width: 24px;
height: 24px;
margin-right: 12px;
margin-left: 12px;
}
}
a.item-with-icon:focus {
outline: none;
}
.menu-icon {
margin-left: 24px;
}
@keyframes rotateGemColors {
/* Gems are green by default, so we rotate through ROYGBIV starting with green. */
20% {
fill: #46A7D9; /* Blue */
}
40% {
fill: #925CF3; /* Purple */
}
60% {
fill: #DE3F3F; /* Red */
}
80% {
fill: #FA8537; /* Orange */
}
100% {
fill: #FFB445; /* Yellow */
}
}
.gem:hover {
cursor: pointer;
& ::v-deep path:nth-child(1) {
animation: rotateGemColors 3s linear infinite alternate;
}
}
.message-count {
background-color: $blue-50;
border-radius: 50%;
height: 20px;
width: 20px;
float: right;
color: $white;
text-align: center;
font-weight: bold;
font-size: 12px;
}
.message-count.top-count {
background-color: $red-50;
position: absolute;
right: 0;
top: -0.5em;
padding: .2em;
}
</style>
<script>
@@ -12,13 +12,13 @@
.message-count {
background-color: $red-50;
border-radius: 50%;
color: $white;
font-size: 12px;
font-weight: bold;
height: 20px;
left: 24px;
text-align: center;
width: 20px;
float: right;
color: $white;
text-align: center;
font-weight: bold;
font-size: 12px;
svg {
width: 12px;
@@ -36,11 +36,4 @@
.message-count.top-count-gray {
background-color: $gray-200;
}
@media only screen and (max-width: 992px) {
.message-count {
left: 12px;
}
}
</style>
@@ -14,7 +14,7 @@
:top="true"
/>
<div
class="top-menu-icon svg-icon mr-2"
class="top-menu-icon svg-icon user"
v-html="icons.user"
></div>
</div>
@@ -105,11 +105,6 @@
<style lang='scss' scoped>
@import '~@/assets/scss/colors.scss';
@media only screen and (max-width: 992px) {
.item-with-icon.item-user {
margin-right: 0px;
}
}
.user-dropdown {
width: 14.75em;
@@ -21,10 +21,10 @@
<slot
name="itemBadge"
:item="item"
></slot><Sprite
></slot><span
class="item-content"
:image-name="itemContentClass"
/>
:class="itemContentClass"
></span>
</div><span
v-if="label"
class="item-label"
@@ -46,12 +46,8 @@
<script>
import { v4 as uuid } from 'uuid';
import Sprite from '@/components/ui/sprite';
export default {
components: {
Sprite,
},
props: {
item: {
type: Object,
@@ -1,113 +0,0 @@
<template>
<div
ref="root"
v-if="draggedItem"
class="draggedItemInfo mouse"
v-mousePosition="30"
@mouseMoved="mouseMoved($event)">
<Sprite
class="dragging-icon"
:image-name="imageName()"
/>
<div class="popover">
<div
class="popover-content"
>
{{ $t(popoverTextKey, { [translationKey]: itemText() }) }}
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.draggedItemInfo {
position: absolute;
left: -500px;
z-index: 1080;
&.mouse {
position: fixed;
pointer-events: none
}
.dragging-icon {
width: 68px;
margin: 0 auto 8px;
display: block;
transform: scale(1.5);
}
.popover {
position: static;
width: 180px;
}
.popover-content {
color: white;
margin: 15px;
text-align: center;
}
}
</style>
<script>
import Sprite from '@/components/ui/sprite';
import MouseMoveDirective from '@/directives/mouseposition.directive';
export default {
name: 'ItemPopover',
components: {
Sprite,
},
directives: {
mousePosition: MouseMoveDirective,
},
props: {
draggedItem: {
type: Object,
default: null,
},
popoverTextKey: {
type: String,
default: '',
},
translationKey: {
type: String,
default: '',
},
},
methods: {
imageName () {
if (this.draggedItem) {
if (this.draggedItem.class) {
return this.draggedItem.class;
}
if (this.draggedItem.target) {
return `Pet_Food_${this.draggedItem.key}`;
}
}
return '';
},
mouseMoved ($event) {
if (this.$refs.root) {
this.$refs.root.style.left = `${$event.x - 60}px`;
this.$refs.root.style.top = `${$event.y + 10}px`;
}
},
itemText () {
if (this.draggedItem) {
if (this.draggedItem.text) {
if (typeof this.draggedItem.text === 'function') {
return this.draggedItem.text();
}
return this.draggedItem.text;
}
return this.draggedItem.class;
}
return '';
},
},
};
</script>
@@ -1,6 +1,8 @@
<template>
<div
v-mousePosition="30"
class="row"
@mouseMoved="mouseMoved($event)"
>
<div class="standard-sidebar d-none d-sm-block">
<filter-sidebar>
@@ -97,7 +99,7 @@
{{ context.item.text }}
</h4>
<div
v-if="!currentDraggingPotion"
v-if="currentDraggingPotion == null"
class="popover-content-text"
>
{{ context.item.notes }}
@@ -146,7 +148,7 @@
<h4 class="popover-content-title">
{{ context.item.text }}
</h4>
<div class="popover-content-text" v-if="!currentDraggingEgg">
<div class="popover-content-text">
{{ context.item.notes }}
</div>
</template>
@@ -222,24 +224,120 @@
</div>
</div>
<hatchedPetDialog />
<ItemPopover
:dragged-item="currentDraggingEgg"
popoverTextKey="clickOnPotionToHatch"
translationKey="eggName" />
<ItemPopover
:dragged-item="currentDraggingPotion"
popoverTextKey="clickOnEggToHatch"
translationKey="potionName" />
<div
ref="draggingEggInfo"
class="eggInfo"
>
<div v-if="currentDraggingEgg != null">
<div
class="potion-icon"
:class="`Pet_Egg_${currentDraggingEgg.key}`"
></div>
<div class="popover">
<div class="popover-content">
{{ $t('dragThisEgg', {eggName: currentDraggingEgg.text }) }}
</div>
</div>
</div>
</div>
<div
v-if="eggClickMode"
ref="clickEggInfo"
class="eggInfo mouse"
>
<div v-if="currentDraggingEgg != null">
<div
class="potion-icon"
:class="`Pet_Egg_${currentDraggingEgg.key}`"
></div>
<div class="popover">
<div
class="popover-content"
>
{{ $t('clickOnPotionToHatch', {eggName: currentDraggingEgg.text }) }}
</div>
</div>
</div>
</div>
<div
ref="draggingPotionInfo"
class="hatchingPotionInfo"
>
<div v-if="currentDraggingPotion != null">
<div
class="potion-icon"
:class="`Pet_HatchingPotion_${currentDraggingPotion.key}`"
></div>
<div class="popover">
<div
class="popover-content"
>
{{ $t('dragThisPotion', {potionName: currentDraggingPotion.text }) }}
</div>
</div>
</div>
</div>
<div
v-if="potionClickMode"
ref="clickPotionInfo"
class="hatchingPotionInfo mouse"
>
<div v-if="currentDraggingPotion != null">
<div
class="potion-icon"
:class="`Pet_HatchingPotion_${currentDraggingPotion.key}`"
></div>
<div class="popover">
<div
class="popover-content"
>
{{ $t('clickOnEggToHatch', {potionName: currentDraggingPotion.text }) }}
</div>
</div>
</div>
</div>
<questDetailModal :group="user.party" />
<cards-modal :card-options="cardOptions" />
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.eggInfo, .hatchingPotionInfo {
position: absolute;
left: -500px;
z-index: 1080;
&.mouse {
position: fixed;
pointer-events: none
}
.potion-icon {
margin: 0 auto 8px;
transform: scale(1.5);
}
.popover {
position: inherit;
width: 180px;
}
.popover-content {
color: white;
margin: 15px;
text-align: center;
}
}
</style>
<script>
import each from 'lodash/each';
import throttle from 'lodash/throttle';
import moment from 'moment';
import ItemPopover from '@/components/inventory/itemPopover';
import Item from '@/components/inventory/item';
import ItemRows from '@/components/ui/itemRows';
import CountBadge from '@/components/ui/countBadge';
@@ -256,6 +354,7 @@ import { createAnimal } from '@/libs/createAnimal';
import notifications from '@/mixins/notifications';
import DragDropDirective from '@/directives/dragdrop.directive';
import MouseMoveDirective from '@/directives/mouseposition.directive';
import FilterGroup from '@/components/ui/filterGroup';
import Checkbox from '@/components/ui/checkbox';
import SelectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
@@ -276,6 +375,8 @@ const groups = [
allowedItems,
}));
let lastMouseMoveEvent = {};
export default {
name: 'Items',
components: {
@@ -290,10 +391,10 @@ export default {
cardsModal,
QuestInfo,
FilterSidebar,
ItemPopover,
},
directives: {
drag: DragDropDirective,
mousePosition: MouseMoveDirective,
},
mixins: [notifications],
data () {
@@ -304,7 +405,9 @@ export default {
sortBy: 'quantity', // or 'AZ'
currentDraggingEgg: null,
eggClickMode: false,
currentDraggingPotion: null,
potionClickMode: false,
cardOptions: {
cardType: '',
messageOptions: 0,
@@ -464,13 +567,22 @@ export default {
}
this.currentDraggingPotion = null;
this.potionClickMode = false;
return;
}
if (this.currentDraggingEgg === null || this.currentDraggingEgg !== egg) {
this.currentDraggingEgg = egg;
this.eggClickMode = true;
// Wait for the div.eggInfo.mouse node to be added to the DOM before
// changing its position.
this.$nextTick(() => {
this.mouseMoved(lastMouseMoveEvent);
});
} else {
this.currentDraggingEgg = null;
this.eggClickMode = false;
}
},
onPotionClicked ($event, potion) {
@@ -480,12 +592,21 @@ export default {
}
this.currentDraggingEgg = null;
this.eggClickMode = false;
return;
}
if (this.currentDraggingPotion === null || this.currentDraggingPotion !== potion) {
this.currentDraggingPotion = potion;
this.potionClickMode = true;
// Wait for the div.hatchingPotionInfo.mouse node to be added to the
// DOM before changing its position.
this.$nextTick(() => {
this.mouseMoved(lastMouseMoveEvent);
});
} else {
this.currentDraggingPotion = null;
this.potionClickMode = false;
}
},
@@ -519,6 +640,23 @@ export default {
});
}
},
mouseMoved ($event) {
// Keep track of the last mouse position even in click mode so that we
// know where to position the dragged potion/egg info on item click.
lastMouseMoveEvent = $event;
// Update the potion/egg popover if we are already dragging it.
if (this.potionClickMode) {
// dragging potioninfo is 180px wide (90 would be centered)
this.$refs.clickPotionInfo.style.left = `${$event.x - 60}px`;
this.$refs.clickPotionInfo.style.top = `${$event.y + 10}px`;
} else if (this.eggClickMode) {
// dragging eggInfo is 180px wide (90 would be centered)
this.$refs.clickEggInfo.style.left = `${$event.x - 60}px`;
this.$refs.clickEggInfo.style.top = `${$event.y + 10}px`;
}
},
},
};
</script>
@@ -13,13 +13,13 @@
:show="true"
:count="itemCount"
/>
<Sprite
<span
v-drag.food="item.key"
class="item-content"
:image-name="`Pet_Food_${item.key}`"
:class="`Pet_Food_${item.key}`"
@itemDragEnd="dragend($event)"
@itemDragStart="dragstart($event)"
/>
></span>
</div>
</div>
<b-popover
@@ -41,14 +41,12 @@
<script>
import { v4 as uuid } from 'uuid';
import DragDropDirective from '@/directives/dragdrop.directive';
import Sprite from '@/components/ui/sprite';
import CountBadge from '@/components/ui/countBadge';
export default {
components: {
CountBadge,
Sprite,
},
directives: {
drag: DragDropDirective,
@@ -20,7 +20,7 @@
</div>
<div class="inner-content">
<div class="pet-background d-flex align-items-center">
<Sprite :image-name="pet.imageName" />
<div :class="pet.class"></div>
</div>
<h4 class="title">
{{ pet.name }}
@@ -76,11 +76,10 @@
height: 112px;
border-radius: 4px;
background-color: $gray-700;
}
img {
transform: scale(1.5);
margin: auto;
}
.Pet {
margin: auto;
}
.dialog-header {
@@ -104,12 +103,8 @@
<script>
import markdownDirective from '@/directives/markdown';
import svgClose from '@/assets/svg/close.svg';
import Sprite from '@/components/ui/sprite';
export default {
components: {
Sprite,
},
directives: {
markdown: markdownDirective,
},
@@ -6,10 +6,10 @@
>
<div class="potionEggGroup">
<div class="potionEggBackground">
<Sprite :image-name="`Pet_HatchingPotion_${hatchablePet.potionKey}`" />
<div :class="`Pet_HatchingPotion_${hatchablePet.potionKey}`"></div>
</div>
<div class="potionEggBackground">
<Sprite :image-name="`Pet_Egg_${hatchablePet.eggKey}`" />
<div :class="`Pet_Egg_${hatchablePet.eggKey}`"></div>
</div>
</div>
<h4 class="title">
@@ -105,7 +105,7 @@
margin-right: 24px;
}
img {
div {
margin: 0 auto;
}
}
@@ -116,12 +116,8 @@
import svgClose from '@/assets/svg/close.svg';
import petMixin from '@/mixins/petMixin';
import Sprite from '@/components/ui/sprite';
export default {
components: {
Sprite,
},
mixins: [petMixin],
props: ['hatchablePet'],
data () {
@@ -1,6 +1,8 @@
<template>
<div
v-mousePosition="30"
class="row stable"
@mouseMoved="mouseMoved($event)"
>
<div class="standard-sidebar d-none d-sm-block">
<filter-sidebar>
@@ -263,10 +265,43 @@
</inventoryDrawer>
</div>
<hatchedPetDialog :hide-text="true" />
<ItemPopover
:dragged-item="currentDraggingFood"
popoverTextKey="clickOnPetToFeed"
translationKey="foodName" />
<div
ref="dragginFoodInfo"
class="foodInfo"
>
<div v-if="currentDraggingFood != null">
<div
class="food-icon"
:class="`Pet_Food_${currentDraggingFood.key}`"
></div>
<div class="popover">
<div
class="popover-content"
>
{{ $t('dragThisFood', {foodName: currentDraggingFood.text() }) }}
</div>
</div>
</div>
</div>
<div
v-if="foodClickMode"
ref="clickFoodInfo"
class="foodInfo mouse"
>
<div v-if="currentDraggingFood != null">
<div
class="food-icon"
:class="`Pet_Food_${currentDraggingFood.key}`"
></div>
<div class="popover">
<div
class="popover-content"
>
{{ $t('clickOnPetToFeed', {foodName: currentDraggingFood.text() }) }}
</div>
</div>
</div>
</div>
<mount-raised-modal />
<welcome-modal />
<hatching-modal :hatchable-pet.sync="hatchablePet" />
@@ -329,6 +364,34 @@
margin-bottom: 0;
}
.foodInfo {
position: absolute;
left: -500px;
z-index: 1080;
&.mouse {
position: fixed;
pointer-events: none
}
.food-icon {
margin: 0 auto 8px;
transform: scale(1.5);
}
.popover {
position: inherit;
width: 180px;
}
.popover-content {
color: white;
margin: 15px;
text-align: center;
}
}
.hatchablePopover {
width: 180px;
@@ -365,7 +428,6 @@ import _throttle from 'lodash/throttle';
import groupBy from 'lodash/groupBy';
import { mapState } from '@/libs/store';
import ItemPopover from '@/components/inventory/itemPopover';
import PetItem from './petItem';
import MountItem from './mountItem.vue';
import FoodItem from './foodItem';
@@ -378,6 +440,7 @@ import InventoryDrawer from '@/components/shared/inventoryDrawer';
import ResizeDirective from '@/directives/resize.directive';
import DragDropDirective from '@/directives/dragdrop.directive';
import MouseMoveDirective from '@/directives/mouseposition.directive';
import { createAnimal } from '@/libs/createAnimal';
@@ -419,11 +482,11 @@ export default {
WelcomeModal,
HatchingModal,
InventoryDrawer,
ItemPopover,
},
directives: {
resize: ResizeDirective,
drag: DragDropDirective,
mousePosition: MouseMoveDirective,
},
mixins: [notifications, openedItemRowsMixin, petMixin, seasonalNPC],
data () {
@@ -13,11 +13,10 @@
name="itemBadge"
:item="item"
></slot>
<Sprite
<span
class="item-content"
:class="itemClass()"
:image-name="imageName()"
/>
></span>
</div>
</div>
<b-popover
@@ -38,12 +37,8 @@
import { v4 as uuid } from 'uuid';
import { mapState } from '@/libs/store';
import { isOwned } from '../../../libs/createAnimal';
import Sprite from '@/components/ui/sprite';
export default {
components: {
Sprite,
},
props: {
item: {
type: Object,
@@ -75,10 +70,7 @@ export default {
return isOwned('mount', this.item, this.userItems);
},
itemClass () {
return this.isOwned() ? '' : 'GreyedOut';
},
imageName () {
return this.isOwned() ? `stable_Mount_Icon_${this.item.key}` : 'PixelPaw';
return this.isOwned() ? `Mount_Icon_${this.item.key}` : 'PixelPaw GreyedOut';
},
},
};
@@ -12,10 +12,10 @@
</div>
<div class="inner-content">
<div class="pet-background">
<Sprite
<div
class="mount"
:image-name="`Mount_Icon_${mount.key}`"
/>
:class="`Mount_Icon_${mount.key}`"
></div>
</div>
<h4 class="title">
{{ mount.text() }}
@@ -82,12 +82,8 @@
<script>
import stable from '@/../../common/script/content/stable';
import markdownDirective from '@/directives/markdown';
import Sprite from '@/components/ui/sprite';
export default {
components: {
Sprite,
},
directives: {
markdown: markdownDirective,
},
@@ -13,23 +13,19 @@
name="itemBadge"
:item="item"
></slot><span
v-if="isHatchable() && !item.isSpecial()"
v-if="mountOwned() && isHatchable() && !item.isSpecial()"
class="item-content hatchAgain"
><Sprite
><span
class="egg"
:image-name="eggClass"
/><Sprite
:class="eggClass"
></span><span
class="potion"
:image-name="potionClass"
/>
</span>
<Sprite
v-else
:class="potionClass"
></span></span><span
v-else
class="item-content"
:class="itemClass()"
:image-name="imageName()"
/>
<span
:class="getPetItemClass()"
></span><span
v-if="isAllowedToFeed() && progress() > 0"
class="pet-progress-background"
><div
@@ -56,9 +52,9 @@
v-html="$t('haveHatchablePet', { potion: item.potionName, egg: item.eggName })"
></div><div class="potionEggGroup">
<div class="potionEggBackground">
<Sprite :image-name="potionClass" />
<div :class="potionClass"></div>
</div><div class="potionEggBackground">
<Sprite :image-name="eggClass" />
<div :class="eggClass"></div>
</div>
</div>
</div><div v-else>
@@ -122,12 +118,8 @@ import foolPet from '@/mixins/foolPet';
import {
isAllowedToFeed, isHatchable, isOwned, isSpecial,
} from '../../../libs/createAnimal';
import Sprite from '@/components/ui/sprite';
export default {
components: {
Sprite,
},
mixins: [foolPet],
props: {
item: {
@@ -176,28 +168,22 @@ export default {
isAllowedToFeed () {
return isAllowedToFeed(this.item, this.userItems);
},
itemClass () {
if (this.isOwned() || this.isHatchable()) {
return '';
}
return 'GreyedOut';
},
imageName () {
getPetItemClass () {
if (this.isOwned() && some(
this.currentEventList,
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'Fungi',
)) {
if (this.isSpecial()) return `stable_${this.foolPet(this.item.key)}`;
if (this.isSpecial()) return `Pet ${this.foolPet(this.item.key)}`;
const petString = `${this.item.eggKey}-${this.item.key}`;
return `stable_${this.foolPet(petString)}`;
return `Pet ${this.foolPet(petString)}`;
}
if (this.isOwned() || (this.mountOwned() && this.isHatchable())) {
return `stable_Pet-${this.item.key}`;
return `Pet Pet-${this.item.key} ${this.item.eggKey}`;
}
if (!this.isOwned() && this.isSpecial()) {
return 'PixelPaw';
return 'GreyedOut PixelPaw';
}
if (this.isHatchable()) {
@@ -205,11 +191,11 @@ export default {
}
if (this.mountOwned()) {
return `stable_Pet-${this.item.key}`;
return `GreyedOut Pet Pet-${this.item.key} ${this.item.eggKey}`;
}
// Can't hatch
return 'PixelPaw';
return 'GreyedOut PixelPaw';
},
progress () {
return this.userItems.pets[this.item.key];
@@ -56,11 +56,11 @@
class="list-group-item"
ng-init="inv.gear[item.key] = user.items.gear.owned[item.key]"
>
<Sprite
<div
class="pull-left"
:imageName="'shop_' + item.key"
:class="'shop_' + item.key"
style="margin-right: 10px"
/>
></div>
{{ item.text() }}
<div class="clearfix">
<label class="radio-inline">
@@ -330,9 +330,9 @@
class="list-group-item"
ng-init="inv.mounts[mount] = user.items.mounts[mount]"
>
<Sprite
<div
class="pull-left"
:imageName="mount.key"
:class="'Mount_Icon_' + mount"
style="margin-right: 10px"
></div>
{{ mount }}
@@ -363,9 +363,9 @@
class="list-group-item"
ng-init="inv.mounts[mount] = user.items.mounts[mount]"
>
<Sprite
<div
class="pull-left"
:imageName="mount.key"
:class="'Mount_Icon_' + mount"
style="margin-right: 10px"
></div>
{{ mount }}
@@ -396,9 +396,9 @@
class="list-group-item"
ng-init="inv.mounts[mount] = user.items.mounts[mount]"
>
<Sprite
<div
class="pull-left"
:imageName="mount.key"
:class="'Mount_Icon_' + mount"
style="margin-right: 10px"
></div>
{{ mount }}
@@ -429,9 +429,9 @@
class="list-group-item"
ng-init="inv.mounts[mount] = user.items.mounts[mount]"
>
<Sprite
<div
class="pull-left"
:imageName="mount.key"
:class="'Mount_Icon_' + mount"
style="margin-right: 10px"
></div>
{{ mount }}
@@ -503,11 +503,11 @@
ng-init="inv.hatchingPotions[item.key] = user.items.hatchingPotions[item.key]"
>
<div class="form-inline clearfix">
<Sprite
<div
class="pull-left"
:class="'Pet_HatchingPotion_' + item.key"
style="margin-right: 10px"
/>
></div>
<p>{{ item.text() }}</p>
<input
class="form-control"
@@ -565,11 +565,11 @@
ng-init="inv.eggs[item.key] = user.items.eggs[item.key]"
>
<div class="form-inline clearfix">
<Sprite
<div
class="pull-left"
:image-name="'Pet_Egg_' + item.key"
:class="'Pet_Egg_' + item.key"
style="margin-right: 10px"
/>
></div>
<p>{{ item.text() }}</p>
<input
class="form-control"
@@ -627,11 +627,11 @@
ng-init="inv.food[item.key] = user.items.food[item.key]"
>
<div class="form-inline clearfix">
<Sprite
<div
class="pull-left"
:class="'Pet_Food_' + item.key"
style="margin-right: 10px"
/>
></div>
<p>{{ item.text() }}</p>
<input
class="form-control"
@@ -690,11 +690,11 @@
ng-if="item.category !== 'world'"
>
<div class="form-inline clearfix">
<Sprite
<div
class="pull-left"
:class="'inventory_quest_scroll_' + item.key"
style="margin-right: 10px"
/>
></div>
<p>{{ item.text() }}</p>
<input
class="form-control"
@@ -730,13 +730,9 @@
import axios from 'axios';
import Content from '@/../../common/script/content';
import Sprite from '@/components/ui/sprite.vue';
import { mapState } from '@/libs/store';
export default {
components: {
Sprite,
},
data () {
const showInv = {};
const inv = {
@@ -295,7 +295,7 @@ h2 {
// import { nextTick } from 'vue'; // may not need this? I don't know!
import debounce from 'lodash/debounce';
import find from 'lodash/find';
import isUUID from 'validator/es/lib/isUUID';
import isUUID from 'validator/lib/isUUID';
import moment from 'moment';
import { mapState } from '@/libs/store';
import closeIcon from '@/assets/svg/close.svg';
@@ -46,10 +46,10 @@
</div>
<div class="row">
<div class="col-2">
<Sprite
:image-name="currentMysterySet"
<div
:class="currentMysterySet"
class="mt-n1"
/>
></div>
</div>
<div class="col-10">
<h3> {{ $t('monthlyMysteryItems') }} </h3>
@@ -628,7 +628,6 @@ import paymentsMixin from '../../mixins/payments';
import notificationsMixin from '../../mixins/notifications';
import subscriptionOptions from './subscriptionOptions.vue';
import Sprite from '@/components/ui/sprite';
import amazonPayLogo from '@/assets/svg/amazonpay.svg';
import applePayLogo from '@/assets/svg/apple-pay-logo.svg';
@@ -649,7 +648,6 @@ import subscriberHourglasses from '@/assets/svg/subscriber-hourglasses.svg';
export default {
components: {
subscriptionOptions,
Sprite,
},
mixins: [paymentsMixin, notificationsMixin],
data () {
@@ -715,12 +715,6 @@ export default {
if (this.item.notes instanceof Function) {
return this.item.notes();
}
if (this.item.items) {
if (this.item.items[0].notes instanceof Function) {
return this.item.items[0].notes();
}
return this.item.items[0].notes;
}
return this.item.notes;
},
gemsLeft () {
@@ -109,7 +109,6 @@
</style>
<script>
import find from 'lodash/find';
import shops from '@/../../common/script/libs/shops';
import throttle from 'lodash/throttle';
import { mapState } from '@/libs/store';
@@ -146,16 +145,9 @@ export default {
return Object.values(this.viewOptions).some(g => g.selected);
},
imageURLs () {
const currentEvent = find(this.currentEventList, event => Boolean(event.season));
if (!currentEvent) {
return {
background: 'url(/static/npc/normal/customizations_background.png)',
npc: 'url(/static/npc/normal/customizations_npc.png)',
};
}
return {
background: `url(/static/npc/${currentEvent.season}/customizations_background.png)`,
npc: `url(/static/npc/${currentEvent.season}/customizations_npc.png)`,
background: 'url(/static/npc/normal/customizations_background.png)',
npc: 'url(/static/npc/normal/customizations_npc.png)',
};
},
categories () {
@@ -17,10 +17,10 @@
:emptyItem="emptyItem"
></slot>
<div class="image">
<Sprite
<div
v-once
:image-name="item.class"
/>
:class="item.class"
></div>
<slot
name="itemImage"
:item="item"
@@ -157,11 +157,9 @@
<script>
import { v4 as uuid } from 'uuid';
import Sprite from '@/components/ui/sprite';
export default {
components: {
Sprite,
},
props: {
item: {
@@ -38,6 +38,26 @@
}
</style>
<style>
.key_to_pets {
background-image: url('~@/assets/images/keys/key-to-the-pet-kennels.png');
width: 68px;
height: 68px;
}
.key_to_mounts {
background-image: url('~@/assets/images/keys/key-to-the-mount-kennels.png');
width: 68px;
height: 68px;
}
.key_to_both {
background-image: url('~@/assets/images/keys/keys-to-the-kennels.png');
width: 68px;
height: 68px;
}
</style>
<script>
import { beastCount, mountMasterProgress } from '@/../../common/script/count';
import { mapState } from '@/libs/store';
@@ -28,12 +28,6 @@
:item="item"
:abbreviated="true"
/>
<div
v-if="item.addlNotes"
class="mx-4 mb-3"
>
{{ item.addlNotes }}
</div>
<quest-rewards :quest="item" />
<div
v-if="!item.locked"
@@ -58,6 +52,12 @@
<div class="how-many-to-buy">
<strong>{{ $t('howManyToBuy') }}</strong>
</div>
<div
v-if="item.addlNotes"
class="mb-3"
>
{{ item.addlNotes }}
</div>
<div>
<number-increment
@updateQuantity="selectedAmountToBuy = $event"
@@ -82,7 +82,7 @@
v-if="priceType === 'gems'
&& !enoughCurrency(priceType, item.value * selectedAmountToBuy)
&& !item.locked"
class="btn btn-primary mb-3"
class="btn btn-primary"
@click="purchaseGems()"
>
{{ $t('purchaseGems') }}
@@ -177,6 +177,7 @@
.inner-content {
margin: 33px auto auto;
padding: 0px 24px;
}
.item-notes {
@@ -232,6 +233,8 @@
}
.purchase-amount {
margin-top: 24px;
.how-many-to-buy {
margin-bottom: 16px;
}
@@ -498,6 +501,38 @@ export default {
hideDialog () {
this.$root.$emit('bv::hide::modal', 'buy-quest-modal');
},
getDropIcon (drop) {
switch (drop.type) {
case 'gear':
return `shop_${drop.key}`;
case 'hatchingPotions':
return `Pet_HatchingPotion_${drop.key}`;
case 'food':
return `Pet_Food_${drop.key}`;
case 'eggs':
return `Pet_Egg_${drop.key}`;
case 'quests':
return `inventory_quest_scroll_${drop.key}`;
default:
return '';
}
},
getDropName (drop) {
switch (drop.type) {
case 'gear':
return this.content.gear.flat[drop.key].text();
case 'quests':
return this.content.quests[drop.key].text();
case 'hatchingPotions':
return this.$t('namedHatchingPotion', { type: this.content.hatchingPotions[drop.key].text() });
case 'food':
return this.content.food[drop.key].text();
case 'eggs':
return this.content.eggs[drop.key].text();
default:
return `Unknown type: ${drop.type}`;
}
},
purchaseGems () {
this.$root.$emit('bv::show::modal', 'buy-gems');
},
@@ -19,9 +19,9 @@ export const QuestHelperMixin = {
case 'quests':
return `inventory_quest_scroll_${drop.key}`;
case 'mounts':
return `Mount_Icon_${drop.key}`;
return `rewards_mount Mount_Icon_${drop.key}`;
case 'pets':
return `stable_Pet-${drop.key}`;
return `rewards_pet Pet-${drop.key}`;
default:
return `shop_${drop.key}`;
}
@@ -2,7 +2,7 @@
<div class="quest-content">
<div
class="quest-image"
:class="item.purchaseType === 'bundles' ? `quest_bundle_${item.key}` : `quest_${item.key}`"
:class="'quest_' + item.key"
></div>
<h3 class="text-center">
{{ itemText }}
@@ -17,7 +17,7 @@
<user-label :user="leader" />
</div>
<div
class="mx-4"
class="text"
v-html="itemNotes"
></div>
<questInfo
@@ -42,6 +42,12 @@
margin-top: 24px;
}
.text {
margin: 16px 16px;
overflow-y: auto;
text-overflow: ellipsis;
}
.leader-label {
font-size: 14px;
font-weight: bold;
@@ -1,7 +1,7 @@
<template>
<div>
<div
class="row mt-3"
class="row"
>
<div
v-if="quest.collect"
@@ -25,10 +25,7 @@
<dt>{{ $t('bossHP') + ':' }}</dt>
<dd>{{ quest.boss.hp }}</dd>
</div>
<div
class="table-row"
v-if="quest.purchaseType !== 'bundles'"
>
<div class="table-row">
<dt>{{ $t('difficulty') + ':' }}</dt>
<dd>
<div
@@ -42,6 +39,7 @@
</div>
<div
v-if="quest.end && !abbreviated"
class="m-auto"
>
{{ limitedString }}
</div>
@@ -1,7 +1,7 @@
<template>
<div
v-if="quest.drop"
class="quest-rewards mb-3"
class="quest-rewards"
>
<div
class="header d-flex align-items-center"
@@ -39,7 +39,7 @@
label-class="purple"
>
<div slot="itemImage">
<Sprite :image-name="getDropIcon(drop)" />
<div :class="getDropIcon(drop)"></div>
</div>
<div slot="popoverContent">
<quest-popover :item="drop" />
@@ -92,7 +92,7 @@
:count="drop.amount"
/>
<div slot="itemImage">
<Sprite :image-name="getDropIcon(drop)" />
<div :class="getDropIcon(drop)"></div>
</div>
<div slot="popoverContent">
<equipmentAttributesPopover
@@ -133,7 +133,6 @@ import { QuestHelperMixin } from './quest-helper.mixin';
import EquipmentAttributesPopover from '@/components/inventory/equipment/attributesPopover';
import QuestPopover from './questPopover';
import CountBadge from '../../ui/countBadge';
import Sprite from '../../ui/sprite';
export default {
components: {
@@ -142,7 +141,6 @@ export default {
ItemWithLabel,
SectionButton,
EquipmentAttributesPopover,
Sprite,
},
mixins: [QuestHelperMixin],
props: ['quest'],
@@ -480,7 +480,7 @@ export default {
});
await this.triggerGetWorldState();
this.currentEvent = _find(this.currentEventList, event => Boolean(event.season));
this.currentEvent = _find(this.currentEventList, event => Boolean(['winter', 'spring', 'summer', 'fall'].includes(event.season)));
this.imageURLs.background = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_background.png)`;
this.imageURLs.npc = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_npc.png)`;
},
@@ -41,10 +41,10 @@
class="suggestedDot"
></span>
<div class="image">
<Sprite
<div
v-once
:image-name="item.class"
/>
:class="item.class"
></div>
<slot
name="itemImage"
:item="item"
@@ -281,13 +281,11 @@ import svgClock from '@/assets/svg/clock.svg';
import EquipmentAttributesPopover from '@/components/inventory/equipment/attributesPopover';
import QuestInfo from './quests/questInfo.vue';
import Sprite from '@/components/ui/sprite';
export default {
components: {
EquipmentAttributesPopover,
QuestInfo,
Sprite,
},
props: {
item: {
@@ -8,10 +8,10 @@
v-if="notification.type === 'drop'"
class="icon-item"
>
<Sprite
:image-name="notification.icon"
<div
:class="notification.icon"
class="icon-negative-margin"
/>
></div>
</div>
<div
@@ -231,13 +231,9 @@ import star from '@/assets/svg/star.svg';
import mana from '@/assets/svg/mana.svg';
import sword from '@/assets/svg/sword.svg';
import CloseIcon from '../shared/closeIcon';
import Sprite from '@/components/ui/sprite';
export default {
components: {
CloseIcon,
Sprite,
},
components: { CloseIcon },
props: ['notification', 'visibleAmount'],
data () {
return {
@@ -1,6 +1,7 @@
<template>
<div
class="notifications"
:class="notificationsTopPosClass"
:style="{'--current-scrollY': notificationTopY}"
>
<transition-group
@@ -103,6 +104,7 @@ export default {
computed: {
...mapState({
notificationStore: 'notificationStore',
userSleeping: 'user.data.preferences.sleep',
currentEventList: 'worldState.data.currentEventList',
}),
currentEvent () {
@@ -111,6 +113,18 @@ export default {
isEventActive () {
return Boolean(this.currentEvent?.event);
},
notificationsTopPosClass () {
const base = 'notifications-top-pos-';
let modifier = '';
if (this.userSleeping) {
modifier = 'sleeping';
} else {
modifier = 'normal';
}
return `${base}${modifier} scroll-${this.scrollY}`;
},
notificationBannerHeight () {
let scrollPosToCheck = 56;
@@ -139,6 +139,13 @@
<style lang='scss' scoped>
@import '~@/assets/scss/colors.scss';
@media only screen and (max-width : 750px) {
.login-button {
margin: 0 auto !important;
margin-top: 18px !important;
}
}
.habitica-logo {
height: 64px;
margin: 28px auto 0px auto;
@@ -158,7 +165,7 @@
nav.navbar {
background: $purple-100 url(~@/assets/svg/for-css/bits.svg) right no-repeat;
padding-left: 24px;
padding-left: 25px;
padding-right: 12.5px;
height: 56px;
box-shadow: 0 1px 2px 0 rgba($black, 0.24);
@@ -258,16 +265,6 @@
}
}
}
@media only screen and (max-width : 750px) {
.login-button {
margin: 0 auto !important;
margin-top: 18px !important;
}
.habitica-logo {
margin: 4px auto 0px auto;
}
}
</style>
<script>
@@ -781,7 +781,7 @@
<script>
import hello from 'hellojs';
import debounce from 'lodash/debounce';
import isEmail from 'validator/es/lib/isEmail';
import isEmail from 'validator/lib/isEmail';
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import { buildAppleAuthUrl } from '../../libs/auth';
import googlePlay from '@/assets/images/home/google-play-badge.svg';
@@ -8,10 +8,10 @@
<div class="spell">
<div class="spell-border">
<div class="mana">
<Sprite
class="img"
:imageName="`shop_${spell.key}`"
/>
<div
class="img"
:class="`shop_${spell.key} shop-sprite item-img`"
></div>
</div>
</div>
<div class="details">
@@ -75,9 +75,10 @@
class="spell"
>
<div class="details">
<Sprite
:imageName="`shop_${skill.key}`"
/>
<div
class="img"
:class="`shop_${skill.key} shop-sprite item-img`"
></div>
</div>
<div
v-if="user.stats.lvl < skill.lvl"
@@ -400,12 +401,10 @@ import {
setLocalSetting,
getLocalSetting,
} from '@/libs/userlocalManager';
import Sprite from '@/components/ui/sprite';
export default {
components: {
Drawer,
Sprite,
},
directives: {
mousePosition: MouseMoveDirective,
@@ -1,50 +0,0 @@
<template>
<img
class="pixel-art"
v-if="imageName && imageName !== ''"
:src="imageUrl()"
>
</template>
<style>
.pixel-art {
image-rendering: pixelated;
}
</style>
<script>
import GIF_SPRITES from '@/../../common/script/content/constants/gifSprites';
export default {
props: {
imageName: {
type: String,
},
prefix: {
type: String,
},
},
methods: {
getFileType (name) {
if (GIF_SPRITES.includes(name)) {
return 'gif';
}
return 'png';
},
imageUrl () {
if (!this.imageName) {
return '';
}
let name = this.imageName;
if (name.indexOf(' ') !== -1) {
const components = name.split(' ');
name = components[components.length - 1];
}
if (this.prefix) {
name = `${this.prefix}_${name}`;
}
return `https://habitica-assets.s3.amazonaws.com/mobileApp/images/${name}.${this.getFileType(name)}`;
},
},
};
</script>
@@ -18,9 +18,9 @@
v-if="label !== 'skip'"
:id="key"
class="gear box"
:class="{white: isUsed(equippedItems, key)}"
:class="{white: equippedItems[key] && equippedItems[key].indexOf('base_0') === -1}"
>
<Sprite v-if="isUsed(equippedItems, key)" :image-name="`shop_${equippedItems[key]}`"/>
<div :class="`shop_${equippedItems[key]}`"></div>
</div>
<b-popover
v-if="label !== 'skip'
@@ -64,9 +64,9 @@
v-if="label !== 'skip'"
:id="key + 'C'"
class="gear box"
:class="{white: isUsed(costumeItems, key)}"
:class="{white: costumeItems[key] && costumeItems[key].indexOf('base_0') === -1}"
>
<Sprite v-if="isUsed(costumeItems, key)" :image-name="`shop_${costumeItems[key]}`"/>
<div :class="`shop_${costumeItems[key]}`"></div>
</div>
<!-- Show background on 8th tile rather than a piece of equipment.-->
<div
@@ -75,7 +75,7 @@
:class="{white: user.preferences.background}"
style="overflow:hidden"
>
<Sprite :image-name="'icon_background_' + user.preferences.background" />
<div :class="'icon_background_' + user.preferences.background"></div>
</div>
<b-popover
v-if="label !== 'skip'
@@ -124,10 +124,10 @@
class="box"
:class="{white: user.items.currentPet}"
>
<Sprite
:image-name="user.items.currentPet ?
`stable_Pet-${user.items.currentPet}` : ''"
/>
<div
class="Pet"
:class="`Pet-${user.items.currentPet}`"
></div>
</div>
</div>
<div class="pet-mount-well-text">
@@ -156,10 +156,10 @@
class="box"
:class="{white: user.items.currentMount}"
>
<Sprite
:image-name="user.items.currentMount ?
`stable_Mount_Icon_${user.items.currentMount}` : ''"
/>
<div
class="mount"
:class="`Mount_Icon_${user.items.currentMount}`"
></div>
</div>
</div>
<div class="pet-mount-well-text">
@@ -330,7 +330,6 @@ import statsComputed from '@/../../common/script/libs/statsComputed';
import { mapState } from '@/libs/store';
import attributesGrid from '@/components/inventory/equipment/attributesGrid';
import toggleSwitch from '@/components/ui/toggleSwitch';
import Sprite from '@/components/ui/sprite';
const DROP_ANIMALS = keys(Content.pets);
const TOTAL_NUMBER_OF_DROP_ANIMALS = DROP_ANIMALS.length;
@@ -338,7 +337,6 @@ export default {
components: {
toggleSwitch,
attributesGrid,
Sprite,
},
props: ['user', 'showAllocation'],
data () {
@@ -419,9 +417,6 @@ export default {
},
methods: {
isUsed (items, key) {
return items[key] && items[key].indexOf('base_0') === -1;
},
getGearTitle (key) {
return this.flatGear[key].text();
},
-1
View File
@@ -37,7 +37,6 @@ export function createAnimal (egg, potion, type, _content, userItems) {
return {
key: animalKey,
class: type === 'pet' ? `Pet Pet-${animalKey}` : `Mount_Icon_${animalKey}`,
imageName: type === 'pet' ? `stable_Pet-${animalKey}` : `stable_Mount_Icon_${animalKey}`,
eggKey: egg.key,
eggName: getText(egg.text),
potionKey: potion.key,
+2 -20
View File
@@ -1,16 +1,6 @@
import Vue from 'vue';
import axios from 'axios';
import {
ModalPlugin,
DropdownPlugin,
PopoverPlugin,
FormPlugin,
FormInputPlugin,
FormRadioPlugin,
TooltipPlugin,
NavbarPlugin,
CollapsePlugin,
} from 'bootstrap-vue';
import BootstrapVue from 'bootstrap-vue';
import Fragment from 'vue-fragment';
import AppComponent from './app';
import {
@@ -39,15 +29,7 @@ Vue.config.productionTip = IS_PRODUCTION;
// window['habitica-i18n] is injected by the server
Vue.use(i18n, { i18nData: window && window['habitica-i18n'] });
Vue.use(StoreModule);
Vue.use(ModalPlugin);
Vue.use(DropdownPlugin);
Vue.use(PopoverPlugin);
Vue.use(FormPlugin);
Vue.use(FormInputPlugin);
Vue.use(FormRadioPlugin);
Vue.use(TooltipPlugin);
Vue.use(NavbarPlugin);
Vue.use(CollapsePlugin);
Vue.use(BootstrapVue);
Vue.use(Fragment.Plugin);
setUpLogging();
@@ -5,7 +5,6 @@ import unlock from '@/../../common/script/ops/unlock';
import buy from '@/../../common/script/ops/buy/buy';
import appearanceSets from '@/../../common/script/content/appearance/sets';
import appearances from '@/../../common/script/content/appearance';
import { getScheduleMatchingGroup } from '@/../../common/script/content/constants/schedule';
import { userStateMixin } from './userState';
@@ -32,11 +31,8 @@ export const avatarEditorUtilities = { // eslint-disable-line import/prefer-defa
option.key = key;
option.pathKey = pathKey;
option.active = userPreference === key;
option.imageName = this.createImageName(type, subType, key);
option.class = this.createClass(type, subType, key);
option.click = optionParam => (option.gemLocked ? this.unlock(`${optionParam.pathKey}.${key}`) : this.set({ [`preferences.${optionParam.pathKey}`]: optionParam.key }));
option.text = subType ? appearances[type][subType][key].text()
: appearances[type][key].text();
return option;
},
mapKeysToOption (key, type, subType, set) {
@@ -60,8 +56,8 @@ export const avatarEditorUtilities = { // eslint-disable-line import/prefer-defa
return option;
},
createImageName (type, subType, key) {
let str = '';
createClass (type, subType, key) {
let str = `${type} ${subType} `;
switch (type) {
case 'shirt': {
@@ -74,14 +70,14 @@ export const avatarEditorUtilities = { // eslint-disable-line import/prefer-defa
}
case 'hair': {
if (subType === 'color') {
str += `color_hair_bangs_${this.user.preferences.hair.bangs || 1}_${key}`;
str += `icon_hair_bangs_${this.user.preferences.hair.bangs || 1}_${key}`;
} else {
str += `hair_${subType}_${key}_${this.user.preferences.hair.color}`;
str += `icon_hair_${subType}_${key}_${this.user.preferences.hair.color}`;
}
break;
}
case 'skin': {
str += `skin_${key}`;
str += `skin skin_${key}`;
break;
}
default: {
@@ -131,7 +131,7 @@ input {
<script>
import axios from 'axios';
import isEmail from 'validator/es/lib/isEmail';
import * as validator from 'validator';
import debounce from 'lodash/debounce';
import { mapState } from '@/libs/store';
@@ -162,7 +162,7 @@ export default {
user: 'user.data',
}),
validEmail () {
return isEmail(this.updates.newEmail);
return validator.isEmail(this.updates.newEmail);
},
allowedToSave () {
return !this.validEmail || this.updates.password.length === 0;
@@ -68,7 +68,7 @@
<script>
import axios from 'axios';
import isEmail from 'validator/es/lib/isEmail';
import * as validator from 'validator';
import { mapState } from '@/libs/store';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
@@ -99,7 +99,7 @@ export default {
return this.previousEmail !== this.updates.newEmail;
},
validEmail () {
return isEmail(this.updates.newEmail);
return validator.isEmail(this.updates.newEmail);
},
disallowedToSave () {
return !this.emailChanged
@@ -208,7 +208,7 @@ table {
</style>
<script>
import isURL from 'validator/es/lib/isURL';
import * as validator from 'validator';
import uuid from '@/../../common/script/libs/uuid';
import { mapState } from '@/libs/store';
@@ -247,7 +247,7 @@ export default {
},
methods: {
isValidUrl (url) {
return isURL(url, {
return validator.isURL(url, {
require_tld: true,
require_protocol: true,
protocols: ['http', 'https'],
-388
View File
@@ -1,388 +0,0 @@
<template>
<div
id="app"
:class="{
'casting-spell': castingSpell,
}"
>
<!-- <banned-account-modal /> -->
<amazon-payments-modal v-if="!isStaticPage" />
<payments-success-modal />
<sub-cancel-modal-confirm v-if="isUserLoaded" />
<sub-canceled-modal v-if="isUserLoaded" />
<bug-report-modal v-if="isUserLoaded" />
<bug-report-success-modal v-if="isUserLoaded" />
<external-link-modal />
<birthday-modal />
<template v-if="isUserLoaded">
<chat-banner />
<damage-paused-banner />
<gems-promo-banner />
<gift-promo-banner />
<birthday-banner />
<notifications-display />
<app-menu />
<div
class="container-fluid"
:class="{'no-margin': noMargin}"
>
<app-header />
<buyModal
:item="selectedItemToBuy || {}"
:with-pin="true"
:generic-purchase="genericPurchase(selectedItemToBuy)"
@buyPressed="customPurchase($event)"
/>
<selectMembersModal
:item="selectedSpellToBuy || {}"
:group="user.party"
@memberSelected="memberSelected($event)"
/>
<div :class="{sticky: user.preferences.stickyHeader}">
<router-view />
</div>
</div>
<app-footer v-if="!hideFooter" />
<audio
id="sound"
ref="sound"
autoplay="autoplay"
></audio>
</template>
</div>
</template>
<style lang='scss' scoped>
@import '~@/assets/scss/colors.scss';
#app {
display: flex;
flex-direction: column;
overflow-x: hidden;
}
.casting-spell {
cursor: crosshair;
}
.container-fluid {
flex: 1 0 auto;
}
.no-margin {
margin-left: 0;
margin-right: 0;
padding-left: 0;
padding-right: 0;
}
.notification {
border-radius: 1000px;
background-color: $green-10;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
padding: .5em 1em;
color: $white;
margin-top: .5em;
margin-bottom: .5em;
}
</style>
<style lang='scss'>
@import '~@/assets/scss/colors.scss';
.modal-backdrop {
opacity: .9 !important;
background-color: $purple-100 !important;
}
/* Push progress bar above modals */
#nprogress .bar {
z-index: 1600 !important; /* Must stay above nav bar */
}
</style>
<script>
import axios from 'axios';
import { loadProgressBar } from 'axios-progress-bar';
import birthdayModal from '@/components/news/birthdayModal';
import AppMenu from '@/components/header/menu';
import AppHeader from '@/components/header/index';
import ChatBanner from '@/components/header/banners/chatBanner';
import DamagePausedBanner from '@/components/header/banners/damagePaused';
import GemsPromoBanner from '@/components/header/banners/gemsPromo';
import GiftPromoBanner from '@/components/header/banners/giftPromo';
import BirthdayBanner from '@/components/header/banners/birthdayBanner';
import AppFooter from '@/components/appFooter';
import notificationsDisplay from '@/components/notifications';
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import BuyModal from '@/components/shops/buyModal.vue';
import SelectMembersModal from '@/components/selectMembersModal.vue';
import notifications from '@/mixins/notifications';
import { setup as setupPayments } from '@/libs/payments';
import amazonPaymentsModal from '@/components/payments/amazonModal';
import paymentsSuccessModal from '@/components/payments/successModal';
import subCancelModalConfirm from '@/components/payments/cancelModalConfirm';
import subCanceledModal from '@/components/payments/canceledModal';
import externalLinkModal from '@/components/externalLinkModal.vue';
import spellsMixin from '@/mixins/spells';
import {
CONSTANTS,
getLocalSetting,
removeLocalSetting,
} from '@/libs/userlocalManager';
const bugReportModal = () => import(/* webpackChunkName: "bug-report-modal" */'@/components/bugReportModal');
const bugReportSuccessModal = () => import(/* webpackChunkName: "bug-report-success-modal" */'@/components/bugReportSuccessModal');
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
export default {
name: 'App',
components: {
AppMenu,
AppHeader,
AppFooter,
birthdayModal,
ChatBanner,
DamagePausedBanner,
GemsPromoBanner,
GiftPromoBanner,
BirthdayBanner,
notificationsDisplay,
BuyModal,
SelectMembersModal,
amazonPaymentsModal,
paymentsSuccessModal,
subCancelModalConfirm,
subCanceledModal,
bugReportModal,
bugReportSuccessModal,
externalLinkModal,
},
mixins: [notifications, spellsMixin],
data () {
return {
selectedItemToBuy: null,
selectedSpellToBuy: null,
audioSource: null,
audioSuffix: null,
loading: true,
bannerHidden: false,
};
},
computed: {
...mapState(['isUserLoggedIn', 'browserTimezoneUtcOffset', 'isUserLoaded']),
...mapState({ user: 'user.data' }),
isStaticPage () {
return this.$route.meta.requiresLogin === false;
},
castingSpell () {
return this.$store.state.spellOptions.castingSpell;
},
noMargin () {
return ['privateMessages'].includes(this.$route.name);
},
hideFooter () {
return ['privateMessages'].includes(this.$route.name);
},
},
created () {
this.$root.$on('playSound', sound => {
const theme = this.user.preferences.sound;
if (!theme || theme === 'off') {
return;
}
const file = `https://habitica-assets.s3.amazonaws.com/mobileApp/sounds/${theme}/${sound}`;
if (this.audioSuffix === null) {
this.audioSource = document.createElement('source');
if (this.$refs.sound.canPlayType('audio/ogg')) {
this.audioSuffix = '.ogg';
this.audioSource.type = 'audio/ogg';
} else {
this.audioSuffix = '.mp3';
this.audioSource.type = 'audio/mp3';
}
this.audioSource.src = file + this.audioSuffix;
this.$refs.sound.appendChild(this.audioSource);
} else {
this.audioSource.src = file + this.audioSuffix;
}
this.$refs.sound.load();
});
this.$root.$on('buyModal::showItem', item => {
this.selectedItemToBuy = item;
this.$root.$emit('bv::show::modal', 'buy-modal');
});
this.$root.$on('bv::modal::hidden', event => {
if (event.componentId === 'buy-modal') {
this.$root.$emit('buyModal::hidden', this.selectedItemToBuy.key);
}
});
this.$root.$on('selectMembersModal::showItem', item => {
this.selectedSpellToBuy = item;
this.$root.$emit('bv::show::modal', 'select-member-modal');
});
// @TODO split up this file, it's too big
loadProgressBar({
showSpinner: false,
});
// Setup listener for title
this.$store.watch(state => state.title, title => {
document.title = title;
});
// Load the user and the user tasks
Promise.all([
this.$store.dispatch('user:fetch'),
this.$store.dispatch('tasks:fetchUserTasks'),
]).then(() => {
this.$store.state.isUserLoaded = true;
Analytics.setUser();
Analytics.updateUser();
if (window && window['habitica-i18n']) {
if (this.user.preferences.language === window['habitica-i18n'].language.code) {
return null;
}
}
if (window && window['habitica-i18n']) {
if (this.user.preferences.language === window['habitica-i18n'].language.code) {
return null;
}
}
return axios.get(
'/api/v4/i18n/browser-script',
{
language: this.user.preferences.language,
headers: {
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
Expires: '0',
},
},
);
}).then(() => {
const i18nData = window && window['habitica-i18n'];
this.$loadLocale(i18nData);
this.hideLoadingScreen();
// Adjust the timezone offset
const browserTimezoneOffset = -this.browserTimezoneUtcOffset;
if (this.user.preferences.timezoneOffset !== browserTimezoneOffset) {
this.$store.dispatch('user:set', {
'preferences.timezoneOffset': browserTimezoneOffset,
});
}
let appState = getLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
if (appState) {
appState = JSON.parse(appState);
if (appState.paymentCompleted) {
removeLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
this.$root.$emit('habitica:payment-success', appState);
}
}
this.$nextTick(() => {
// Load external scripts after the app has been rendered
setupPayments();
});
}).catch(err => {
console.error('Impossible to fetch user. Clean up localStorage and refresh.', err); // eslint-disable-line no-console
});
},
beforeDestroy () {
this.$root.$off('playSound');
this.$root.$off('buyModal::showItem');
this.$root.$off('selectMembersModal::showItem');
},
mounted () {
// Remove the index.html loading screen and now show the inapp loading
const loadingScreen = document.getElementById('loading-screen');
if (loadingScreen) document.body.removeChild(loadingScreen);
},
methods: {
checkForBannedUser (error) {
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
const parseSettings = JSON.parse(AUTH_SETTINGS);
const errorMessage = error.response.data.message;
// Case where user is not logged in
if (!parseSettings) {
return false;
}
const bannedMessage = this.$t('accountSuspended', {
communityManagerEmail: COMMUNITY_MANAGER_EMAIL,
userId: parseSettings.auth.apiId,
});
if (errorMessage !== bannedMessage) return false;
this.$store.dispatch('auth:logout', { redirectToLogin: true });
return true;
},
itemSelected (item) {
this.selectedItemToBuy = item;
},
genericPurchase (item) {
if (!item) return false;
if (['card', 'debuffPotion'].includes(item.purchaseType)) return false;
return true;
},
customPurchase (item) {
if (item.purchaseType === 'card') {
this.selectedSpellToBuy = item;
// hide the dialog
this.$root.$emit('bv::hide::modal', 'buy-modal');
// remove the dialog from our modal-stack,
// the default hidden event is delayed
this.$root.$emit('bv::modal::hidden', {
target: {
id: 'buy-modal',
},
});
this.$root.$emit('bv::show::modal', 'select-member-modal');
}
if (item.purchaseType === 'debuffPotion') {
this.castStart(item, this.user);
}
},
async memberSelected (member) {
await this.castStart(this.selectedSpellToBuy, member);
this.selectedSpellToBuy = null;
if (this.user.party._id) {
this.$store.dispatch('party:getMembers', { forceLoad: true });
}
this.$root.$emit('bv::hide::modal', 'select-member-modal');
},
hideLoadingScreen () {
this.loading = false;
},
},
};
</script>
<style src="intro.js/minified/introjs.min.css"></style>
<style src="axios-progress-bar/dist/nprogress.css"></style>
-29
View File
@@ -76,9 +76,6 @@ const webpackPlugins = [
if ((context.includes('sinon') || resource.includes('sinon') || context.includes('nise')) && nconf.get('TIME_TRAVEL_ENABLED') !== 'true') {
return true;
}
if (context.includes('yargs')) {
return true;
}
return false;
},
}),
@@ -94,28 +91,6 @@ module.exports = {
dependency: { not: ['url'] },
type: 'asset/source',
},
{
test: /\.js$/,
// Exclude transpiling `node_modules`, except `bootstrap-vue/src`
exclude: /node_modules\/(?!bootstrap-vue\/src\/)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
{
test: /\.js$/,
// Exclude transpiling `node_modules`, except `bootstrap-vue/src`
exclude: /node_modules\/(?!validator)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
resolve: {
@@ -127,10 +102,6 @@ module.exports = {
stream: false,
timers: require.resolve('timers-browserify'),
},
alias: {
// Alias for using source of BootstrapVue
'bootstrap-vue$': 'bootstrap-vue/src/index.js',
},
},
plugins: webpackPlugins,
},
+1 -1
View File
@@ -125,7 +125,7 @@
"paymentSubBillingWithMethod": "Tvé předplatné <strong>$<%= amount %> bude účtováno </strong> každé/CZ <strong><%= months %> měsíce/ů </strong> skrze <strong><%= paymentMethod %></strong>.",
"invalidUnlockSet": "Tento set předmětů je prošlý a nemůže být odemčen.",
"amountExp": "<%= amount %> Zk",
"limitedAvailabilityDays": "K získání na <%= day %>d <%= hours%>h <%= minutes %>m",
"limitedAvailabilityDays": "K získání na <%= dny %>d <%= hodin%>h <%= minut %>m",
"nGemsGift": "<%= nGems %> Drahokamy (Dárek)",
"nMonthsSubscriptionGift": "<%= nMonths %> Měsíční odběr (Dárek)",
"nGems": "<%= nGems %> Drahokamy",
+2 -12
View File
@@ -214,7 +214,7 @@
"backgroundStormyRooftopsNotes": "Schleiche über stürmische Hausdächer.",
"backgroundWindyAutumnText": "Windiger Herbst",
"backgroundWindyAutumnNotes": "Jage Laub an einem windigen Herbsttag.",
"incentiveBackgrounds": "Standard Hintergründe",
"incentiveBackgrounds": "Einfaches Hintergründe-Set",
"backgroundVioletText": "Violett",
"backgroundVioletNotes": "Ein vital-violetter Hintergrund.",
"backgroundBlueText": "Blau",
@@ -871,15 +871,5 @@
"backgroundPottersStudioText": "Töpfer Atelier",
"backgroundPottersStudioNotes": "Erschaffe Kunst im Töpfer Atelier.",
"backgrounds052024": "SET 120: Veröffentlicht im Mai 2024",
"backgroundDragonsBackNotes": "Segle durch den Himmel auf einem Drachenrücken.",
"backgrounds062024": "SET 121: Veröffentlicht im Juni 2024",
"backgroundShellGateText": "Muscheltor",
"backgroundShellGateNotes": "Spaziere durch das korallenverzierte Muscheltor.",
"backgrounds072024": "SET 122: Veröffentlicht im Juli 2024",
"backgroundRiverBottomText": "Flussgrund",
"backgroundRiverBottomNotes": "Erkunde den Grund eines Flusses.",
"monthlyBackgrounds": "Hintergrund des Monats",
"backgrounds082024": "Set 123: Veröffentlicht im August 2024",
"backgroundSavannaText": "Dunstiges Grasland",
"backgroundSavannaNotes": "Wandere durch Dunstiges Grasland."
"backgroundDragonsBackNotes": "Segle durch den Himmel auf einem Drachenrücken."
}
+1 -6
View File
@@ -189,10 +189,5 @@
"notEnoughGold": "Nicht genügend Gold.",
"chatCastSpellPartyTimes": "<%= username %> verwendet <%= spell %> <%= times %> Male für Deine Party <%= times %>.",
"chatCastSpellUserTimes": "<%= username %> spricht <%= times %> mal <%= spell %> auf <%= target %>.",
"nextReward": "Nächste Anmelde-Belohnung",
"skins": "Hautfarben",
"titleHaircolor": "Haarfarben",
"titleFacialHair": "Bärte",
"titleHairbase": "Frisuren",
"customizations": "Individualisierungen"
"nextReward": "Nächste Anmelde-Belohnung"
}
@@ -8,13 +8,13 @@
"commGuideHeadingInteractions": "Interaktionen in Habitica",
"commGuidePara015": "Habitica hat verschiedene Orte wo du mit anderen Spielern in Kontakt kommen kannst. Darunter sind die Chats (Privatnachrichten oder Party Chats) und außerdem die Möglichkeit nach Parties und Herausforderungen zu suchen.",
"commGuidePara016": "Wenn Du dich durch die sozialen Aspekte von Habitica bewegst, gibt es ein paar allgemeine Regeln, damit jeder sicher und glücklich ist.",
"commGuideList02A": "<strong>Respektiert einander</strong>. Sei höflich, freundlich und hilfsbereit. Vergiss nicht: Habiticaner kommen aus den verschiedensten Hintergründen und haben sehr unterschiedliche Erfahrungen gemacht.",
"commGuideList02A": "<strong>Respektiert einander</strong>. Sei höflich, freundlich und hilfsbereit. Vergiss nicht: Habiticaner kommen aus den verschiedensten Hintergründen und haben sehr unterschiedliche Erfahrungen gemacht. Das macht Habitica so eigenartig! Es ist wichtig, dass man beim Aufbauen einer Community seine Unterschiede und Ähnlichkeiten respektieren, aber natürlich auch feiern kann.",
"commGuideList02B": "<strong>Halte Dich an die <a href='/static/terms' target='_blank'>allgemeinen Geschäftsbedingungen</a></strong>, sowohl in öffentlichen als auch in privaten Bereichen.",
"commGuideList02C": "<strong>Poste keine Bilder oder Texte, die Gewalt darstellen, andere einschüchtern, oder eindeutig/indirekt sexuell sind, die Diskriminierung, Fanatismus, Rassismus, Sexismus, Hass, Belästigungen oder Hetze gegen jedwede Individuen oder Gruppen beinhalten.</strong> Auch nicht als Scherz oder Meme. Das bezieht sowohl Sprüche als auch Stellungnahmen mit ein. Nicht jeder hat den gleichen Humor, etwas, was Du als Witz wahrnimmst, kann für jemand anderen verletzend sein.",
"commGuideList02D": "<strong>Sei dir bewusst, dass Habiticaner Menschen unterschiedlichen Alters und mit verschiedenen Hintergründen sind.</strong> Wettbewerbe und Spielerprofile sollten den Jugendschutz beachten, sowie Schimpfwörter, Streitigkeiten und Konflikte vermeiden.",
"commGuideList02D": "<strong>Halte die Diskussionen für alle Altersgruppen angemessen</strong>. Das heißt, Erwachsenenthemen in öffentlichen Bereichen zu vermeiden. Viele junge Habiticaner und Menschen mit verschiedenen Hintergründen nutzen diese Seite. Wir wollen unsere Gemeinschaft so angenehm und inklusiv wie möglich gestalten.",
"commGuideList02E": "<strong>Vermeide vulgäre Ausdrücke. Dazu gehören auch mildere, religiöse Ausdrücke, die anderswo möglicherweise akzeptiert werden, oder verschleierte Schimpfwörter</strong>. Unter uns sind Menschen aus allen religiösen und kulturellen Hintergründen und wir wollen, dass sich alle im öffentlichen Raum wohl fühlen. <strong>Wenn Dir ein Moderator oder Mitarbeiter mitteilt, dass ein bestimmter Ausdruck in Habitica nicht erlaubt ist, selbst wenn er Dir vielleicht nicht problematisch vorkommt, ist diese Entscheidung endgültig</strong>. Zusätzlich werden verbale Angriffe jeder Art strenge Konsequenzen haben, da sie auch unsere Nutzungsbedingungen verletzen.",
"commGuideList02F": "Vermeide längere Diskussionen über spaltende Themen in der Taverne und wenn sie außerhalb des Themenbereichs liegen. Wenn jemand etwas sagt, das zwar von den Richtlinien her erlaubt ist, das Dich aber verletzt, dann ist es in Ordnung, diese Person höflich darauf hinzuweisen. Wenn Dir eine Person sagt, dass ihr Dein Verhalten unangenehm ist, nimm Dir Zeit, darüber zu reflektieren, anstatt im Zorn zu antworten. Aber wenn Du das Gefühl hast, dass ein Gespräch hitzig, übermäßig emotional, oder verletzend wird, dann <strong>lass dich nicht darauf ein. Melde stattdessen die Beiträge, um uns darüber in Kenntnis zu setzen.</strong> Moderatoren werden so schnell wie möglich antworten. Du kannst auch eine E-Mail an <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> senden und gegebenenfalls Screenshots anhängen.",
"commGuideList02G": "<strong>Erfülle alle Mitarbeitenden-Anfragen sofort</strong>. Diese könnten Folgendes beinhalten, ist aber nicht darauf beschränkt: Dich aufzufordern, deine Beiträge in einem bestimmten Bereich zu begrenzen, dein Profil zu bearbeiten, um ungeeignete Inhalte zu entfernen, etc. Diskutiere nicht mit Mitarbeitenden. Solltest du mit einer Entscheidung unzufrieden sein, oder anderes Feedback zur Mitarbeitenden haben, sende eine E-mail an <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a>, um unseren Community Manager zu kontaktieren.",
"commGuideList02G": "<strong>Erfülle alle Mitarbeitenden-Anfragen sofort</strong>. Diese könnten Folgendes beinhalten, ist aber nicht darauf beschränkt: Dich aufzufordern, deine Beiträge in einem bestimmten Bereich zu begrenzen, dein Profil zu bearbeiten, um ungeeignete Inhalte zu entfernen, dich zu bitten, deine Diskussion in einen geeigneteren Bereich zu verschieben, etc. Diskutiere nicht mit Mitarbeitenden. Solltest du mit einer Entscheidung unzufrieden sein, oder anderes Feedback zur Mitarbeitenden haben, sende eine E-mail an <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> um unseren Community Manager zu kontaktieren.",
"commGuideList02J": "<strong>Poste keinen Spam</strong>. Spamming kann Folgendes beinhalten, ist aber nicht beschränkt auf: das Posten desselben Kommentars oder derselben Frage an mehreren Stellen, <strong>das Posten von Links ohne Erklärung oder Kontext</strong>, das Posten unsinniger Nachrichten, das Posten mehrerer Werbebotschaften für eine Gilde, Party, oder Herausforderung, oder das Posten vieler Nachrichten hintereinander. Wenn Du irgendeinen Nutzen daraus ziehst, wenn jemand auf einen Link klickt, musst Du das im Text Deiner Nachricht offenlegen, sonst wird sie auch als Spam betrachtet. Mods können gegebenenfalls nach ihrem Ermessen entscheiden, was Spam ausmacht.",
"commGuideList02K": "<strong>Bitte vermeide große Überschriften in öffentlichen Chats, vor allem in der Taverne.</strong> Ähnlich wie bei GROSSBUCHSTABEN liest sich der Text, als ob Du schreien würdest, und beeinträchtigt die gemütliche Atmosphäre.",
"commGuideList02L": "<strong>Wir raten Dir dringend davon ab, persönliche Informationen - besonders solche, mit denen Du identifiziert werden könntest - in öffentlichen Chats zu teilen.</strong> Zu den identifizierenden Informationen gehören unter anderem: Deine Adresse, Deine E-Mail-Adresse und Dein API-Token/Passwort. Dies dient nur Deiner Sicherheit! Mitarbeiter oder Moderatoren werden solche Beiträge nach eigenem Ermessen entfernen. Wenn Du nach persönlichen Informationen in einer privaten Gilde, Party oder per PN gefragt wirst, empfehlen wir dringend, dass Du höflich ablehnst und Mitarbeiter und Moderatoren informierst, indem Du entweder 1) den Beitrag über das Fähnchen meldest, oder 2) eine E-Mail an <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> schreibst und Screenshots anhängst.",
@@ -130,7 +130,7 @@
"commGuideList02M": "Frage nicht nach oder bettle nicht um Edelsteine, Abonnements oder die Mitgliedschaft in Gruppenplänen. Wenn Du ungewollte Nachrichten erhältst, in denen man Dich um bezahlte Artikel fragt, melde sie bitte. Wiederholte Betteleien nach Edelsteinen oder Abonnements, vor allem nachdem bereits eine Warnung ausgesprochen wurde, können zu einer Kontosperre führen.",
"commGuideList09D": "Entfernung oder Herabstufung des Mitwirkenden-Ranges",
"commGuideList05H": "Schwerwiegende oder wiederholte Versuche, andere Spielende zu betrügen oder zu bedrängen, um an Gegenstände zu kommen, die echtes Geld kosten",
"commGuideList02N": "<strong>Melde Nachrichten, in denen diese Richtlinien oder die Nutzungsbedingungen nicht eingehalten werden.</strong> Melde eine Nachricht direkt oder informiere Mitarbeiter:innen über <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a>, um Verstöße in Profilen oder Wettbewerben zu melden. Wir werden uns schnellstmöglich darum kümmern. Kontaktiere uns in deiner Muttersprache, wenn dir das leichter fällt. Es kann sein, dass wir Google Translate nutzen müssen, aber wir wollen, dass du dich sicher fühlst, uns zu kontaktieren, falls bei dir ein Problem auftreten sollte.",
"commGuideList02N": "<strong>Markiere und melde Nachrichten, in denen diese Richtlinien oder die Nutzungsbedingungen nicht eingehalten werden.</strong> Wir werden uns so schnell wie möglich darum kümmern. Alternativ kannst du Mitarbeiter:innen über <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> benachrichtigen, doch die Markierung ist der schnellste Weg, um Hilfe zu erhalten.",
"commGuideList02H": "<strong>Alle Anzeigenamen und @Benutzernamen müssen den Service-Bedingungen entsprechen</strong>. Um deinen Anzeigenamen und/oder @Benutzernamen zu ändern: wähle in der mobilen App Menü > Einstellungen > Account. Auf der Webseite navigierst du über das Benutzer-Icon in der oberen Navigationsleiste.",
"commGuideList02I": "<strong>Namen von Herausforderungen sollten für alle Bereiche angemessen sein, weil sie im öffentlichen Profil der Gewinner erscheinen</strong>. Behalte das in Erinnerung beim Erstellen von Herausforderungen, weil wir gezwungen sein könnten, den Eintrag in ihrem Profil zu ändern, falls es eine Meldung gibt.",
"commGuideList02P": "<strong>Wir raten davon ab, unaufgefordert private Nachrichten zu verschicken</strong>. Wenn du ungewollt eine Nachricht empfängst, die dir unangenehm ist, oder die gegen diese Richtlinien oder die Nutzungsbedingungen verstößt, sperre bitte den Absender und melde sie, um den Mitarbeiterstab darauf aufmerksam zu machen.",
+1 -10
View File
@@ -376,14 +376,5 @@
"hatchingPotionRoseGold": "Rotgold",
"hatchingPotionPinkMarble": "Pink Marmor",
"hatchingPotionTeaShop": "Teeladen",
"hatchingPotionFungi": "Fungus",
"questEggGiraffeMountText": "Giraffe",
"questEggChameleonText": "Chamäleon",
"questEggChameleonMountText": "Chamäleon",
"hatchingPotionKoi": "Koi",
"questEggGiraffeText": "Giraffe",
"questEggGiraffeAdjective": "eine riesengroße",
"questEggChameleonAdjective": "ein chaotisches",
"questEggCrabText": "Krabbe",
"questEggCrabMountText": "Krabbe"
"hatchingPotionFungi": "Fungus"
}

Some files were not shown because too many files have changed in this diff Show More