diff --git a/website/assets/sprites/spritesmith/misc/inventory_quest_scroll.png b/website/assets/sprites/spritesmith/misc/inventory_quest_scroll.png deleted file mode 100644 index 553ab4e8e3..0000000000 Binary files a/website/assets/sprites/spritesmith/misc/inventory_quest_scroll.png and /dev/null differ diff --git a/website/assets/sprites/spritesmith/misc/inventory_quest_scroll_locked.png b/website/assets/sprites/spritesmith/misc/inventory_quest_scroll_locked.png deleted file mode 100644 index 00b9480e65..0000000000 Binary files a/website/assets/sprites/spritesmith/misc/inventory_quest_scroll_locked.png and /dev/null differ diff --git a/website/client/assets/scss/badge.scss b/website/client/assets/scss/badge.scss index 629cb72011..4994ce8083 100644 --- a/website/client/assets/scss/badge.scss +++ b/website/client/assets/scss/badge.scss @@ -20,10 +20,3 @@ position: absolute; top: -9px; } - -.badge-quantity { - color: $white; - right: -9px; - padding: 4.5px 8.5px; - background-color: $orange-100; -} \ No newline at end of file diff --git a/website/client/components/inventory/items/index.vue b/website/client/components/inventory/items/index.vue index 6e8724f540..bc9c75e61b 100644 --- a/website/client/components/inventory/items/index.vue +++ b/website/client/components/inventory/items/index.vue @@ -32,7 +32,7 @@ h2 | {{ $t(group.key) }} | - span.badge.badge-pill.badge-default {{group.quantity}} + span.badge.badge-pill.badge-default(v-if="group.key != 'special'") {{group.quantity}} itemRows( @@ -47,7 +47,7 @@ item( :item="context.item", :key="context.item.key", - :itemContentClass="`${group.classPrefix}${context.item.key}`", + :itemContentClass="context.item.class", :highlightBorder="isHatchable(currentDraggingPotion, context.item.key)", v-drag.drop.hatch="context.item.key", @@ -61,7 +61,10 @@ h4.popover-content-title {{ context.item.text }} .popover-content-text(v-if="currentDraggingPotion == null") {{ context.item.notes }} template(slot="itemBadge", scope="context") - span.badge.badge-pill.badge-item.badge-quantity {{ context.item.quantity }} + countBadge( + :show="true", + :count="context.item.quantity" + ) itemRows( v-else-if="group.key === 'hatchingPotions'", @@ -75,7 +78,7 @@ item( :item="context.item", :key="context.item.key", - :itemContentClass="`${group.classPrefix}${context.item.key}`", + :itemContentClass="context.item.class", :showPopover="currentDraggingPotion == null", :active="currentDraggingPotion == context.item", v-drag.hatch="context.item.key", @@ -89,7 +92,10 @@ h4.popover-content-title {{ context.item.text }} .popover-content-text {{ context.item.notes }} template(slot="itemBadge", scope="context") - span.badge.badge-pill.badge-item.badge-quantity {{ context.item.quantity }} + countBadge( + :show="true", + :count="context.item.quantity" + ) itemRows( v-else, @@ -103,14 +109,18 @@ item( :item="context.item", :key="context.item.key", - :itemContentClass="`${group.classPrefix}${context.item.key}`", - :showPopover="currentDraggingPotion == null" + :itemContentClass="context.item.class", + :showPopover="currentDraggingPotion == null", + @click="itemClicked(group.key, context.item)", ) template(slot="popoverContent", scope="context") h4.popover-content-title {{ context.item.text }} .popover-content-text {{ context.item.notes }} template(slot="itemBadge", scope="context") - span.badge.badge-pill.badge-item.badge-quantity {{ context.item.quantity }} + countBadge( + :show="true", + :count="context.item.quantity" + ) hatchedPetDialog( :pet="hatchedPet", @@ -162,14 +172,16 @@ import bDropdown from 'bootstrap-vue/lib/components/dropdown'; import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item'; import Item from 'client/components/inventory/item'; import ItemRows from 'client/components/ui/itemRows'; +import CountBadge from 'client/components/ui/countBadge'; import HatchedPetDialog from '../stable/hatchedPetDialog'; import createAnimal from 'client/libs/createAnimal'; +import moment from 'moment'; const allowedSpecialItems = ['snowball', 'spookySparkles', 'shinySeed', 'seafoam']; - +import notifications from 'client/mixins/notifications'; import DragDropDirective from 'client/directives/dragdrop.directive'; import MouseMoveDirective from 'client/directives/mouseposition.directive'; @@ -191,6 +203,7 @@ const groups = [ let lastMouseMoveEvent = {}; export default { + mixins: [notifications], name: 'Items', components: { Item, @@ -198,6 +211,7 @@ export default { bDropdown, bDropdownItem, HatchedPetDialog, + CountBadge, }, directives: { drag: DragDropDirective, @@ -243,6 +257,7 @@ export default { if (isSearched) { itemsArray.push({ ...item, + class: `${group.classPrefix}${item.key}`, text: item.text(), notes: item.notes(), quantity: itemQuantity, @@ -262,6 +277,30 @@ export default { }); }); + let specialArray = []; + + if (this.user.purchased.plan.mysteryItems.length) { + specialArray.push({ + key: 'mysteryItem', + class: `inventory_present inventory_present_${moment().format('MM')}`, + text: this.$t('subscriberItemText'), + quantity: this.user.purchased.plan.mysteryItems.length, + }); + } + + if (this.user.purchased.plan.consecutive.trinkets) { + specialArray.push({ + key: 'timeTravelers', + class: 'inventory_special_trinket', + text: this.$t('mysticHourglassPopover'), + quantity: this.user.purchased.plan.consecutive.trinkets, + }); + } + + if (specialArray.length > 0) { + itemsByType.special = specialArray; + } + return itemsByType; }, }, @@ -347,6 +386,19 @@ export default { this.hatchedPet = null; }, + async itemClicked (groupKey, item) { + if (groupKey === 'special') { + if (item.key === 'timeTravelers') { + this.$router.push({name: 'time'}); + } else if (item.key === 'mysteryItem') { + let result = await this.$store.dispatch('user:openMysteryItem'); + + let openedItem = result.data.data; + let text = this.content.gear.flat[openedItem.key].text(); + this.drop(this.$t('messageDropMysteryItem', {dropText: text}), openedItem); + } + } + }, mouseMoved ($event) { if (this.potionClickMode) { diff --git a/website/client/components/shops/buyModal.vue b/website/client/components/shops/buyModal.vue index c61382914f..228e0644f2 100644 --- a/website/client/components/shops/buyModal.vue +++ b/website/client/components/shops/buyModal.vue @@ -167,9 +167,10 @@ import BalanceInfo from './balanceInfo.vue'; import currencyMixin from './_currencyMixin'; + import notifications from 'client/mixins/notifications'; export default { - mixins: [currencyMixin], + mixins: [currencyMixin, notifications], components: { bModal, BalanceInfo, @@ -206,6 +207,16 @@ this.$emit('change', $event); }, buyItem () { + if (this.genericPurchase) { + this.$store.dispatch('shops:genericPurchase', { + pinType: this.item.pinType, + type: this.item.purchaseType, + key: this.item.key, + currency: this.item.currency, + }); + this.purchased(this.item.text); + } + this.$emit('buyPressed', this.item); this.hideDialog(); }, @@ -236,6 +247,10 @@ withPin: { type: Boolean, }, + genericPurchase: { + type: Boolean, + default: true, + }, }, }; diff --git a/website/client/components/shops/market/index.vue b/website/client/components/shops/market/index.vue index 62105f5d58..480bdae4d6 100644 --- a/website/client/components/shops/market/index.vue +++ b/website/client/components/shops/market/index.vue @@ -150,9 +150,12 @@ span(slot="popoverContent") h4.popover-content-title {{ item.text }} + template(slot="itemImage", scope="scope") + span.svg-icon.inline.icon-48(v-if="scope.item.key == 'gem'", v-html="icons.gem") + template(slot="itemBadge", scope="ctx") countBadge( - v-if="item.purchaseType !== 'card'", + v-if="item.showCount != false", :show="userItems[item.purchaseType][item.key] != 0", :count="userItems[item.purchaseType][item.key] || 0" ) @@ -230,8 +233,7 @@ priceType="gold", :withPin="true", @change="resetGearToBuy($event)", - @buyPressed="buyGear($event)", - @togglePinned="togglePinned($event)" + @togglePinned="togglePinned($event)", ) template(slot="item", scope="ctx") div @@ -250,14 +252,16 @@ :item="selectedItemToBuy", :priceType="selectedItemToBuy ? selectedItemToBuy.currency : ''", @change="resetItemToBuy($event)", - @buyPressed="buyItem($event)", - @togglePinned="togglePinned($event)" + @togglePinned="togglePinned($event)", + @buyPressed="purchaseCallback($event)", + :genericPurchase="selectedItemToBuy != null && selectedItemToBuy.key != 'rebirth_orb'" ) template(slot="item", scope="ctx") item.flat.bordered-item( :item="ctx.item", :itemContentClass="ctx.item.class", - :showPopover="false" + :showPopover="false", + v-if="ctx.item.key != 'gem'" ) selectMembersModal( @@ -306,6 +310,10 @@ width: 12px; height: 12px; } + .icon-48 { + width: 48px; + height: 48px; + } .hand-cursor { cursor: pointer; @@ -409,6 +417,7 @@ import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item'; import svgPin from 'assets/svg/pin.svg'; + import svgGem from 'assets/svg/gem.svg'; import svgInformation from 'assets/svg/information.svg'; import svgWarrior from 'assets/svg/warrior.svg'; import svgWizard from 'assets/svg/wizard.svg'; @@ -475,6 +484,7 @@ export default { icons: Object.freeze({ pin: svgPin, + gem: svgGem, information: svgInformation, warrior: svgWarrior, wizard: svgWizard, @@ -528,9 +538,50 @@ export default { items: _map(_filter(this.content.cardTypes, (value) => { return value.yearRound; }), (value) => { - return getItemInfo(this.user, 'card', value); + return { + ...getItemInfo(this.user, 'card', value), + showCount: false, + }; }), }); + + let specialItems = []; + + if (this.user.purchased.plan.customerId) { + specialItems.push({ + showCount: false, + key: 'gem', + class: 'gem', + pinKey: 'gems', + purchaseType: 'gems', + text: this.$t('subGemName'), + notes: this.$t('subGemPop'), + currency: 'gold', + value: 20, + }); + } + + if (this.user.flags.rebirthEnabled) { + specialItems.push({ + showCount: false, + key: 'rebirth_orb', + class: 'rebirth_orb', + purchaseType: 'rebirth_orb', + text: this.$t('rebirthName'), + notes: this.$t('rebirthPop'), + currency: 'gems', + value: this.user.stats.lvl < 100 ? 6 : '', + }); + } + + if (specialItems.length > 0) { + categories.push({ + identifier: 'special', + text: this.$t('special'), + items: specialItems, + }); + } + categories.map((category) => { this.$set(this.viewOptions, category.identifier, { selected: true, @@ -655,7 +706,9 @@ export default { return !this.userItems.gear.owned[gear.key]; }); - result = _sortBy(result, [sortGearTypeMap[sortBy]]); + // first all unlocked + // then the selected sort + result = _sortBy(result, [(item) => item.locked, sortGearTypeMap[sortBy]]); return result; }, @@ -735,12 +788,6 @@ export default { this.$parent.showUnpinNotification(item); } }, - buyGear (item) { - this.$store.dispatch('shops:buyItem', {key: item.key}); - }, - buyItem (item) { - this.$store.dispatch('shops:purchase', {type: item.purchaseType, key: item.key}); - }, itemSelected (item) { if (item.purchaseType === 'card') { if (this.user.party._id) { @@ -761,6 +808,12 @@ export default { this.$store.dispatch('user:castSpell', {key: this.selectedCardToBuy.key, targetId: member.id}); this.selectedCardToBuy = null; }, + async purchaseCallback (item) { + if (item.key === 'rebirth_orb') { + await this.$store.dispatch('user:rebirth'); + window.location.reload(true); + } + }, }, created () { this.selectedGroupGearByClass = this.userStats.class; diff --git a/website/client/components/shops/quests/buyQuestModal.vue b/website/client/components/shops/quests/buyQuestModal.vue index 4f0720b306..9da142755c 100644 --- a/website/client/components/shops/quests/buyQuestModal.vue +++ b/website/client/components/shops/quests/buyQuestModal.vue @@ -235,9 +235,10 @@ import BalanceInfo from '../balanceInfo.vue'; import currencyMixin from '../_currencyMixin'; import QuestInfo from './questInfo.vue'; + import notifications from 'client/mixins/notifications'; export default { - mixins: [currencyMixin], + mixins: [currencyMixin, notifications], components: { bModal, BalanceInfo, @@ -278,6 +279,13 @@ this.$emit('change', $event); }, buyItem () { + this.$store.dispatch('shops:genericPurchase', { + pinType: this.item.pinType, + type: this.item.purchaseType, + key: this.item.key, + currency: this.item.currency, + }); + this.purchased(this.item.text); this.$emit('buyPressed', this.item); this.hideDialog(); }, diff --git a/website/client/components/shops/quests/index.vue b/website/client/components/shops/quests/index.vue index 5546d83c74..91378d6458 100644 --- a/website/client/components/shops/quests/index.vue +++ b/website/client/components/shops/quests/index.vue @@ -183,7 +183,6 @@ :priceType="selectedItemToBuy ? selectedItemToBuy.currency : ''", :withPin="true", @change="resetItemToBuy($event)", - @buyPressed="buyItem($event)", @togglePinned="togglePinned($event)" ) template(slot="item", scope="ctx") @@ -477,9 +476,6 @@ export default { this.$parent.showUnpinNotification(item); } }, - buyItem (item) { - this.$store.dispatch('shops:purchase', {type: item.purchaseType, key: item.key}); - }, }, created () { this.$store.dispatch('shops:fetchQuests'); diff --git a/website/client/components/shops/seasonal/index.vue b/website/client/components/shops/seasonal/index.vue index 16d5c9c014..654974f909 100644 --- a/website/client/components/shops/seasonal/index.vue +++ b/website/client/components/shops/seasonal/index.vue @@ -112,7 +112,6 @@ :priceType="selectedItemToBuy ? selectedItemToBuy.currency : ''", :withPin="true", @change="resetItemToBuy($event)", - @buyPressed="buyItem($event)", @togglePinned="togglePinned($event)" ) template(slot="item", scope="ctx") @@ -491,9 +490,6 @@ this.$parent.showUnpinNotification(item); } }, - buyItem (item) { - this.$store.dispatch('shops:purchase', {type: item.purchaseType, key: item.key}); - }, }, created () { this.$store.dispatch('shops:fetchSeasonal'); diff --git a/website/client/components/shops/shopItem.vue b/website/client/components/shops/shopItem.vue index ae4c8b6910..c2fd1ef53c 100644 --- a/website/client/components/shops/shopItem.vue +++ b/website/client/components/shops/shopItem.vue @@ -23,6 +23,7 @@ b-popover( div.image div(:class="item.class", v-once) + slot(name="itemImage", :item="item") div.price span.svg-icon.inline.icon-16(v-html="icons[currencyClass]") diff --git a/website/client/components/ui/countBadge.vue b/website/client/components/ui/countBadge.vue index 9d05cf0637..8a003fea41 100644 --- a/website/client/components/ui/countBadge.vue +++ b/website/client/components/ui/countBadge.vue @@ -11,7 +11,7 @@ span.badge.badge-pill.badge-item.badge-count( right: -9px; color: $white; background: $gray-200; - padding: 4.5px 6px; + padding: 4.5px 8.5px; min-width: 24px; height: 24px; box-shadow: 0 1px 1px 0 rgba($black, 0.12); diff --git a/website/client/mixins/notifications.js b/website/client/mixins/notifications.js index 2d438e30e1..3d0f0fef2d 100644 --- a/website/client/mixins/notifications.js +++ b/website/client/mixins/notifications.js @@ -86,6 +86,11 @@ export default { mp (val) { this.notify(`${this.sign(val)} ${this.round(val)}`, 'mp', 'glyphicon glyphicon-fire', this.sign(val)); }, + purchased (itemName) { + this.notify(this.$t('purchasedItem', { + itemName, + })); + }, streak (val) { this.notify(`${this.$t('streaks')}: ${val}`, 'streak', 'glyphicon glyphicon-repeat'); }, diff --git a/website/client/store/actions/shops.js b/website/client/store/actions/shops.js index 26615f0feb..5d7bb832d1 100644 --- a/website/client/store/actions/shops.js +++ b/website/client/store/actions/shops.js @@ -1,10 +1,12 @@ import axios from 'axios'; import { loadAsyncResource } from 'client/libs/asyncResource'; import buyOp from 'common/script/ops/buy'; +import buyQuestOp from 'common/script/ops/buyQuest'; import purchaseOp from 'common/script/ops/purchaseWithSpell'; import buyMysterySetOp from 'common/script/ops/buyMysterySet'; import hourglassPurchaseOp from 'common/script/ops/hourglassPurchase'; import sellOp from 'common/script/ops/sell'; +import unlockOp from 'common/script/ops/unlock'; export function fetchMarket (store, forceLoad = false) { // eslint-disable-line no-shadow return loadAsyncResource({ @@ -57,44 +59,86 @@ export function fetchTimeTravelers (store, forceLoad = false) { // eslint-disabl 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)); + let opResult = buyOp(user, {params}); + + return { + result: opResult, + httpCall: axios.post(`/api/v3/user/buy/${params.key}`), + }; +} + +export function buyQuestItem (store, params) { + const user = store.state.user.data; + let opResult = buyQuestOp(user, {params}); + + return { + result: opResult, + httpCall: axios.post(`/api/v3/user/buy-quest/${params.key}`), + }; } export function purchase (store, params) { const user = store.state.user.data; - purchaseOp(user, {params}); - axios - .post(`/api/v3/user/purchase/${params.type}/${params.key}`); - // TODO - // .then((res) => console.log('equip', res)) - // .catch((err) => console.error('equip', err)); + let opResult = purchaseOp(user, {params}); + + return { + result: opResult, + httpCall: axios.post(`/api/v3/user/purchase/${params.type}/${params.key}`), + }; } export function purchaseMysterySet (store, params) { const user = store.state.user.data; - buyMysterySetOp(user, {params}); - axios - .post(`/api/v3/user/buy-mystery-set/${params.key}`); - // TODO - // .then((res) => console.log('equip', res)) - // .catch((err) => console.error('equip', err)); + let opResult = buyMysterySetOp(user, {params}); + + return { + result: opResult, + httpCall: axios.post(`/api/v3/user/buy-mystery-set/${params.key}`), + }; } export function purchaseHourglassItem (store, params) { const user = store.state.user.data; - hourglassPurchaseOp(user, {params}); - axios - .post(`/api/v3/user/purchase-hourglass/${params.type}/${params.key}`); - // TODO - // .then((res) => console.log('equip', res)) - // .catch((err) => console.error('equip', err)); + let opResult = hourglassPurchaseOp(user, {params}); + + return { + result: opResult, + httpCall: axios.post(`/api/v3/user/purchase-hourglass/${params.type}/${params.key}`), + }; } +export function unlock (store, params) { + const user = store.state.user.data; + let opResult = unlockOp(user, params); + + return { + result: opResult, + httpCall: axios.post(`/api/v3/user/unlock?path=${params.query.path}`), + }; +} + +export function genericPurchase (store, params) { + switch (params.pinType) { + case 'mystery_set': + return purchaseMysterySet(store, params); + case 'potion': + case 'armoire': + case 'marketGear': + return buyItem(store, params); + case 'background': + return unlock(store, { + query: { + path: `background.${params.key}`, + }, + }); + default: + if (params.pinType === 'quests' && params.currency === 'gold') { + return buyQuestItem(store, params); + } else { + return purchase(store, params); + } + } +} export function sellItems (store, params) { const user = store.state.user.data; diff --git a/website/client/store/actions/user.js b/website/client/store/actions/user.js index 4fcebfb71f..742187766e 100644 --- a/website/client/store/actions/user.js +++ b/website/client/store/actions/user.js @@ -96,3 +96,11 @@ export function castSpell (store, params) { return axios.post(spellUrl); } + +export function openMysteryItem () { + return axios.post('/api/v3/user/open-mystery-item'); +} + +export function rebirth () { + return axios.post('/api/v3/user/rebirth'); +} diff --git a/website/common/locales/en/newClient.json b/website/common/locales/en/newClient.json index dbb8ad68f3..430b198dda 100644 --- a/website/common/locales/en/newClient.json +++ b/website/common/locales/en/newClient.json @@ -303,5 +303,6 @@ "health_wellness": "Health & Wellness", "self_care": "Self-Care", "sendLink": "Send Link", - "forgotPassword": "Forgot Password" + "forgotPassword": "Forgot Password", + "purchasedItem": "You bought <%= itemName %>" } diff --git a/website/common/script/content/appearance/backgrounds.js b/website/common/script/content/appearance/backgrounds.js index cfa226410f..07c72047ff 100644 --- a/website/common/script/content/appearance/backgrounds.js +++ b/website/common/script/content/appearance/backgrounds.js @@ -584,12 +584,20 @@ let backgrounds = { }; /* eslint-enable quote-props */ +let flat = {}; + forOwn(backgrounds, function prefillBackgroundSet (backgroundsInSet, set) { forOwn(backgroundsInSet, function prefillBackground (background, bgKey) { background.key = bgKey; background.set = set; background.price = 7; + + flat[bgKey] = background; }); }); -module.exports = backgrounds; +module.exports = { + tree: backgrounds, + flat, +}; + diff --git a/website/common/script/content/index.js b/website/common/script/content/index.js index 0d00d2d01f..f3073ebcb4 100644 --- a/website/common/script/content/index.js +++ b/website/common/script/content/index.js @@ -24,7 +24,7 @@ import { } from './quests'; import appearances from './appearance'; -import backgrounds from './appearance/backgrounds.js'; +import backgrounds from './appearance/backgrounds'; import spells from './spells'; import subscriptionBlocks from './subscriptionBlocks'; import faq from './faq'; @@ -523,7 +523,8 @@ each(api.food, (food, key) => { api.appearances = appearances; -api.backgrounds = backgrounds; +api.backgrounds = backgrounds.tree; +api.backgroundsFlat = backgrounds.flat; api.userDefaults = { habits: [ diff --git a/website/common/script/libs/getItemInfo.js b/website/common/script/libs/getItemInfo.js index 31935111ca..02b1649fd1 100644 --- a/website/common/script/libs/getItemInfo.js +++ b/website/common/script/libs/getItemInfo.js @@ -137,7 +137,7 @@ module.exports = function getItemInfo (user, type, item, language = 'en') { }; }) : undefined, lvl: item.lvl, - class: locked ? `inventory_quest_scroll_locked inventory_quest_scroll_${item.key}_locked` : `inventory_quest_scroll inventory_quest_scroll_${item.key}`, + class: locked ? `inventory_quest_scroll_${item.key}_locked` : `inventory_quest_scroll_${item.key}`, purchaseType: 'quests', path: `quests.${item.key}`, pinType: 'quests', diff --git a/website/common/script/ops/unlock.js b/website/common/script/ops/unlock.js index 24eeb1f31d..dbbb01352e 100644 --- a/website/common/script/ops/unlock.js +++ b/website/common/script/ops/unlock.js @@ -9,6 +9,10 @@ import { BadRequest, } from '../libs/errors'; +import { removeItemByPath } from './pinnedGearUtils'; +import getItemInfo from '../libs/getItemInfo'; +import content from '../content/index'; + // If item is already purchased -> equip it // Otherwise unlock it module.exports = function unlock (user, req = {}, analytics) { @@ -75,10 +79,11 @@ module.exports = function unlock (user, req = {}, analytics) { setWith(user, `purchased.${pathPart}`, true, Object); }); } else { + let split = path.split('.'); + let value = split.pop(); + let key = split.join('.'); + if (alreadyOwns) { // eslint-disable-line no-lonely-if - let split = path.split('.'); - let value = split.pop(); - let key = split.join('.'); if (key === 'background' && value === user.preferences.background) { value = ''; } @@ -88,6 +93,10 @@ module.exports = function unlock (user, req = {}, analytics) { } else { // Using Object so path[1] won't create an array but an object {path: {1: value}} setWith(user, `purchased.${path}`, true, Object); + + let backgroundContent = content.backgroundsFlat[value]; + let itemInfo = getItemInfo(user, 'background', backgroundContent); + removeItemByPath(user, itemInfo.path); } }