diff --git a/test/client/unit/specs/components/inventory/drawer.js b/test/client/unit/specs/components/ui/drawer.js similarity index 87% rename from test/client/unit/specs/components/inventory/drawer.js rename to test/client/unit/specs/components/ui/drawer.js index b76b76718f..fa252b84d7 100644 --- a/test/client/unit/specs/components/inventory/drawer.js +++ b/test/client/unit/specs/components/ui/drawer.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import DrawerComponent from 'client/components/inventory/drawer.vue'; +import DrawerComponent from 'client/components/ui/drawer.vue'; describe('DrawerComponent', () => { it('sets the correct default data', () => { diff --git a/test/common/ops/sell.js b/test/common/ops/sell.js index da62c6c776..11c9676c03 100644 --- a/test/common/ops/sell.js +++ b/test/common/ops/sell.js @@ -65,6 +65,16 @@ describe('shared.ops.sell', () => { } }); + it('returns an error when the requested amount is above the available amount', (done) => { + try { + sell(user, {params: { type, key }, query: {amount: 2} }); + } catch (err) { + expect(err).to.be.an.instanceof(NotFound); + expect(err.message).to.equal(i18n.t('userItemsNotEnough', {type})); + done(); + } + }); + it('reduces item count from user', () => { sell(user, {params: { type, key } }); diff --git a/website/client/assets/images/market/market_banner_web_alexnpc.png b/website/client/assets/images/market/market_banner_web_alexnpc.png new file mode 100644 index 0000000000..38121f7a88 Binary files /dev/null and b/website/client/assets/images/market/market_banner_web_alexnpc.png differ diff --git a/website/client/assets/images/market/shop_background.png b/website/client/assets/images/market/shop_background.png new file mode 100644 index 0000000000..d88617d9dd Binary files /dev/null and b/website/client/assets/images/market/shop_background.png differ diff --git a/website/client/assets/scss/banner.scss b/website/client/assets/scss/banner.scss new file mode 100644 index 0000000000..622c247062 --- /dev/null +++ b/website/client/assets/scss/banner.scss @@ -0,0 +1,36 @@ + +@import '~client/assets/scss/colors.scss'; + +.featured-label { + width: auto; + height: 28px; + border-radius: 2px; + background-color: #b36213; + display: inline-flex; + align-items: center; + justify-content: center; + + &.with-border { + border: solid 2px $yellow-10; + height: 32px; + } + + .rectangle { + margin: 9px; + display: inline-block; + width: 6px; + height: 6px; + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); + background-color: $yellow-100; + border: solid 2px $yellow-10; + } + + .text { + font-size: 12px; + font-weight: bold; + line-height: 1.33; + color: $white; + flex: 1; + } +} diff --git a/website/client/assets/scss/dropdown.scss b/website/client/assets/scss/dropdown.scss index 4fa36b5473..6923bddfd7 100644 --- a/website/client/assets/scss/dropdown.scss +++ b/website/client/assets/scss/dropdown.scss @@ -59,3 +59,14 @@ margin-right: 20px; margin-left: 20px; } + +.dropdown-icon-item { + .svg-icon { + margin: 0px 16px 0px 0px; + vertical-align: middle; + } + + .text { + vertical-align: middle; + } +} diff --git a/website/client/assets/scss/icon.scss b/website/client/assets/scss/icon.scss index 63c2d18f6f..933f032963 100644 --- a/website/client/assets/scss/icon.scss +++ b/website/client/assets/scss/icon.scss @@ -1,9 +1,9 @@ .svg-icon { display: block; stroke-width: 0; + transition: none !important; stroke: currentColor; fill: currentColor; - transition: none !important; svg { display: block; @@ -12,6 +12,13 @@ * { transition: none !important; } + + &.color { + svg path { + stroke: currentColor; + fill: currentColor; + } + } } .icon-16 { diff --git a/website/client/assets/scss/index.scss b/website/client/assets/scss/index.scss index 4a60ede1b3..492048832a 100644 --- a/website/client/assets/scss/index.scss +++ b/website/client/assets/scss/index.scss @@ -25,3 +25,4 @@ @import './task'; @import './categories'; @import './dragdrop'; +@import './banner'; diff --git a/website/client/assets/scss/item.scss b/website/client/assets/scss/item.scss index 63f4babe4e..f880f096a5 100644 --- a/website/client/assets/scss/item.scss +++ b/website/client/assets/scss/item.scss @@ -48,6 +48,10 @@ } } +.flat .item { + box-shadow: none; +} + .drawer-content .item:hover { border-color: transparent; box-shadow: none; diff --git a/website/client/assets/scss/modal.scss b/website/client/assets/scss/modal.scss new file mode 100644 index 0000000000..acfd201f42 --- /dev/null +++ b/website/client/assets/scss/modal.scss @@ -0,0 +1,36 @@ + +@import '~client/assets/scss/colors.scss'; + + +@mixin centeredModal() { + display: flex; + justify-content: center; + flex-direction: column; + + header, footer { + border: 0; + } +} + +.modal-dialog { + .title { + height: 24px; + margin-top: 24px; + font-family: 'Roboto Condensed'; + font-size: 20px; + font-weight: bold; + line-height: 1.2; + text-align: center; + color: $gray-50; + } + + .text { + height: 60px; + font-family: Roboto; + font-size: 14px; + line-height: 1.43; + text-align: center; + color: $gray-100; + } + +} diff --git a/website/client/assets/svg/clock.svg b/website/client/assets/svg/clock.svg new file mode 100644 index 0000000000..5f8c2a41b5 --- /dev/null +++ b/website/client/assets/svg/clock.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/website/client/assets/svg/lock.svg b/website/client/assets/svg/lock.svg new file mode 100644 index 0000000000..0ac53170bd --- /dev/null +++ b/website/client/assets/svg/lock.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/pin.svg b/website/client/assets/svg/pin.svg new file mode 100644 index 0000000000..2e0bf00699 --- /dev/null +++ b/website/client/assets/svg/pin.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/components/appMenu.vue b/website/client/components/appMenu.vue index e9a4b32059..400cba4e70 100644 --- a/website/client/components/appMenu.vue +++ b/website/client/components/appMenu.vue @@ -14,8 +14,13 @@ div router-link.dropdown-item(:to="{name: 'items'}", exact) {{ $t('items') }} router-link.dropdown-item(:to="{name: 'equipment'}") {{ $t('equipment') }} router-link.dropdown-item(:to="{name: 'stable'}") {{ $t('stable') }} - router-link.nav-item(tag="li", :to="{name: 'shops'}", exact) + router-link.nav-item.dropdown(tag="li", :to="{name: 'market'}", :class="{'active': $route.path.startsWith('/shop')}") a.nav-link(v-once) {{ $t('shops') }} + .dropdown-menu + router-link.dropdown-item(:to="{name: 'market'}", exact) {{ $t('market') }} + router-link.dropdown-item(:to="{name: 'quests'}") {{ $t('quests') }} + router-link.dropdown-item(:to="{name: 'seasonal'}") {{ $t('titleSeasonalShop') }} + router-link.dropdown-item(:to="{name: 'time'}") {{ $t('titleTimeTravelers') }} router-link.nav-item(tag="li", :to="{name: 'party'}") a.nav-link(v-once) {{ $t('party') }} router-link.nav-item.dropdown(tag="li", :to="{name: 'tavern'}", :class="{'active': $route.path.startsWith('/guilds')}") diff --git a/website/client/components/avatar.vue b/website/client/components/avatar.vue index 165323d739..cf32d38e9e 100644 --- a/website/client/components/avatar.vue +++ b/website/client/components/avatar.vue @@ -16,21 +16,21 @@ // Show avatar only if not currently affected by visual buff template(v-if!="!member.stats.buffs.snowball && !member.stats.buffs.spookySparkles && !member.stats.buffs.shinySeed && !member.stats.buffs.seafoam") span(:class="'chair_' + member.preferences.chair") - span(:class="member.items.gear[costumeClass].back") + span(:class="getGearClass('back')") span(:class="skinClass") span.head_0 span(:class="member.preferences.size + '_shirt_' + member.preferences.shirt") - span(:class="member.preferences.size + '_' + member.items.gear[costumeClass].armor") - span(:class="member.items.gear[costumeClass].back_collar") - span(:class="member.items.gear[costumeClass].body") + span(:class="member.preferences.size + '_' + getGearClass('armor')") + span(:class="getGearClass('back_collar')") + span(:class="getGearClass('body')") template(v-for="type in ['base', 'bangs', 'mustache', 'beard']") span(:class="'hair_' + type + '_' + member.preferences.hair[type] + '_' + member.preferences.hair.color") - span(:class="member.items.gear[costumeClass].eyewear") - span(:class="member.items.gear[costumeClass].head") - span(:class="member.items.gear[costumeClass].headAccessory") + span(:class="getGearClass('eyewear')") + span(:class="getGearClass('head')") + span(:class="getGearClass('headAccessory')") span(:class="'hair_flower_' + member.preferences.hair.flower") - span(:class="member.items.gear[costumeClass].shield") - span(:class="member.items.gear[costumeClass].weapon") + span(:class="getGearClass('shield')") + span(:class="getGearClass('weapon')") // Resting span.zzz(v-if="member.preferences.sleep") @@ -105,6 +105,12 @@ export default { type: Boolean, default: false, }, + withBackground: { + type: Boolean, + }, + overrideAvatarGear: { + type: Object, + }, width: { type: Number, default: 140, @@ -144,7 +150,9 @@ export default { backgroundClass () { let background = this.member.preferences.background; - if (background && !this.avatarOnly) { + let allowToShowBackground = !this.avatarOnly || this.withBackground; + + if (background && allowToShowBackground) { return `background_${this.member.preferences.background}`; } @@ -167,5 +175,16 @@ export default { return this.member.preferences.costume ? 'costume' : 'equipped'; }, }, + methods: { + getGearClass (gearType) { + let result = this.member.items.gear[this.costumeClass][gearType]; + + if (this.overrideAvatarGear && this.overrideAvatarGear[gearType]) { + result = this.overrideAvatarGear[gearType]; + } + + return result; + }, + }, }; diff --git a/website/client/components/inventory/equipment/index.vue b/website/client/components/inventory/equipment/index.vue index f5abed5302..5a7e2c42ec 100644 --- a/website/client/components/inventory/equipment/index.vue +++ b/website/client/components/inventory/equipment/index.vue @@ -134,8 +134,8 @@ import toggleSwitch from 'client/components/ui/toggleSwitch'; import Item from 'client/components/inventory/item'; import EquipmentAttributesPopover from 'client/components/inventory/equipment/attributesPopover'; -import StarBadge from 'client/components/inventory/starBadge'; -import Drawer from 'client/components/inventory/drawer'; +import StarBadge from 'client/components/ui/starBadge'; +import Drawer from 'client/components/ui/drawer'; import i18n from 'common/script/i18n'; diff --git a/website/client/components/inventory/item.vue b/website/client/components/inventory/item.vue index 6a55ac07eb..ce7b8fd005 100644 --- a/website/client/components/inventory/item.vue +++ b/website/client/components/inventory/item.vue @@ -6,7 +6,7 @@ div(v-if="emptyItem") span.item-label(v-if="label") {{ label }} b-popover( v-else, - :triggers="['hover']", + :triggers="[showPopover?'hover':'']", :placement="popoverPosition", ) span(slot="content") @@ -46,6 +46,10 @@ export default { type: String, default: 'bottom', }, + showPopover: { + type: Boolean, + default: true, + }, }, methods: { click () { diff --git a/website/client/components/inventory/stable/foodItem.vue b/website/client/components/inventory/stable/foodItem.vue index 2a63bd37e9..7521065649 100644 --- a/website/client/components/inventory/stable/foodItem.vue +++ b/website/client/components/inventory/stable/foodItem.vue @@ -25,7 +25,7 @@ b-popover( import bPopover from 'bootstrap-vue/lib/components/popover'; import DragDropDirective from 'client/directives/dragdrop.directive'; -import CountBadge from './countBadge'; +import CountBadge from 'client/components/ui/countBadge'; export default { components: { diff --git a/website/client/components/inventory/stable/index.vue b/website/client/components/inventory/stable/index.vue index 3846d06d05..7e369b9407 100644 --- a/website/client/components/inventory/stable/index.vue +++ b/website/client/components/inventory/stable/index.vue @@ -248,6 +248,7 @@ + + diff --git a/website/client/components/shops/market/buyModal.vue b/website/client/components/shops/market/buyModal.vue new file mode 100644 index 0000000000..6002385f72 --- /dev/null +++ b/website/client/components/shops/market/buyModal.vue @@ -0,0 +1,195 @@ + + + + diff --git a/website/client/components/shops/market/equipmentAttributesGrid.vue b/website/client/components/shops/market/equipmentAttributesGrid.vue new file mode 100644 index 0000000000..3e645d7041 --- /dev/null +++ b/website/client/components/shops/market/equipmentAttributesGrid.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/website/client/components/shops/market/index.vue b/website/client/components/shops/market/index.vue new file mode 100644 index 0000000000..e54d98b65e --- /dev/null +++ b/website/client/components/shops/market/index.vue @@ -0,0 +1,667 @@ + + + + + + diff --git a/website/client/components/shops/market/sellModal.vue b/website/client/components/shops/market/sellModal.vue new file mode 100644 index 0000000000..88b2e4ee01 --- /dev/null +++ b/website/client/components/shops/market/sellModal.vue @@ -0,0 +1,184 @@ + + + + diff --git a/website/client/components/shops/shopItem.vue b/website/client/components/shops/shopItem.vue new file mode 100644 index 0000000000..53619cc94b --- /dev/null +++ b/website/client/components/shops/shopItem.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/website/client/components/inventory/stable/countBadge.vue b/website/client/components/ui/countBadge.vue similarity index 100% rename from website/client/components/inventory/stable/countBadge.vue rename to website/client/components/ui/countBadge.vue diff --git a/website/client/components/inventory/drawer.vue b/website/client/components/ui/drawer.vue similarity index 96% rename from website/client/components/inventory/drawer.vue rename to website/client/components/ui/drawer.vue index d82b1be2dc..9602ab93e6 100644 --- a/website/client/components/inventory/drawer.vue +++ b/website/client/components/ui/drawer.vue @@ -2,7 +2,7 @@ .drawer-container .drawer-title(@click="open = !open") | {{title}} - .drawer-toggle-icon.svg-icon(v-html="open ? icons.minimize : icons.expand", :class="{ closed: !open }") + .drawer-toggle-icon.svg-icon.icon-10(v-html="open ? icons.minimize : icons.expand", :class="{ closed: !open }") transition(name="slide-up", @afterLeave="adjustPagePadding", @afterEnter="adjustPagePadding") .drawer-content(v-show="open") slot(name="drawer-header") @@ -36,6 +36,7 @@ .drawer-toggle-icon { float: right; margin-right: 16px; + margin-top: 16px; &.closed { margin-top: 3px; diff --git a/website/client/components/ui/drawerHeaderTabs.vue b/website/client/components/ui/drawerHeaderTabs.vue new file mode 100644 index 0000000000..4952b112f6 --- /dev/null +++ b/website/client/components/ui/drawerHeaderTabs.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/website/client/components/inventory/stable/drawerSlider.vue b/website/client/components/ui/drawerSlider.vue similarity index 100% rename from website/client/components/inventory/stable/drawerSlider.vue rename to website/client/components/ui/drawerSlider.vue diff --git a/website/client/components/ui/itemRows.vue b/website/client/components/ui/itemRows.vue new file mode 100644 index 0000000000..b09adb0ef6 --- /dev/null +++ b/website/client/components/ui/itemRows.vue @@ -0,0 +1,71 @@ + + + diff --git a/website/client/components/inventory/starBadge.vue b/website/client/components/ui/starBadge.vue similarity index 100% rename from website/client/components/inventory/starBadge.vue rename to website/client/components/ui/starBadge.vue diff --git a/website/client/router.js b/website/client/router.js index b528e0d071..9d9e8737ea 100644 --- a/website/client/router.js +++ b/website/client/router.js @@ -84,6 +84,10 @@ const MyChallenges = () => import(/* webpackChunkName: "challenges" */ './compon const FindChallenges = () => import(/* webpackChunkName: "challenges" */ './components/challenges/findChallenges'); const ChallengeDetail = () => import(/* webpackChunkName: "challenges" */ './components/challenges/challengeDetail'); +// Shops +const ShopsContainer = () => import(/* webpackChunkName: "shops" */'./components/shops/index'); +const MarketPage = () => import(/* webpackChunkName: "shops-market" */'./components/shops/market/index'); + Vue.use(VueRouter); const router = new VueRouter({ @@ -111,7 +115,16 @@ const router = new VueRouter({ { name: 'stable', path: 'stable', component: StablePage }, ], }, - { name: 'shops', path: '/shops', component: Page }, + { + path: '/shops', + component: ShopsContainer, + children: [ + { name: 'market', path: 'market', component: MarketPage }, + { name: 'quests', path: 'quests', component: Page }, + { name: 'seasonal', path: 'seasonal', component: Page }, + { name: 'time', path: 'time', component: Page }, + ], + }, { name: 'party', path: '/party', component: GuildPage }, { name: 'groupPlan', path: '/group-plans', component: GroupPlansAppPage }, { diff --git a/website/client/store/actions/index.js b/website/client/store/actions/index.js index 4d43b4330f..e6e515a128 100644 --- a/website/client/store/actions/index.js +++ b/website/client/store/actions/index.js @@ -13,6 +13,7 @@ import * as chat from './chat'; import * as notifications from './notifications'; import * as tags from './tags'; import * as hall from './hall'; +import * as shops from './shops'; // Actions should be named as 'actionName' and can be accessed as 'namespace:actionName' // Example: fetch in user.js -> 'user:fetch' @@ -31,6 +32,7 @@ const actions = flattenAndNamespace({ notifications, tags, hall, + shops, }); export default actions; diff --git a/website/client/store/actions/shops.js b/website/client/store/actions/shops.js new file mode 100644 index 0000000000..88ed46efe6 --- /dev/null +++ b/website/client/store/actions/shops.js @@ -0,0 +1,53 @@ +import axios from 'axios'; +import { loadAsyncResource } from 'client/libs/asyncResource'; +import buyOp from 'common/script/ops/buy'; +import sellOp from 'common/script/ops/sell'; + +export function fetch (store, forceLoad = false) { // eslint-disable-line no-shadow + return loadAsyncResource({ + store, + path: 'shops.market', + url: '/api/v3/shops/market', + deserialize (response) { + return response.data.data; + }, + forceLoad, + }); +} + +export function buyItem (store, params) { + const user = store.state.user.data; + buyOp(user, {params}); + axios + .post(`/api/v3/user/buy/${params.key}`); + // TODO + // .then((res) => console.log('equip', res)) + // .catch((err) => console.error('equip', err)); +} + +export function sellItems (store, params) { + const user = store.state.user.data; + sellOp(user, {params, query: {amount: params.amount}}); + axios + .post(`/api/v3/user/sell/${params.type}/${params.key}?amount=${params.amount}`); + // TODO + // .then((res) => console.log('equip', res)) + // .catch((err) => console.error('equip', err)); +} + + +export function pinGear (store, params) { + //axios + // .post(`/api/v3/user/pin/${params.key}`); + // TODO + // .then((res) => console.log('equip', res)) + // .catch((err) => console.error('equip', err)); +} + +export function unpinGear (store, params) { + //axios + // .post(`/api/v3/user/unpin/${params.key}`); + // TODO + // .then((res) => console.log('equip', res)) + // .catch((err) => console.error('equip', err)); +} diff --git a/website/client/store/getters/index.js b/website/client/store/getters/index.js index 1a8eeec476..af2400ae94 100644 --- a/website/client/store/getters/index.js +++ b/website/client/store/getters/index.js @@ -1,5 +1,6 @@ import { flattenAndNamespace } from 'client/libs/store/helpers/internals'; import * as user from './user'; +import * as shops from './shops'; import * as tasks from './tasks'; import * as party from './party'; import * as members from './members'; @@ -12,6 +13,7 @@ const getters = flattenAndNamespace({ tasks, party, members, + shops, }); export default getters; diff --git a/website/client/store/getters/shops.js b/website/client/store/getters/shops.js new file mode 100644 index 0000000000..8564ee3b50 --- /dev/null +++ b/website/client/store/getters/shops.js @@ -0,0 +1,3 @@ +export function market (store) { + return store.state.shops.market; +} diff --git a/website/client/store/index.js b/website/client/store/index.js index 4643f22600..71f9c760cc 100644 --- a/website/client/store/index.js +++ b/website/client/store/index.js @@ -43,6 +43,9 @@ export default function () { quest: {}, members: asyncResourceFactory(), }, + shops: { + market: asyncResourceFactory(), + }, myGuilds: [], editingGroup: {}, // TODO move to local state // content data, frozen to prevent Vue from modifying it since it's static and never changes diff --git a/website/common/locales/en/newClient.json b/website/common/locales/en/newClient.json index 27673de57e..3700ede724 100644 --- a/website/common/locales/en/newClient.json +++ b/website/common/locales/en/newClient.json @@ -43,6 +43,7 @@ "sortByColor": "Color", "sortByHatchable": "Hatchable", "hatch": "Hatch!", + "foodTitle": "Food", "dragThisFood": "Drag this <%= foodName %> to a Pet and watch it grow!", "clickOnPetToFeed": "Click on a Pet to feed <%= foodName %> and watch it grow!", "dragThisPotion": "Drag this <%= potionName %> to an Egg and hatch a new pet!", @@ -110,7 +111,6 @@ "wantToJoinPartyTitle": "Want to join a Party?", "wantToJoinPartyDescription": "Aenean non mattis eros, quis semper ipsum. Phasellus vulputate in nibh et suscipit. In hac habitasse platea dictumst.", "copy": "Copy", - "lookingForGroup": "Looking for Group", "inviteToPartyOrQuest": "Invite Party to Quest", "inviteInformation": "Clicking “Invite” will send an invitation to your party members. When all members have accepted or denied, the Quest begins.", "questOwnerRewards": "Quest Owner Rewards", @@ -167,7 +167,6 @@ "helpfulLinks": "Helpful Links", "communityGuidelinesLink": "Community Guidelines", "lookingForGroup": "Looking for Group (Party Wanted) Posts", - "faq": "FAQ", "dataDisplayTool": "Data Display Tool", "reportProblem": "Report a Problem", "requestFeature": "Request a Feature", @@ -213,5 +212,21 @@ "challengeInformationPlaceHolder": "Write a short description advertising your Challenge to other Habiticans. What is the main purpose of your Challenge and why should people join it? Try to include useful keywords in the description so that Habiticans can easily find it when they search!", "where": "Where*", "challengeMinimum": "Minimum 1 Gem for public Challenges (helps prevent spam, it really does).", - "group": "Group" + "group": "Group", + + "sortByType": "Type", + "sortByPrice": "Price", + "sortByCon": "Con", + "sortByPer": "Per", + "sortByStr": "Str", + "sortByInt": "Int", + "classEquipment": "Class Equipment", + "showAllEquipment": "Show All <%= classType %> Equipment", + "showLessEquipment": "Show Less <%= classType %> Equipment", + "howManyToSell": "How many would you like to sell?", + "yourBalance": "Your balance", + "sell": "Sell", + "buyNow": "Buy Now", + "sortByNumber": "Number", + "featuredItems": "Featured Items!" } diff --git a/website/common/locales/en/npc.json b/website/common/locales/en/npc.json index 0fdd68e323..595b3d47e2 100644 --- a/website/common/locales/en/npc.json +++ b/website/common/locales/en/npc.json @@ -41,6 +41,7 @@ "plusOneGem": "+1 Gem", "typeNotSellable": "Type is not sellable. Must be one of the following <%= acceptedTypes %>", "userItemsKeyNotFound": "Key not found for user.items <%= type %>", + "userItemsNotEnough": "Not enough items found for user.items <%= type %>", "pathRequired": "Path string is required", "unlocked": "Items have been unlocked", "alreadyUnlocked": "Full set already unlocked.", diff --git a/website/common/script/content/shop-featuredItems.js b/website/common/script/content/shop-featuredItems.js new file mode 100644 index 0000000000..0b05de4bc5 --- /dev/null +++ b/website/common/script/content/shop-featuredItems.js @@ -0,0 +1,10 @@ +const featuredItems = { + market: [ + 'head_armoire_vikingHelm', + 'weapon_special_1', + 'shield_special_0', + 'armor_warrior_5', + ], +}; + +export default featuredItems; diff --git a/website/common/script/ops/sell.js b/website/common/script/ops/sell.js index 9a2f3db403..5c754cd521 100644 --- a/website/common/script/ops/sell.js +++ b/website/common/script/ops/sell.js @@ -14,6 +14,7 @@ const ACCEPTEDTYPES = ['eggs', 'hatchingPotions', 'food']; module.exports = function sell (user, req = {}) { let key = get(req.params, 'key'); let type = get(req.params, 'type'); + let amount = get(req.query, 'amount', 1); if (!type) { throw new BadRequest(i18n.t('typeRequired', req.language)); @@ -31,8 +32,14 @@ module.exports = function sell (user, req = {}) { throw new NotFound(i18n.t('userItemsKeyNotFound', {type}, req.language)); } - user.items[type][key]--; - user.stats.gp += content[type][key].value; + let currentAmount = user.items[type][key]; + + if (amount > currentAmount) { + throw new NotFound(i18n.t('userItemsNotEnough', {type}, req.language)); + } + + user.items[type][key] -= amount; + user.stats.gp += content[type][key].value * amount; return [ pick(user, splitWhitespace('stats items')), diff --git a/website/server/controllers/api-v3/user.js b/website/server/controllers/api-v3/user.js index ee61b8a08b..cf66e8c1fd 100644 --- a/website/server/controllers/api-v3/user.js +++ b/website/server/controllers/api-v3/user.js @@ -1490,12 +1490,13 @@ api.userReleaseMounts = { }; /** - * @api {post} /api/v3/user/sell/:type/:key Sell a gold-sellable item owned by the user + * @api {post} /api/v3/user/sell/:type/:key?amount=1 Sell a gold-sellable item owned by the user * @apiName UserSell * @apiGroup User * - * @apiParam {String="eggs","hatchingPotions","food"} type The type of item to sell. - * @apiParam {String} key The key of the item + * @apiParam (Path) {String="eggs","hatchingPotions","food"} type The type of item to sell. + * @apiParam (Path) {String} key The key of the item + * @apiParam (Query) {Number} (optional) amount The amount to sell * * @apiSuccess {Object} data.stats * @apiSuccess {Object} data.items