+
+
+
+
+
+
+ {{ $t('sortBy') }}
+
+
+
+
+
+
+
+ {{ category.text }}
+
+
+
+
+ {{ ctx.item.text }}
>
+
+
+
+
+
+
+
+
+
+
{{ items[0].set.text() }}
+
+
+
+ {{ ctx.item.text }}
>
+
+
+
+
+
+
+
+
+
+
+
{{ $t('purchaseAll') }}
+
+
{{ items[0].set.setPrice }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/website/client/src/components/shops/index.vue b/website/client/src/components/shops/index.vue
index d12a19c46e..953ebc5afb 100644
--- a/website/client/src/components/shops/index.vue
+++ b/website/client/src/components/shops/index.vue
@@ -26,6 +26,12 @@
>
{{ $t('titleTimeTravelers') }}
+
+ {{ $t('titleCustomizations') }}
+
diff --git a/website/client/src/router/index.js b/website/client/src/router/index.js
index f3e87d4584..7794721ee0 100644
--- a/website/client/src/router/index.js
+++ b/website/client/src/router/index.js
@@ -62,6 +62,7 @@ const MarketPage = () => import(/* webpackChunkName: "shops-market" */'@/compone
const QuestsPage = () => import(/* webpackChunkName: "shops-quest" */'@/components/shops/quests/index');
const SeasonalPage = () => import(/* webpackChunkName: "shops-seasonal" */'@/components/shops/seasonal/index');
const TimeTravelersPage = () => import(/* webpackChunkName: "shops-timetravelers" */'@/components/shops/timeTravelers/index');
+const CustomizationsShopPage = () => import(/* webpackChunkName: "shops-customizations" */'@/components/shops/customizations/index');
Vue.use(VueRouter);
@@ -113,6 +114,7 @@ const router = new VueRouter({
{ name: 'quests', path: 'quests', component: QuestsPage },
{ name: 'seasonal', path: 'seasonal', component: SeasonalPage },
{ name: 'time', path: 'time', component: TimeTravelersPage },
+ { name: 'customizations', path: 'customizations', component: CustomizationsShopPage },
],
},
{ name: 'party', path: '/party', component: GroupPage },
diff --git a/website/common/locales/en/character.json b/website/common/locales/en/character.json
index 4b6bdd7ecd..5a699b970e 100644
--- a/website/common/locales/en/character.json
+++ b/website/common/locales/en/character.json
@@ -34,6 +34,9 @@
"bodyFacialHair": "Facial Hair",
"beard": "Beard",
"mustache": "Mustache",
+ "titleFacialHair": "Facial Hair",
+ "titleHaircolor": "Hair Colors",
+ "titleHairbase": "Hair styles",
"flower": "Flower",
"accent": "Accent",
"headband": "Headband",
diff --git a/website/common/locales/en/generic.json b/website/common/locales/en/generic.json
index bfaa238727..3b156d53b0 100644
--- a/website/common/locales/en/generic.json
+++ b/website/common/locales/en/generic.json
@@ -8,6 +8,7 @@
"gotIt": "Got it!",
"titleTimeTravelers": "Time Travelers",
"titleSeasonalShop": "Seasonal Shop",
+ "titleCustomizations": "Customizations",
"saveEdits": "Save Edits",
"showMore": "Show More",
"showLess": "Show Less",
diff --git a/website/common/script/content/constants/index.js b/website/common/script/content/constants/index.js
index 358edb82b1..624285ec06 100644
--- a/website/common/script/content/constants/index.js
+++ b/website/common/script/content/constants/index.js
@@ -40,3 +40,4 @@ export { default as QUEST_PETS } from '../quests/pets';
export { default as QUEST_POTIONS } from '../quests/potions';
export { default as QUEST_TIME_TRAVEL } from '../quests/timeTravel';
export { default as QUEST_WORLD } from '../quests/world';
+export { getScheduleMatchingGroup, getCurrentGalaKey } from './schedule';
diff --git a/website/common/script/content/constants/schedule.js b/website/common/script/content/constants/schedule.js
index eed010bfc6..c5a0bbbafa 100644
--- a/website/common/script/content/constants/schedule.js
+++ b/website/common/script/content/constants/schedule.js
@@ -3,7 +3,6 @@ import SEASONAL_SETS from './seasonalSets';
function backgroundMatcher (month1, month2, oddYear) {
return function call (key) {
- if (!key.startsWith('backgrounds')) return true;
const keyLength = key.length;
const month = parseInt(key.substring(keyLength - 6, keyLength - 4), 10);
return (month === month1 || month === month2)
@@ -24,6 +23,26 @@ function inListMatcher (list) {
};
}
+const ALWAYS_AVAILABLE_CUSTOMIZATIONS = [
+ 'animalSkins',
+ 'rainbowSkins',
+ 'rainbowHairColors',
+ 'specialShirts',
+ 'facialHair',
+ 'baseHair1',
+ 'baseHair2',
+ 'baseHair3',
+];
+
+function customizationMatcher (list) {
+ return function call (item) {
+ if (ALWAYS_AVAILABLE_CUSTOMIZATIONS.indexOf(item) !== -1) {
+ return true;
+ }
+ return list.indexOf(item) !== -1;
+ };
+}
+
export const FIRST_RELEASE_DAY = 1;
export const SECOND_RELEASE_DAY = 7;
export const THIRD_RELEASE_DAY = 14;
@@ -601,6 +620,12 @@ export const MONTHLY_SCHEDULE = {
};
export const GALA_SWITCHOVER_DAY = 21;
+export const GALA_KEYS = [
+ 'winter',
+ 'spring',
+ 'summer',
+ 'fall',
+];
export const GALA_SCHEDULE = {
0: [
{
@@ -620,6 +645,13 @@ export const GALA_SCHEDULE = {
'evilsanta2',
],
},
+ {
+ type: 'customizations',
+ matcher: customizationMatcher([
+ 'winteryHairColors',
+ 'winterySkins',
+ ]),
+ },
],
1: [
{
@@ -632,20 +664,15 @@ export const GALA_SCHEDULE = {
'shinySeed',
],
},
+ {
+ type: 'customizations',
+ matcher: customizationMatcher([
+ 'shimmerHairColors',
+ 'pastelSkins',
+ ]),
+ },
],
2: [
- {
- type: 'seasonalGear',
- items: SEASONAL_SETS.fall,
- },
- {
- type: 'seasonalSpells',
- items: [
- 'spookySparkles',
- ],
- },
- ],
- 3: [
{
type: 'seasonalGear',
items: SEASONAL_SETS.summer,
@@ -656,9 +683,45 @@ export const GALA_SCHEDULE = {
'seafoam',
],
},
+ {
+ type: 'customizations',
+ matcher: customizationMatcher([
+ 'splashySkins',
+ ]),
+ },
+ ],
+ 3: [
+ {
+ type: 'seasonalGear',
+ items: SEASONAL_SETS.fall,
+ },
+ {
+ type: 'seasonalSpells',
+ items: [
+ 'spookySparkles',
+ ],
+ },
+ {
+ type: 'customizations',
+ matcher: customizationMatcher([
+ 'hauntedHairColors',
+ 'supernaturalSkins',
+ ]),
+ },
],
};
+function getGalaIndex (date) {
+ const month = date instanceof moment ? date.month() : date.getMonth();
+ const todayDay = date instanceof moment ? date.date() : date.getDate();
+ let galaMonth = month;
+ const galaCount = Object.keys(GALA_SCHEDULE).length;
+ if (todayDay >= GALA_SWITCHOVER_DAY) {
+ galaMonth += 1;
+ }
+ return parseInt((galaCount / 12) * galaMonth, 10);
+}
+
export function assembleScheduledMatchers (date) {
const items = [];
const month = date instanceof moment ? date.month() : date.getMonth();
@@ -674,12 +737,8 @@ export function assembleScheduledMatchers (date) {
items.push(...value);
}
}
- let galaMonth = month;
- const galaCount = Object.keys(GALA_SCHEDULE).length;
- if (todayDay >= GALA_SWITCHOVER_DAY) {
- galaMonth += 1;
- }
- items.push(...GALA_SCHEDULE[parseInt((galaCount / 12) * galaMonth, 10)]);
+
+ items.push(...GALA_SCHEDULE[getGalaIndex(date)]);
return items;
}
@@ -688,7 +747,7 @@ let cachedScheduleMatchers = null;
export function getScheduleMatchingGroup (type, date) {
if (!cachedScheduleMatchers) {
cachedScheduleMatchers = {};
- assembleScheduledMatchers(date !== undefined ? date : new Date()).forEach(matcher => {
+ assembleScheduledMatchers(date || new Date()).forEach(matcher => {
if (!cachedScheduleMatchers[matcher.type]) {
cachedScheduleMatchers[matcher.type] = {
matchers: [],
@@ -718,3 +777,7 @@ export function getScheduleMatchingGroup (type, date) {
}
return cachedScheduleMatchers[type];
}
+
+export function getCurrentGalaKey (date) {
+ return GALA_KEYS[getGalaIndex(date || new Date())];
+}
diff --git a/website/common/script/libs/getItemInfo.js b/website/common/script/libs/getItemInfo.js
index 59df678a01..ff9477b30c 100644
--- a/website/common/script/libs/getItemInfo.js
+++ b/website/common/script/libs/getItemInfo.js
@@ -379,6 +379,96 @@ export default function getItemInfo (user, type, item, officialPinnedItems, lang
};
break;
}
+ case 'haircolor': {
+ itemInfo = {
+ key: item.key,
+ class: `hair_bangs_${user.preferences.hair.bangs}_${item.key}`,
+ text: item.key,
+ notes: '',
+ value: item.price,
+ set: item.set,
+ locked: false,
+ currency: 'gems',
+ path: `customizations.hair.color.${item.key}`,
+ pinType: 'timeTravelersStable',
+ };
+ break;
+ }
+ case 'hairbase': {
+ itemInfo = {
+ key: item.key,
+ class: `hair_base_${item.key}_${user.preferences.hair.color}`,
+ text: item.key,
+ notes: '',
+ value: item.price,
+ set: item.set,
+ locked: false,
+ currency: 'gems',
+ path: `customizations.hair.base.${item.key}`,
+ pinType: 'timeTravelersStable',
+ };
+ break;
+ }
+ case 'hairmustache': {
+ itemInfo = {
+ key: item.key,
+ class: `hair_mustache_${item.key}_${user.preferences.hair.color}`,
+ text: item.key,
+ notes: '',
+ value: item.price,
+ set: item.set,
+ locked: false,
+ currency: 'gems',
+ path: `customizations.hair.mustache.${item.key}`,
+ pinType: 'timeTravelersStable',
+ };
+ break;
+ }
+ case 'hairbeard': {
+ itemInfo = {
+ key: item.key,
+ class: `hair_beard_${item.key}_${user.preferences.hair.color}`,
+ text: item.key,
+ notes: '',
+ value: item.price,
+ set: item.set,
+ locked: false,
+ currency: 'gems',
+ path: `customizations.hair.beard.${item.key}`,
+ pinType: 'timeTravelersStable',
+ };
+ break;
+ }
+ case 'shirt': {
+ itemInfo = {
+ key: item.key,
+ class: `${user.preferences.size}_shirt_${item.key}`,
+ text: item.key,
+ notes: '',
+ value: item.price,
+ set: item.set,
+ locked: false,
+ currency: 'gems',
+ path: `customizations.shirt.${item.key}`,
+ pinType: 'timeTravelersStable',
+ };
+ break;
+ }
+ case 'skin': {
+ itemInfo = {
+ key: item.key,
+ class: `skin_${item.key}`,
+ text: item.key,
+ notes: '',
+ value: item.price,
+ set: item.set,
+ locked: false,
+ currency: 'gems',
+ path: `customizations.skin.${item.key}`,
+ pinType: 'timeTravelersStable',
+ };
+ break;
+ }
}
if (itemInfo) {
diff --git a/website/common/script/libs/shops-seasonal.config.js b/website/common/script/libs/shops-seasonal.config.js
index 62ebde0958..d625dc601f 100644
--- a/website/common/script/libs/shops-seasonal.config.js
+++ b/website/common/script/libs/shops-seasonal.config.js
@@ -1,50 +1,25 @@
-import find from 'lodash/find';
import upperFirst from 'lodash/upperFirst';
-import moment from 'moment';
import {
- EVENTS,
- SEASONAL_SETS,
+ getCurrentGalaKey,
} from '../content/constants';
+import {
+ armor,
+} from '../content/gear/sets/special';
-const CURRENT_EVENT = find(EVENTS, event => moment().isBetween(event.start, event.end)
- && ['winter', 'spring', 'summer', 'fall'].includes(event.season));
+const CURRENT_EVENT_KEY = getCurrentGalaKey();
+
+function getCurrentSeasonalSets () {
+ const year = new Date().getFullYear();
+ return {
+ rogue: armor[`${CURRENT_EVENT_KEY}${year}Rogue`].set,
+ warrior: armor[`${CURRENT_EVENT_KEY}${year}Warrior`].set,
+ wizard: armor[`${CURRENT_EVENT_KEY}${year}Mage`].set,
+ healer: armor[`${CURRENT_EVENT_KEY}${year}Healer`].set,
+ };
+}
export default {
- opened: CURRENT_EVENT,
-
- currentSeason: CURRENT_EVENT ? upperFirst(CURRENT_EVENT.season) : 'Closed',
-
- dateRange: {
- start: CURRENT_EVENT ? moment(CURRENT_EVENT.start) : moment().subtract(1, 'days').toDate(),
- end: CURRENT_EVENT ? moment(CURRENT_EVENT.end) : moment().subtract(1, 'seconds').toDate(),
- },
-
- availableSets: CURRENT_EVENT
- ? [
- ...SEASONAL_SETS[CURRENT_EVENT.season],
- ]
- : [],
-
- pinnedSets: CURRENT_EVENT
- ? {
- rogue: 'spring2024MeltingSnowRogueSet',
- warrior: 'spring2024FluoriteWarriorSet',
- wizard: 'spring2024HibiscusMageSet',
- healer: 'spring2024BluebirdHealerSet',
- }
- : {},
-
- availableSpells: CURRENT_EVENT && moment().isBetween('2024-04-18T08:00-04:00', CURRENT_EVENT.end)
- ? [
- 'shinySeed',
- ]
- : [],
-
- availableQuests: CURRENT_EVENT && moment().isBetween('2024-03-26T08:00-04:00', CURRENT_EVENT.end)
- ? [
- 'egg',
- ]
- : [],
-
- featuredSet: 'spring2020LapisLazuliRogueSet',
+ currentSeason: CURRENT_EVENT_KEY ? upperFirst(CURRENT_EVENT_KEY) : 'Closed',
+ pinnedSets: getCurrentSeasonalSets(),
+ featuredSet: 'winter2019PoinsettiaSet',
};
diff --git a/website/common/script/libs/shops.js b/website/common/script/libs/shops.js
index d486d8e015..944ebf489d 100644
--- a/website/common/script/libs/shops.js
+++ b/website/common/script/libs/shops.js
@@ -449,8 +449,8 @@ shops.getSeasonalShop = function getSeasonalShop (user, language) {
identifier: 'seasonalShop',
text: i18n.t('seasonalShop'),
notes: i18n.t(`seasonalShop${seasonalShopConfig.currentSeason}Text`),
- imageName: seasonalShopConfig.opened ? 'seasonalshop_open' : 'seasonalshop_closed',
- opened: seasonalShopConfig.opened,
+ imageName: 'seasonalshop_open',
+ opened: true,
categories: this.getSeasonalShopCategories(user, language),
featured: {
text: i18n.t(seasonalShopConfig.featuredSet),
@@ -467,10 +467,6 @@ shops.getSeasonalShop = function getSeasonalShop (user, language) {
return resObject;
};
-// To switch seasons/available inventory, edit the AVAILABLE_SETS object to whatever should be sold.
-// let AVAILABLE_SETS = {
-// setKey: i18n.t('setTranslationString', language),
-// };
shops.getSeasonalShopCategories = function getSeasonalShopCategories (user, language) {
const officialPinnedItems = getOfficialPinnedItems(user);
@@ -553,4 +549,107 @@ shops.getBackgroundShopSets = function getBackgroundShopSets (language) {
return sets;
};
+/* Customization Shop */
+
+shops.getCustomizationShop = function getCustomizationShop (user, language) {
+ return {
+ identifier: 'customizationShop',
+ text: i18n.t('titleCustomizations'),
+ notes: i18n.t('timeTravelersPopover'),
+ imageName: 'npc_timetravelers_active',
+ categories: shops.getCustomizationShopCategories(user, language),
+ };
+};
+
+shops.getCustomizationShopCategories = function getCustomizationShopCategories (user, language) {
+ const categories = [];
+ const officialPinnedItems = getOfficialPinnedItems(user);
+
+ const backgroundCategory = {
+ identifier: 'backgrounds',
+ text: i18n.t('backgrounds', language),
+ items: [],
+ };
+
+ const matchers = getScheduleMatchingGroup('backgrounds');
+ eachRight(content.backgrounds, (group, key) => {
+ if (matchers.match(key)) {
+ each(group, bg => {
+ if (!user.purchased.background[bg.key]) {
+ const item = getItemInfo(
+ user,
+ 'background',
+ bg,
+ officialPinnedItems,
+ language,
+ );
+ backgroundCategory.items.push(item);
+ }
+ });
+ }
+ });
+ categories.push(backgroundCategory);
+
+ const facialHairCategory = {
+ identifier: 'facialHair',
+ text: i18n.t('titleFacialHair', language),
+ items: [],
+ };
+ const customizationMatcher = getScheduleMatchingGroup('customizations');
+ each(['color', 'base', 'mustache', 'beard'], hairType => {
+ let category;
+ if (hairType === 'beard' || hairType === 'mustache') {
+ category = facialHairCategory;
+ } else {
+ category = {
+ identifier: hairType,
+ text: i18n.t(`titleHair${hairType}`, language),
+ items: [],
+ };
+ }
+ eachRight(content.appearances.hair[hairType], (hairStyle, key) => {
+ if (hairStyle.price > 0 && (!user.purchased.hair || !user.purchased.hair[hairType]
+ || !user.purchased.hair[hairType][key])
+ && customizationMatcher.match(hairStyle.set.key)) {
+ const item = getItemInfo(
+ user,
+ `hair${hairType}`,
+ hairStyle,
+ officialPinnedItems,
+ language,
+ );
+ category.items.push(item);
+ }
+ });
+ // only add the facial hair category once
+ if (hairType !== 'beard') {
+ categories.push(category);
+ }
+ });
+
+ each(['shirt', 'skin'], type => {
+ const category = {
+ identifier: type,
+ text: i18n.t(type, language),
+ items: [],
+ };
+ eachRight(content.appearances[type], (appearance, key) => {
+ if (appearance.price > 0 && (!user.purchased[type] || !user.purchased[type][key])
+ && customizationMatcher.match(appearance.set.key)) {
+ const item = getItemInfo(
+ user,
+ type,
+ appearance,
+ officialPinnedItems,
+ language,
+ );
+ category.items.push(item);
+ }
+ });
+ categories.push(category);
+ });
+
+ return categories;
+};
+
export default shops;
diff --git a/website/server/controllers/api-v3/shops.js b/website/server/controllers/api-v3/shops.js
index efaaff2395..349abd83df 100644
--- a/website/server/controllers/api-v3/shops.js
+++ b/website/server/controllers/api-v3/shops.js
@@ -144,4 +144,26 @@ api.getBackgroundShopItems = {
},
};
+/**
+ * @apiIgnore
+ * @api {get} /api/v3/shops/customizations get the available items for the backgrounds shop
+ * @apiName GetCustomizationShopItems
+ * @apiGroup Shops
+ *
+ * @apiSuccess {Object} data List of available backgrounds
+ * @apiSuccess {string} message Success message
+ */
+api.getBackgroundShopItems = {
+ method: 'GET',
+ url: '/shops/customizations',
+ middlewares: [authWithHeaders()],
+ async handler (req, res) {
+ const { user } = res.locals;
+
+ const resObject = shops.getCustomizationShop(user, req.language);
+
+ res.respond(200, resObject);
+ },
+};
+
export default api;