1 ///////////////////////////////////////////////////////////////////////////////
2 // Copyright (C) 2004-2011 by The Allacrost Project
3 // Copyright (C) 2012-2016 by Bertram (Valyria Tear)
4 // All Rights Reserved
5 //
6 // This code is licensed under the GNU GPL version 2. It is free software
7 // and you may modify it and/or redistribute it under the terms of this license.
8 // See https://www.gnu.org/copyleft/gpl.html for details.
9 ///////////////////////////////////////////////////////////////////////////////
10
11 /** ****************************************************************************
12 *** \file shop_buy.cpp
13 *** \author Tyler Olsen, roots@allacrost.org
14 *** \author Yohann Ferreira, yohann ferreira orange fr
15 *** \brief Source file for buy interface of shop mode
16 ***
17 *** \note The contents of this file are near identical to the contents of
18 *** shop_sell.cpp. When making any changes to this file, please look to shop_sell.cpp
19 *** to see if it should have similar changes made.
20 *** ***************************************************************************/
21
22 #include "shop_buy.h"
23
24 #include "shop.h"
25
26 #include "engine/audio/audio.h"
27 #include "engine/input.h"
28 #include "engine/system.h"
29 #include "engine/video/video.h"
30
31 #include "common/global/global.h"
32
33 using namespace vt_utils;
34 using namespace vt_audio;
35 using namespace vt_input;
36 using namespace vt_system;
37 using namespace vt_video;
38 using namespace vt_gui;
39 using namespace vt_global;
40
41 namespace vt_shop
42 {
43
44 namespace private_shop
45 {
46
47 // *****************************************************************************
48 // ***** BuyInterface class methods
49 // *****************************************************************************
50
BuyInterface()51 BuyInterface::BuyInterface() :
52 _view_mode(SHOP_VIEW_MODE_INVALID),
53 _selected_object(nullptr),
54 _buy_deal_types(0),
55 _number_categories(0),
56 _current_category(0)
57 {
58 _category_header.SetStyle(TextStyle("title24"));
59 _category_header.SetText(UTranslate("Category"));
60
61 _name_header.SetStyle(TextStyle("title24"));
62 _name_header.SetText(UTranslate("Name"));
63
64 _properties_header.SetDimensions(300.0f, 30.0f, 3, 1, 3, 1);
65 _properties_header.SetOptionAlignment(VIDEO_X_RIGHT, VIDEO_Y_CENTER);
66 _properties_header.SetTextStyle(TextStyle("title24"));
67 _properties_header.SetCursorState(VIDEO_CURSOR_STATE_HIDDEN);
68 _properties_header.AddOption(UTranslate("Price"));
69 _properties_header.AddOption(UTranslate("Stock"));
70 _properties_header.AddOption(UTranslate("Own"));
71
72 _selected_name.SetStyle(TextStyle("text22"));
73
74 _selected_properties.SetOwner(ShopMode::CurrentInstance()->GetBottomWindow());
75 _selected_properties.SetPosition(480.0f, 70.0f);
76 _selected_properties.SetDimensions(300.0f, 30.0f, 3, 1, 3, 1);
77 _selected_properties.SetOptionAlignment(VIDEO_X_RIGHT, VIDEO_Y_CENTER);
78 _selected_properties.SetTextStyle(TextStyle("text22"));
79 _selected_properties.SetCursorState(VIDEO_CURSOR_STATE_HIDDEN);
80 _selected_properties.AddOption(ustring());
81 _selected_properties.AddOption(ustring());
82 _selected_properties.AddOption(ustring());
83 }
84
85
~BuyInterface()86 BuyInterface::~BuyInterface()
87 {
88 for(uint32_t i = 0; i < _list_displays.size(); i++) {
89 delete _list_displays[i];
90 }
91 }
92
93
_UpdateAvailableBuyDealTypes()94 void BuyInterface::_UpdateAvailableBuyDealTypes()
95 {
96 _buy_deal_types = 0;
97
98 // Determine what types of objects the shop deals in based on the managed object list
99 std::map<uint32_t, ShopObject *>* buy_objects = ShopMode::CurrentInstance()->GetAvailableBuy();
100 for(std::map<uint32_t, ShopObject *>::iterator it = buy_objects->begin(); it != buy_objects->end(); ++it) {
101 vt_global::GLOBAL_OBJECT object_type = it->second->GetObject()->GetObjectType();
102 switch(object_type) {
103 case GLOBAL_OBJECT_ITEM:
104 _buy_deal_types |= DEALS_ITEMS;
105 break;
106 case GLOBAL_OBJECT_WEAPON:
107 _buy_deal_types |= DEALS_WEAPONS;
108 break;
109 case GLOBAL_OBJECT_HEAD_ARMOR:
110 _buy_deal_types |= DEALS_HEAD_ARMOR;
111 break;
112 case GLOBAL_OBJECT_TORSO_ARMOR:
113 _buy_deal_types |= DEALS_TORSO_ARMOR;
114 break;
115 case GLOBAL_OBJECT_ARM_ARMOR:
116 _buy_deal_types |= DEALS_ARM_ARMOR;
117 break;
118 case GLOBAL_OBJECT_LEG_ARMOR:
119 _buy_deal_types |= DEALS_LEG_ARMOR;
120 break;
121 case GLOBAL_OBJECT_SPIRIT:
122 _buy_deal_types |= DEALS_SPIRIT;
123 break;
124 default:
125 IF_PRINT_WARNING(SHOP_DEBUG) << "unknown object type sold in shop: " << object_type << std::endl;
126 break;
127 }
128 // Test whether this is a key item
129 if (it->second->GetObject()->IsKeyItem())
130 _buy_deal_types |= DEALS_KEY_ITEMS;
131 }
132 }
133
134
_RefreshItemCategories()135 void BuyInterface::_RefreshItemCategories()
136 {
137 // Clear the data
138 _category_icons.clear();
139 _category_names.clear();
140 ShopMedia *shop_media = ShopMode::CurrentInstance()->Media();
141 std::vector<ustring>* all_category_names = shop_media->GetAllCategoryNames();
142 std::vector<StillImage>* all_category_icons = GlobalManager->Media().GetAllItemCategoryIcons();
143
144 // Determine which categories are used in this shop and populate the true containers with that data
145 _UpdateAvailableBuyDealTypes();
146
147 uint8_t bit_x = 0x01; // Used to do a bit-by-bit analysis of the obj_types variable
148 for(uint8_t i = 0; i < GLOBAL_OBJECT_TOTAL; i++, bit_x <<= 1) {
149 // Check whether the type is available by doing a bit-wise comparison
150 if(_buy_deal_types & bit_x) {
151 _category_names.push_back(all_category_names->at(i));
152 _category_icons.push_back(&all_category_icons->at(i));
153 }
154 }
155
156 // If here is more than one category, add the text/icon for all wares
157 if(_category_names.size() > 1) {
158 _category_names.push_back(all_category_names->at(8));
159 _category_icons.push_back(&all_category_icons->at(8));
160 }
161
162 _number_categories = _category_names.size();
163 }
164
165
Reinitialize()166 void BuyInterface::Reinitialize()
167 {
168 _RefreshItemCategories();
169
170 // Set the initial category to the last category that was added (this is usually "All Wares")
171 _current_category = _number_categories > 0 ? _number_categories - 1 : 0;
172 // Initialize the category display with the initial category
173 if(_number_categories > 0)
174 _category_display.ChangeCategory(_category_names[_current_category], _category_icons[_current_category]);
175
176 // Prepare object data containers and determine category index mappings
177 // Containers of object data used to populate the display lists
178 std::vector<std::vector<ShopObject *> > object_data;
179
180 for(uint32_t i = 0; i < _number_categories; ++i) {
181 object_data.push_back(std::vector<ShopObject *>());
182 }
183
184 // Holds the index to the _object_data vector where the container for a specific object type is located
185 std::vector<uint32_t> type_index(GLOBAL_OBJECT_TOTAL + 1, 0); // + key items
186 // Used to set the appropriate data in the type_index vector
187 uint32_t next_index = 0;
188 // Used to do a bit-by-bit analysis of the deal_types variable
189 uint8_t bit_x = 0x01;
190
191 // This loop determines where each type of object should be placed in the object_data container. For example,
192 // if the available categories in the shop are items, weapons, spirits, and all wares, the size of object_data
193 // will be four. When we go to add an object of one of these types into the object_data container, we need
194 // to know the correct index for each type of object. These indeces are stored in the type_index vector. The
195 // size of this vector is the number of object types, so it becomes simple to map each object type to its correct
196 // location in object_data.
197 for(uint8_t i = 0; i < GLOBAL_OBJECT_TOTAL; i++, bit_x <<= 1) {
198 // Check if the type is available by doing a bit-wise comparison
199 if(_buy_deal_types & bit_x) {
200 type_index[i] = next_index++;
201 }
202 }
203
204 // Populate the object_data containers
205
206 // Pointer to the container of all objects that are bought/sold/traded in the shop
207 std::map<uint32_t, ShopObject *>* buy_objects = ShopMode::CurrentInstance()->GetAvailableBuy();
208
209 for(std::map<uint32_t, ShopObject *>::iterator it = buy_objects->begin(); it != buy_objects->end(); ++it) {
210 ShopObject* obj = it->second;
211 switch(obj->GetObject()->GetObjectType()) {
212 case GLOBAL_OBJECT_ITEM:
213 object_data[type_index[0]].push_back(obj);
214 break;
215 case GLOBAL_OBJECT_WEAPON:
216 object_data[type_index[1]].push_back(obj);
217 break;
218 case GLOBAL_OBJECT_HEAD_ARMOR:
219 object_data[type_index[2]].push_back(obj);
220 break;
221 case GLOBAL_OBJECT_TORSO_ARMOR:
222 object_data[type_index[3]].push_back(obj);
223 break;
224 case GLOBAL_OBJECT_ARM_ARMOR:
225 object_data[type_index[4]].push_back(obj);
226 break;
227 case GLOBAL_OBJECT_LEG_ARMOR:
228 object_data[type_index[5]].push_back(obj);
229 break;
230 case GLOBAL_OBJECT_SPIRIT:
231 object_data[type_index[6]].push_back(obj);
232 break;
233 default:
234 IF_PRINT_WARNING(SHOP_DEBUG) << "added object of unknown type: " << obj->GetObject()->GetObjectType() << std::endl;
235 break;
236 }
237
238 // Test whether this is a key item
239 if (it->second->GetObject()->IsKeyItem())
240 object_data[type_index[7]].push_back(obj);
241
242 // If there is an "All Wares" category, make sure the object gets added there as well
243 if(_number_categories > 1) {
244 object_data.back().push_back(obj);
245 }
246 }
247
248 // Create the buy displays using the object data that is now ready
249 for(uint32_t i = 0; i < _list_displays.size(); ++i) {
250 delete _list_displays[i];
251 }
252 _list_displays.clear();
253
254 for(uint32_t i = 0; i < object_data.size(); ++i) {
255 BuyListDisplay *new_list = new BuyListDisplay();
256 new_list->PopulateList(object_data[i]);
257 _list_displays.push_back(new_list);
258 }
259
260 if(_number_categories > 0)
261 _selected_object = _list_displays[_current_category]->GetSelectedObject();
262 ChangeViewMode(SHOP_VIEW_MODE_LIST);
263 }
264
MakeActive()265 void BuyInterface::MakeActive()
266 {
267 Reinitialize();
268
269 if(_list_displays.empty()) {
270 ShopMode::CurrentInstance()->ChangeState(SHOP_STATE_ROOT);
271 return;
272 }
273
274 _selected_object = _list_displays[_current_category]->GetSelectedObject();
275 ShopMode::CurrentInstance()->ObjectViewer()->ChangeViewMode(_view_mode);
276 ShopMode::CurrentInstance()->ObjectViewer()->SetSelectedObject(_selected_object);
277 _category_display.ChangeViewMode(_view_mode);
278 _category_display.SetSelectedObject(_selected_object);
279 }
280
TransactionNotification()281 void BuyInterface::TransactionNotification()
282 {
283 Reinitialize();
284
285 for(uint32_t i = 0; i < _list_displays.size(); ++i) {
286 _list_displays[i]->ReconstructList();
287 _list_displays[i]->ResetSelection();
288 }
289
290 _current_category = _number_categories > 0 ? _number_categories - 1 : 0;
291 _view_mode = SHOP_VIEW_MODE_LIST;
292 }
293
Update()294 void BuyInterface::Update()
295 {
296 ShopMode* shop = ShopMode::CurrentInstance();
297
298 if(_view_mode == SHOP_VIEW_MODE_LIST && shop->IsInputEnabled()) {
299 if(InputManager->ConfirmPress()) {
300 ChangeViewMode(SHOP_VIEW_MODE_INFO);
301 } else if(InputManager->CancelPress()) {
302 shop->ChangeState(SHOP_STATE_ROOT);
303 GlobalManager->Media().PlaySound("cancel");
304 }
305
306 // Swap cycles through the object categories
307 else if(InputManager->MenuPress() && (_number_categories > 1)) {
308 if(_ChangeCategory(true))
309 shop->ObjectViewer()->SetSelectedObject(_selected_object);
310 GlobalManager->Media().PlaySound("confirm");
311 }
312
313 // Up/down changes the selected object in the current list
314 else if(InputManager->UpPress()) {
315 if(_ChangeSelection(false)) {
316 shop->ObjectViewer()->SetSelectedObject(_selected_object);
317 GlobalManager->Media().PlaySound("bump");
318 }
319 } else if(InputManager->DownPress()) {
320 if(_ChangeSelection(true)) {
321 shop->ObjectViewer()->SetSelectedObject(_selected_object);
322 GlobalManager->Media().PlaySound("bump");
323 }
324 }
325 } // if (_view_mode == SHOP_VIEW_MODE_LIST)
326
327 else if(_view_mode == SHOP_VIEW_MODE_INFO && shop->IsInputEnabled()) {
328 if(InputManager->ConfirmPress()) {
329 ChangeViewMode(SHOP_VIEW_MODE_LIST);
330 shop->ChangeState(SHOP_STATE_ROOT);
331 if (shop->CompleteTransaction()) {
332 GlobalManager->Media().PlaySound("coins");
333 }
334 else {
335 GlobalManager->Media().PlaySound("cancel");
336 }
337 shop->ClearOrder();
338 shop->ChangeState(SHOP_STATE_SELL); //If the entire sell list is emptied, this somehow helps
339 shop->ChangeState(SHOP_STATE_BUY);
340 } else if(InputManager->CancelPress()) {
341 ChangeViewMode(SHOP_VIEW_MODE_LIST);
342 while(_list_displays[_current_category]->ChangeBuyQuantity(false)) {}
343 GlobalManager->Media().PlaySound("cancel");
344 shop->ClearOrder();
345 }
346
347 // Left/right change the quantity of the object to buy
348 else if(InputManager->LeftPress()) {
349 if(_list_displays[_current_category]->ChangeBuyQuantity(false)) {
350 shop->ObjectViewer()->UpdateCountText();
351 GlobalManager->Media().PlaySound("confirm");
352 } else
353 GlobalManager->Media().PlaySound("bump");
354 } else if(InputManager->RightPress()) {
355 if(_list_displays[_current_category]->ChangeBuyQuantity(true)) {
356 shop->ObjectViewer()->UpdateCountText();
357 GlobalManager->Media().PlaySound("confirm");
358 } else
359 GlobalManager->Media().PlaySound("bump");
360 }
361 }
362
363 _category_display.Update();
364 _list_displays[_current_category]->Update();
365 shop->ObjectViewer()->Update();
366 }
367
Draw()368 void BuyInterface::Draw()
369 {
370 if(_view_mode == SHOP_VIEW_MODE_LIST) {
371 VideoManager->SetDrawFlags(VIDEO_X_CENTER, VIDEO_Y_BOTTOM, 0);
372 VideoManager->Move(200.0f, 210.0f);
373 _category_header.Draw();
374
375 VideoManager->SetDrawFlags(VIDEO_X_LEFT, 0);
376 VideoManager->MoveRelative(95.0f, 0.0f);
377 _name_header.Draw();
378
379 _properties_header.Draw();
380
381 _category_display.Draw();
382 _list_displays[_current_category]->Draw();
383 } else if(_view_mode == SHOP_VIEW_MODE_INFO) {
384 VideoManager->SetDrawFlags(VIDEO_X_LEFT, VIDEO_Y_BOTTOM, 0);
385 VideoManager->Move(295.0f, 593.0f);
386 _name_header.Draw();
387 _properties_header.Draw();
388
389 VideoManager->MoveRelative(0.0f, 50.0f);
390 if (!_selected_icon.GetFilename().empty())
391 _selected_icon.Draw();
392 VideoManager->MoveRelative(30.0f, 0.0f);
393 _selected_name.Draw();
394 _selected_properties.Draw();
395
396 _category_display.Draw();
397 }
398
399 ShopMode::CurrentInstance()->ObjectViewer()->Draw();
400 }
401
ChangeViewMode(SHOP_VIEW_MODE new_mode)402 void BuyInterface::ChangeViewMode(SHOP_VIEW_MODE new_mode)
403 {
404 if(_view_mode == new_mode)
405 return;
406
407 if(new_mode == SHOP_VIEW_MODE_LIST) {
408 _view_mode = new_mode;
409 ShopMode::CurrentInstance()->ObjectViewer()->ChangeViewMode(_view_mode);
410 _category_display.ChangeViewMode(_view_mode);
411
412 _properties_header.SetOwner(ShopMode::CurrentInstance()->GetMiddleWindow());
413 _properties_header.SetPosition(480.0f, 10.0f);
414 } else if(new_mode == SHOP_VIEW_MODE_INFO) {
415 _view_mode = new_mode;
416 ShopMode::CurrentInstance()->ObjectViewer()->ChangeViewMode(_view_mode);
417 _category_display.ChangeViewMode(_view_mode);
418 _category_display.SetSelectedObject(_selected_object);
419
420 _properties_header.SetOwner(ShopMode::CurrentInstance()->GetBottomWindow());
421 _properties_header.SetPosition(480.0f, 15.0f);
422
423 _selected_name.SetText(_selected_object->GetObject()->GetName());
424 _selected_icon = _selected_object->GetObject()->GetIconImage();
425 _selected_icon.SetDimensions(30.0f, 30.0f);
426 _selected_properties.SetOptionText(0, MakeUnicodeString(NumberToString(_selected_object->GetBuyPrice())));
427 if (_selected_object->IsInfiniteAmount())
428 _selected_properties.SetOptionText(1, MakeUnicodeString("∞"));
429 else
430 _selected_properties.SetOptionText(1, MakeUnicodeString("×" + NumberToString(_selected_object->GetStockCount())));
431 uint32_t own_count = GlobalManager->GetInventoryHandler().HowManyObjectsInInventory(_selected_object->GetObject()->GetID());
432 _selected_properties.SetOptionText(2, MakeUnicodeString("×" + NumberToString(own_count)));
433
434 // Set the buy count to one, permitting quick one-buy sequences when affordable.
435 if (_list_displays[_current_category]->ChangeBuyQuantity(true, 1))
436 ShopMode::CurrentInstance()->ObjectViewer()->UpdateCountText();
437 } else {
438 IF_PRINT_WARNING(SHOP_DEBUG) << "tried to change to an invalid/unsupported view mode: " << new_mode << std::endl;
439 }
440 }
441
442
443
_ChangeCategory(bool left_or_right)444 bool BuyInterface::_ChangeCategory(bool left_or_right)
445 {
446 if(_number_categories <= 1)
447 return false;
448
449 if(left_or_right == false) {
450 _current_category = (_current_category == 0) ? (_number_categories - 1) : (_current_category - 1);
451 } else {
452 _current_category = (_current_category >= (_number_categories - 1)) ? 0 : (_current_category + 1);
453 }
454
455 _category_display.ChangeCategory(_category_names[_current_category], _category_icons[_current_category]);
456
457 ShopObject *last_obj = _selected_object;
458 _selected_object = _list_displays[_current_category]->GetSelectedObject();
459 if(last_obj == _selected_object)
460 return false;
461 else
462 return true;
463 }
464
465
466
_ChangeSelection(bool up_or_down)467 bool BuyInterface::_ChangeSelection(bool up_or_down)
468 {
469 if(_current_category >= _list_displays.size())
470 return false;
471
472 BuyListDisplay *selected_list = _list_displays[_current_category];
473
474 if(!selected_list)
475 return false;
476
477 if(up_or_down == false)
478 selected_list->InputUp();
479 else
480 selected_list->InputDown();
481
482 ShopObject *last_obj = _selected_object;
483 _selected_object = _list_displays[_current_category]->GetSelectedObject();
484 if(last_obj == _selected_object)
485 return false;
486 else
487 return true;
488 }
489
490 // *****************************************************************************
491 // ***** BuyListDisplay class methods
492 // *****************************************************************************
493
ReconstructList()494 void BuyListDisplay::ReconstructList()
495 {
496 _identify_list.ClearOptions();
497 _property_list.ClearOptions();
498
499 for(uint32_t i = 0; i < _objects.size(); i++) {
500 ShopObject* obj = _objects[i];
501 // Add an entry with the icon image of the object (scaled down by 4x to 30x30 pixels) followed by the object name
502 if (obj->GetObject()->GetIconImage().GetFilename().empty()) {
503 _identify_list.AddOption(MakeUnicodeString("<30>") + obj->GetObject()->GetName());
504 }
505 else {
506 _identify_list.AddOption(MakeUnicodeString("<" + obj->GetObject()->GetIconImage().GetFilename() + "><30>")
507 + obj->GetObject()->GetName());
508 _identify_list.GetEmbeddedImage(i)->SetDimensions(30.0f, 30.0f);
509 }
510
511 // Add an option for each object property in the order of: price, stock, and number owned.
512 _property_list.AddOption(MakeUnicodeString(NumberToString(obj->GetBuyPrice())));
513 if (obj->IsInfiniteAmount())
514 _property_list.AddOption(MakeUnicodeString("∞"));
515 else
516 _property_list.AddOption(MakeUnicodeString("×" + NumberToString(obj->GetStockCount())));
517 uint32_t own_count = GlobalManager->GetInventoryHandler().HowManyObjectsInInventory(obj->GetObject()->GetID());
518 _property_list.AddOption(MakeUnicodeString("×" + NumberToString(own_count)));
519 }
520
521 if(_objects.empty() == false) {
522 _identify_list.SetSelection(0);
523 _property_list.SetSelection(0);
524 }
525 }
526
527
ChangeBuyQuantity(bool more,uint32_t amount)528 bool BuyListDisplay::ChangeBuyQuantity(bool more, uint32_t amount)
529 {
530 ShopObject* obj = GetSelectedObject();
531 if(obj == nullptr) {
532 IF_PRINT_WARNING(SHOP_DEBUG) << "function could not perform operation because list was empty" << std::endl;
533 return false;
534 }
535
536 // Holds the amount that the quantity will actually increase or decrease by. May be less than the
537 // amount requested if there is an limitation such as shop stock or available funds
538 uint32_t change_amount = amount;
539
540 if(more == false) { // Decrement
541 // Ensure that at least one count of this object is already marked for purchase
542 if(obj->GetBuyCount() == 0) {
543 return false;
544 }
545
546 // Determine if there's a sufficient count selected to decrement by the desire amount. If not, return as many as possible
547 if(amount > obj->GetBuyCount()) {
548 change_amount = obj->GetBuyCount();
549 }
550
551 obj->DecrementBuyCount(change_amount);
552 ShopMode::CurrentInstance()->UpdateFinances(obj->GetBuyPrice() * change_amount);
553 return true;
554 }
555
556 // increment
557
558 // Make sure that there is at least one more object in stock and the player has enough funds to purchase it
559 if((!obj->IsInfiniteAmount() && obj->GetBuyCount() >= obj->GetStockCount()) ||
560 (obj->GetBuyPrice() > ShopMode::CurrentInstance()->GetTotalRemaining())) {
561 return false;
562 }
563
564 // Determine if there's enough of the object in stock to buy. If not, buy as many left as possible
565 if(!obj->IsInfiniteAmount() && (obj->GetStockCount() - obj->GetBuyCount()) < change_amount) {
566 change_amount = obj->GetStockCount() - obj->GetBuyCount();
567 }
568
569 // Determine how many of the possible amount to buy the player can actually afford
570 int32_t total_cost = change_amount * obj->GetBuyPrice();
571 int32_t total_remaining = static_cast<int32_t>(ShopMode::CurrentInstance()->GetTotalRemaining());
572 while(total_cost > total_remaining) {
573 --change_amount;
574 total_cost -= obj->GetBuyPrice();
575 }
576
577 obj->IncrementBuyCount(change_amount);
578 ShopMode::CurrentInstance()->UpdateFinances(-obj->GetBuyPrice() * change_amount);
579 return true;
580 } // bool BuyListDisplay::ChangeBuyQuantity(bool more, uint32_t amount)
581
582 } // namespace private_shop
583
584 } // namespace vt_shop
585