1 /* OutfitterPanel.cpp
2 Copyright (c) 2014 by Michael Zahniser
3
4 Endless Sky is free software: you can redistribute it and/or modify it under the
5 terms of the GNU General Public License as published by the Free Software
6 Foundation, either version 3 of the License, or (at your option) any later version.
7
8 Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY
9 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
10 PARTICULAR PURPOSE. See the GNU General Public License for more details.
11 */
12
13 #include "OutfitterPanel.h"
14
15 #include "text/alignment.hpp"
16 #include "Color.h"
17 #include "Dialog.h"
18 #include "text/DisplayText.h"
19 #include "DistanceMap.h"
20 #include "text/Font.h"
21 #include "text/FontSet.h"
22 #include "text/Format.h"
23 #include "GameData.h"
24 #include "Hardpoint.h"
25 #include "text/layout.hpp"
26 #include "Outfit.h"
27 #include "Planet.h"
28 #include "PlayerInfo.h"
29 #include "Point.h"
30 #include "Screen.h"
31 #include "Ship.h"
32 #include "Sprite.h"
33 #include "SpriteSet.h"
34 #include "SpriteShader.h"
35 #include "text/truncate.hpp"
36 #include "UI.h"
37
38 #include <algorithm>
39 #include <limits>
40 #include <memory>
41
42 using namespace std;
43
44 namespace {
Tons(int tons)45 string Tons(int tons)
46 {
47 return to_string(tons) + (tons == 1 ? " ton" : " tons");
48 }
49 }
50
51
52
OutfitterPanel(PlayerInfo & player)53 OutfitterPanel::OutfitterPanel(PlayerInfo &player)
54 : ShopPanel(player, true)
55 {
56 for(const pair<const string, Outfit> &it : GameData::Outfits())
57 catalog[it.second.Category()].insert(it.first);
58
59 // Add owned licenses
60 const string PREFIX = "license: ";
61 for(auto &it : player.Conditions())
62 if(it.first.compare(0, PREFIX.length(), PREFIX) == 0 && it.second > 0)
63 {
64 const string name = it.first.substr(PREFIX.length()) + " License";
65 const Outfit *outfit = GameData::Outfits().Get(name);
66 if(outfit)
67 catalog[outfit->Category()].insert(name);
68 }
69
70 if(player.GetPlanet())
71 outfitter = player.GetPlanet()->Outfitter();
72 }
73
74
75
Step()76 void OutfitterPanel::Step()
77 {
78 CheckRefill();
79 ShopPanel::Step();
80 if(GetUI()->IsTop(this) && !checkedHelp) {
81 if(!DoHelp("outfitter") && !DoHelp("outfitter 2") && !DoHelp("outfitter 3")) {
82 // All help messages have now been displayed.
83 checkedHelp = true;
84 }
85 }
86 }
87
88
89
TileSize() const90 int OutfitterPanel::TileSize() const
91 {
92 return OUTFIT_SIZE;
93 }
94
95
96
DrawPlayerShipInfo(const Point & point)97 int OutfitterPanel::DrawPlayerShipInfo(const Point &point)
98 {
99 shipInfo.Update(*playerShip, player.FleetDepreciation(), day);
100 shipInfo.DrawAttributes(point);
101
102 return shipInfo.AttributesHeight();
103 }
104
105
106
HasItem(const string & name) const107 bool OutfitterPanel::HasItem(const string &name) const
108 {
109 const Outfit *outfit = GameData::Outfits().Get(name);
110 if((outfitter.Has(outfit) || player.Stock(outfit) > 0) && showForSale)
111 return true;
112
113 if(player.Cargo().Get(outfit) && (!playerShip || showForSale))
114 return true;
115
116 if(player.Storage() && player.Storage()->Get(outfit))
117 return true;
118
119 for(const Ship *ship : playerShips)
120 if(ship->OutfitCount(outfit))
121 return true;
122
123 if(showForSale && HasLicense(name))
124 return true;
125
126 return false;
127 }
128
129
130
DrawItem(const string & name,const Point & point,int scrollY)131 void OutfitterPanel::DrawItem(const string &name, const Point &point, int scrollY)
132 {
133 const Outfit *outfit = GameData::Outfits().Get(name);
134 zones.emplace_back(point, Point(OUTFIT_SIZE, OUTFIT_SIZE), outfit, scrollY);
135 if(point.Y() + OUTFIT_SIZE / 2 < Screen::Top() || point.Y() - OUTFIT_SIZE / 2 > Screen::Bottom())
136 return;
137
138 bool isSelected = (outfit == selectedOutfit);
139 bool isOwned = playerShip && playerShip->OutfitCount(outfit);
140 DrawOutfit(*outfit, point, isSelected, isOwned);
141
142 // Check if this outfit is a "license".
143 bool isLicense = IsLicense(name);
144 int mapSize = outfit->Get("map");
145
146 const Font &font = FontSet::Get(14);
147 const Color &bright = *GameData::Colors().Get("bright");
148 if(playerShip || isLicense || mapSize)
149 {
150 int minCount = numeric_limits<int>::max();
151 int maxCount = 0;
152 if(isLicense)
153 minCount = maxCount = player.GetCondition(LicenseName(name));
154 else if(mapSize)
155 minCount = maxCount = HasMapped(mapSize);
156 else
157 {
158 for(const Ship *ship : playerShips)
159 {
160 int count = ship->OutfitCount(outfit);
161 minCount = min(minCount, count);
162 maxCount = max(maxCount, count);
163 }
164 }
165
166 if(maxCount)
167 {
168 string label = "installed: " + to_string(minCount);
169 if(maxCount > minCount)
170 label += " - " + to_string(maxCount);
171
172 Point labelPos = point + Point(-OUTFIT_SIZE / 2 + 20, OUTFIT_SIZE / 2 - 38);
173 font.Draw(label, labelPos, bright);
174 }
175 }
176 // Don't show the "in stock" amount if the outfit has an unlimited stock or
177 // if it is not something that you can buy.
178 int stock = 0;
179 if(!outfitter.Has(outfit) && outfit->Get("installable") >= 0.)
180 stock = max(0, player.Stock(outfit));
181 int cargo = player.Cargo().Get(outfit);
182 int storage = player.Storage() ? player.Storage()->Get(outfit) : 0;
183
184 string message;
185 if(cargo && storage && stock)
186 message = "cargo+stored: " + to_string(cargo + storage) + ", in stock: " + to_string(stock);
187 else if(cargo && storage)
188 message = "in cargo: " + to_string(cargo) + ", in storage: " + to_string(storage);
189 else if(cargo && stock)
190 message = "in cargo: " + to_string(cargo) + ", in stock: " + to_string(stock);
191 else if(storage && stock)
192 message = "in storage: " + to_string(storage) + ", in stock: " + to_string(stock);
193 else if(cargo)
194 message = "in cargo: " + to_string(cargo);
195 else if(storage)
196 message = "in storage: " + to_string(storage);
197 else if(stock)
198 message = "in stock: " + to_string(stock);
199 else if(!outfitter.Has(outfit))
200 message = "(not sold here)";
201 if(!message.empty())
202 {
203 Point pos = point + Point(
204 OUTFIT_SIZE / 2 - 20 - font.Width(message),
205 OUTFIT_SIZE / 2 - 24);
206 font.Draw(message, pos, bright);
207 }
208 }
209
210
211
DividerOffset() const212 int OutfitterPanel::DividerOffset() const
213 {
214 return 80;
215 }
216
217
218
DetailWidth() const219 int OutfitterPanel::DetailWidth() const
220 {
221 return 3 * outfitInfo.PanelWidth();
222 }
223
224
225
DrawDetails(const Point & center)226 int OutfitterPanel::DrawDetails(const Point ¢er)
227 {
228 string selectedItem = "Nothing Selected";
229 const Font &font = FontSet::Get(14);
230 const Color &bright = *GameData::Colors().Get("bright");
231 const Color &dim = *GameData::Colors().Get("medium");
232 const Sprite *collapsedArrow = SpriteSet::Get("ui/collapsed");
233
234 int heightOffset = 20;
235
236 if(selectedOutfit)
237 {
238 outfitInfo.Update(*selectedOutfit, player, CanSell());
239 selectedItem = selectedOutfit->Name();
240
241 const Sprite *thumbnail = selectedOutfit->Thumbnail();
242 const Sprite *background = SpriteSet::Get("ui/outfitter selected");
243
244 float tileSize = thumbnail
245 ? max(thumbnail->Height(), static_cast<float>(TileSize()))
246 : static_cast<float>(TileSize());
247
248 Point thumbnailCenter(center.X(), center.Y() + 20 + tileSize / 2);
249
250 Point startPoint(center.X() - INFOBAR_WIDTH / 2 + 20, center.Y() + 20 + tileSize);
251
252 double descriptionOffset = 35.;
253 Point descCenter(Screen::Right() - SIDE_WIDTH + INFOBAR_WIDTH / 2, startPoint.Y() + 20.);
254
255 // Maintenance note: This can be replaced with collapsed.contains() in C++20
256 if(!collapsed.count("description"))
257 {
258 descriptionOffset = outfitInfo.DescriptionHeight();
259 outfitInfo.DrawDescription(startPoint);
260 }
261 else
262 {
263 std::string label = "description";
264 font.Draw(label, startPoint + Point(35., 12.), dim);
265 SpriteShader::Draw(collapsedArrow, startPoint + Point(20., 20.));
266 }
267
268 // Calculate the new ClickZone for the description.
269 Point descDimensions(INFOBAR_WIDTH, descriptionOffset + 10.);
270 ClickZone<std::string> collapseDescription = ClickZone<std::string>(descCenter, descDimensions, std::string("description"));
271
272 // Find the old zone, and replace it with the new zone.
273 for(auto it = categoryZones.begin(); it != categoryZones.end(); ++it)
274 {
275 if(it->Value() == "description")
276 {
277 categoryZones.erase(it);
278 break;
279 }
280 }
281 categoryZones.emplace_back(collapseDescription);
282
283 Point attrPoint(startPoint.X(), startPoint.Y() + descriptionOffset);
284 Point reqsPoint(startPoint.X(), attrPoint.Y() + outfitInfo.AttributesHeight());
285
286 SpriteShader::Draw(background, thumbnailCenter);
287 if(thumbnail)
288 SpriteShader::Draw(thumbnail, thumbnailCenter);
289
290 outfitInfo.DrawAttributes(attrPoint);
291 outfitInfo.DrawRequirements(reqsPoint);
292
293 heightOffset = reqsPoint.Y() + outfitInfo.RequirementsHeight();
294 }
295
296 // Draw this string representing the selected item (if any), centered in the details side panel
297 Point selectedPoint(center.X() - .5 * INFOBAR_WIDTH, center.Y());
298 font.Draw({selectedItem, {INFOBAR_WIDTH - 20, Alignment::CENTER, Truncate::MIDDLE}},
299 selectedPoint, bright);
300
301 return heightOffset;
302 }
303
304
305
CanBuy(bool checkAlreadyOwned) const306 bool OutfitterPanel::CanBuy(bool checkAlreadyOwned) const
307 {
308 if(!planet || !selectedOutfit)
309 return false;
310
311 bool isAlreadyOwned = checkAlreadyOwned && IsAlreadyOwned();
312 if(!(outfitter.Has(selectedOutfit) || player.Stock(selectedOutfit) > 0 || isAlreadyOwned))
313 return false;
314
315 int mapSize = selectedOutfit->Get("map");
316 if(mapSize > 0 && HasMapped(mapSize))
317 return false;
318
319 // Determine what you will have to pay to buy this outfit.
320 int64_t cost = player.StockDepreciation().Value(selectedOutfit, day);
321 // Check that the player has any necessary licenses.
322 int64_t licenseCost = LicenseCost(selectedOutfit);
323 if(licenseCost < 0)
324 return false;
325 cost += licenseCost;
326 // If you have this in your cargo hold or in planetary storage, installing it is free.
327 if(cost > player.Accounts().Credits() && !isAlreadyOwned)
328 return false;
329
330 if(HasLicense(selectedOutfit->Name()))
331 return false;
332
333 if(!playerShip)
334 {
335 double mass = selectedOutfit->Mass();
336 return (!mass || player.Cargo().Free() >= mass);
337 }
338
339 for(const Ship *ship : playerShips)
340 if(ShipCanBuy(ship, selectedOutfit))
341 return true;
342
343 return false;
344 }
345
346
347
Buy(bool alreadyOwned)348 void OutfitterPanel::Buy(bool alreadyOwned)
349 {
350 int64_t licenseCost = LicenseCost(selectedOutfit);
351 if(licenseCost)
352 {
353 player.Accounts().AddCredits(-licenseCost);
354 for(const string &licenseName : selectedOutfit->Licenses())
355 if(!player.GetCondition("license: " + licenseName))
356 player.Conditions()["license: " + licenseName] = true;
357 }
358
359 int modifier = Modifier();
360 for(int i = 0; i < modifier && CanBuy(alreadyOwned); ++i)
361 {
362 // Special case: maps.
363 int mapSize = selectedOutfit->Get("map");
364 if(mapSize > 0)
365 {
366 if(!HasMapped(mapSize))
367 {
368 DistanceMap distance(player.GetSystem(), mapSize);
369 for(const System *system : distance.Systems())
370 if(!player.HasVisited(*system))
371 player.Visit(*system);
372 int64_t price = player.StockDepreciation().Value(selectedOutfit, day);
373 player.Accounts().AddCredits(-price);
374 }
375 return;
376 }
377
378 // Special case: licenses.
379 if(IsLicense(selectedOutfit->Name()))
380 {
381 auto &entry = player.Conditions()[LicenseName(selectedOutfit->Name())];
382 if(entry <= 0)
383 {
384 entry = true;
385 int64_t price = player.StockDepreciation().Value(selectedOutfit, day);
386 player.Accounts().AddCredits(-price);
387 }
388 return;
389 }
390
391 // Buying into cargo, either from storage or from stock/supply.
392 if(!playerShip)
393 {
394 if(alreadyOwned)
395 {
396 if(!player.Storage() || !player.Storage()->Get(selectedOutfit))
397 continue;
398 player.Cargo().Add(selectedOutfit);
399 player.Storage()->Remove(selectedOutfit);
400 }
401 else
402 {
403 // Check if the outfit is for sale or in stock so that we can actualy buy it.
404 if(!outfitter.Has(selectedOutfit) && player.Stock(selectedOutfit) <= 0)
405 continue;
406 player.Cargo().Add(selectedOutfit);
407 int64_t price = player.StockDepreciation().Value(selectedOutfit, day);
408 player.Accounts().AddCredits(-price);
409 player.AddStock(selectedOutfit, -1);
410 continue;
411 }
412 }
413
414 // Find the ships with the fewest number of these outfits.
415 const vector<Ship *> shipsToOutfit = GetShipsToOutfit(true);
416
417 for(Ship *ship : shipsToOutfit)
418 {
419 if(!CanBuy(alreadyOwned))
420 return;
421
422 if(player.Cargo().Get(selectedOutfit))
423 player.Cargo().Remove(selectedOutfit);
424 else if(player.Storage() && player.Storage()->Get(selectedOutfit))
425 player.Storage()->Remove(selectedOutfit);
426 else if(alreadyOwned || !(player.Stock(selectedOutfit) > 0 || outfitter.Has(selectedOutfit)))
427 break;
428 else
429 {
430 int64_t price = player.StockDepreciation().Value(selectedOutfit, day);
431 player.Accounts().AddCredits(-price);
432 player.AddStock(selectedOutfit, -1);
433 }
434 ship->AddOutfit(selectedOutfit, 1);
435 int required = selectedOutfit->Get("required crew");
436 if(required && ship->Crew() + required <= static_cast<int>(ship->Attributes().Get("bunks")))
437 ship->AddCrew(required);
438 ship->Recharge();
439 }
440 }
441 }
442
443
444
FailBuy() const445 void OutfitterPanel::FailBuy() const
446 {
447 if(!selectedOutfit)
448 return;
449
450 int64_t cost = player.StockDepreciation().Value(selectedOutfit, day);
451 int64_t credits = player.Accounts().Credits();
452 bool isInCargo = player.Cargo().Get(selectedOutfit);
453 bool isInStorage = player.Storage() && player.Storage()->Get(selectedOutfit);
454 if(!isInCargo && !isInStorage && cost > credits)
455 {
456 GetUI()->Push(new Dialog("You cannot buy this outfit, because it costs "
457 + Format::Credits(cost) + " credits, and you only have "
458 + Format::Credits(credits) + "."));
459 return;
460 }
461 // Check that the player has any necessary licenses.
462 int64_t licenseCost = LicenseCost(selectedOutfit);
463 if(licenseCost < 0)
464 {
465 GetUI()->Push(new Dialog(
466 "You cannot buy this outfit, because it requires a license that you don't have."));
467 return;
468 }
469 if(!isInCargo && !isInStorage && cost + licenseCost > credits)
470 {
471 GetUI()->Push(new Dialog(
472 "You don't have enough money to buy this outfit, because it will cost you an extra "
473 + Format::Credits(licenseCost) + " credits to buy the necessary licenses."));
474 return;
475 }
476
477 if(!(outfitter.Has(selectedOutfit) || player.Stock(selectedOutfit) > 0 || isInCargo || isInStorage))
478 {
479 GetUI()->Push(new Dialog("You cannot buy this outfit here. "
480 "It is being shown in the list because you have one installed in your ship, "
481 "but this " + planet->Noun() + " does not sell them."));
482 return;
483 }
484
485 if(selectedOutfit->Get("map"))
486 {
487 GetUI()->Push(new Dialog("You have already mapped all the systems shown by this map, "
488 "so there is no reason to buy another."));
489 return;
490 }
491
492 if(HasLicense(selectedOutfit->Name()))
493 {
494 GetUI()->Push(new Dialog("You already have one of these licenses, "
495 "so there is no reason to buy another."));
496 return;
497 }
498
499 if(!playerShip)
500 return;
501
502 double outfitNeeded = -selectedOutfit->Get("outfit space");
503 double outfitSpace = playerShip->Attributes().Get("outfit space");
504 if(outfitNeeded > outfitSpace)
505 {
506 string need = to_string(outfitNeeded) + (outfitNeeded != 1. ? "tons" : "ton");
507 GetUI()->Push(new Dialog("You cannot install this outfit, because it takes up "
508 + Tons(outfitNeeded) + " of outfit space, and this ship has "
509 + Tons(outfitSpace) + " free."));
510 return;
511 }
512
513 double weaponNeeded = -selectedOutfit->Get("weapon capacity");
514 double weaponSpace = playerShip->Attributes().Get("weapon capacity");
515 if(weaponNeeded > weaponSpace)
516 {
517 GetUI()->Push(new Dialog("Only part of your ship's outfit capacity is usable for weapons. "
518 "You cannot install this outfit, because it takes up "
519 + Tons(weaponNeeded) + " of weapon space, and this ship has "
520 + Tons(weaponSpace) + " free."));
521 return;
522 }
523
524 double engineNeeded = -selectedOutfit->Get("engine capacity");
525 double engineSpace = playerShip->Attributes().Get("engine capacity");
526 if(engineNeeded > engineSpace)
527 {
528 GetUI()->Push(new Dialog("Only part of your ship's outfit capacity is usable for engines. "
529 "You cannot install this outfit, because it takes up "
530 + Tons(engineNeeded) + " of engine space, and this ship has "
531 + Tons(engineSpace) + " free."));
532 return;
533 }
534
535 if(selectedOutfit->Category() == "Ammunition")
536 {
537 if(!playerShip->OutfitCount(selectedOutfit))
538 GetUI()->Push(new Dialog("This outfit is ammunition for a weapon. "
539 "You cannot install it without first installing the appropriate weapon."));
540 else
541 GetUI()->Push(new Dialog("You already have the maximum amount of ammunition for this weapon. "
542 "If you want to install more ammunition, you must first install another of these weapons."));
543 return;
544 }
545
546 int mountsNeeded = -selectedOutfit->Get("turret mounts");
547 int mountsFree = playerShip->Attributes().Get("turret mounts");
548 if(mountsNeeded && !mountsFree)
549 {
550 GetUI()->Push(new Dialog("This weapon is designed to be installed on a turret mount, "
551 "but your ship does not have any unused turret mounts available."));
552 return;
553 }
554
555 int gunsNeeded = -selectedOutfit->Get("gun ports");
556 int gunsFree = playerShip->Attributes().Get("gun ports");
557 if(gunsNeeded && !gunsFree)
558 {
559 GetUI()->Push(new Dialog("This weapon is designed to be installed in a gun port, "
560 "but your ship does not have any unused gun ports available."));
561 return;
562 }
563
564 if(selectedOutfit->Get("installable") < 0.)
565 {
566 GetUI()->Push(new Dialog("This item is not an outfit that can be installed in a ship."));
567 return;
568 }
569
570 if(!playerShip->Attributes().CanAdd(*selectedOutfit, 1))
571 {
572 GetUI()->Push(new Dialog("You cannot install this outfit in your ship, "
573 "because it would reduce one of your ship's attributes to a negative amount. "
574 "For example, it may use up more cargo space than you have left."));
575 return;
576 }
577 }
578
579
580
CanSell(bool toStorage) const581 bool OutfitterPanel::CanSell(bool toStorage) const
582 {
583 if(!planet || !selectedOutfit)
584 return false;
585
586 if(player.Cargo().Get(selectedOutfit))
587 return true;
588
589 if(!toStorage && player.Storage() && player.Storage()->Get(selectedOutfit))
590 return true;
591
592 for(const Ship *ship : playerShips)
593 if(ShipCanSell(ship, selectedOutfit))
594 return true;
595
596 return false;
597 }
598
599
600
Sell(bool toStorage)601 void OutfitterPanel::Sell(bool toStorage)
602 {
603 // Retrieve the players storage. If we want to store to storage, then
604 // we also request storage to be created if possible.
605 // Will be nullptr if no storage is available.
606 CargoHold *storage = player.Storage(toStorage);
607
608 if(player.Cargo().Get(selectedOutfit))
609 {
610 player.Cargo().Remove(selectedOutfit);
611 if(toStorage && storage && storage->Add(selectedOutfit))
612 {
613 // Transfer to planetary storage completed.
614 // The storage->Add() function should never fail as long as
615 // planetary storage has unlimited size.
616 }
617 else
618 {
619 int64_t price = player.FleetDepreciation().Value(selectedOutfit, day);
620 player.Accounts().AddCredits(price);
621 player.AddStock(selectedOutfit, 1);
622 }
623 return;
624 }
625
626 // Get the ships that have the most of this outfit installed.
627 // If there are no ships that have this outfit, then sell from storage.
628 const vector<Ship *> shipsToOutfit = GetShipsToOutfit();
629
630 if(shipsToOutfit.size() > 0)
631 {
632 for(Ship *ship : shipsToOutfit)
633 {
634 ship->AddOutfit(selectedOutfit, -1);
635 if(selectedOutfit->Get("required crew"))
636 ship->AddCrew(-selectedOutfit->Get("required crew"));
637 ship->Recharge();
638
639 if(toStorage && storage && storage->Add(selectedOutfit))
640 {
641 // Transfer to planetary storage completed.
642 }
643 else if(toStorage)
644 {
645 // No storage available; transfer to cargo even if it
646 // would exceed the cargo capacity.
647 int size = player.Cargo().Size();
648 player.Cargo().SetSize(-1);
649 player.Cargo().Add(selectedOutfit);
650 player.Cargo().SetSize(size);
651 }
652 else
653 {
654 int64_t price = player.FleetDepreciation().Value(selectedOutfit, day);
655 player.Accounts().AddCredits(price);
656 player.AddStock(selectedOutfit, 1);
657 }
658
659 const Outfit *ammo = selectedOutfit->Ammo();
660 if(ammo && ship->OutfitCount(ammo))
661 {
662 // Determine how many of this ammo I must sell to also sell the launcher.
663 int mustSell = 0;
664 for(const pair<const char *, double> &it : ship->Attributes().Attributes())
665 if(it.second < 0.)
666 mustSell = max<int>(mustSell, it.second / ammo->Get(it.first));
667
668 if(mustSell)
669 {
670 ship->AddOutfit(ammo, -mustSell);
671 if(toStorage && storage)
672 mustSell -= storage->Add(ammo, mustSell);
673 if(mustSell)
674 {
675 int64_t price = player.FleetDepreciation().Value(ammo, day, mustSell);
676 player.Accounts().AddCredits(price);
677 player.AddStock(ammo, mustSell);
678 }
679 }
680 }
681 }
682 return;
683 }
684
685 if(!toStorage && storage && storage->Get(selectedOutfit))
686 {
687 storage->Remove(selectedOutfit);
688 int64_t price = player.FleetDepreciation().Value(selectedOutfit, day);
689 player.Accounts().AddCredits(price);
690 player.AddStock(selectedOutfit, 1);
691 }
692 }
693
694
695
FailSell(bool toStorage) const696 void OutfitterPanel::FailSell(bool toStorage) const
697 {
698 const string &verb = toStorage ? "uninstall" : "sell";
699 if(!planet || !selectedOutfit)
700 return;
701 else if(selectedOutfit->Get("map"))
702 GetUI()->Push(new Dialog("You cannot " + verb + " maps. Once you buy one, it is yours permanently."));
703 else if(HasLicense(selectedOutfit->Name()))
704 GetUI()->Push(new Dialog("You cannot " + verb + " licenses. Once you obtain one, it is yours permanently."));
705 else
706 {
707 bool hasOutfit = player.Cargo().Get(selectedOutfit);
708 hasOutfit = hasOutfit || (!toStorage && player.Storage() && player.Storage()->Get(selectedOutfit));
709 for(const Ship *ship : playerShips)
710 if(ship->OutfitCount(selectedOutfit))
711 {
712 hasOutfit = true;
713 break;
714 }
715 if(!hasOutfit)
716 GetUI()->Push(new Dialog("You do not have any of these outfits to " + verb + "."));
717 else
718 {
719 for(const Ship *ship : playerShips)
720 for(const pair<const char *, double> &it : selectedOutfit->Attributes())
721 if(ship->Attributes().Get(it.first) < it.second)
722 {
723 for(const auto &sit : ship->Outfits())
724 if(sit.first->Get(it.first) < 0.)
725 {
726 GetUI()->Push(new Dialog("You cannot " + verb + " this outfit, "
727 "because that would cause your ship's \"" + it.first +
728 "\" value to be reduced to less than zero. "
729 "To " + verb + " this outfit, you must " + verb + " the " +
730 sit.first->Name() + " outfit first."));
731 return;
732 }
733 GetUI()->Push(new Dialog("You cannot " + verb + " this outfit, "
734 "because that would cause your ship's \"" + it.first +
735 "\" value to be reduced to less than zero."));
736 return;
737 }
738 GetUI()->Push(new Dialog("You cannot " + verb + " this outfit, "
739 "because something else in your ship depends on it."));
740 }
741 }
742 }
743
744
745
ShouldHighlight(const Ship * ship)746 bool OutfitterPanel::ShouldHighlight(const Ship *ship)
747 {
748 if(!selectedOutfit)
749 return false;
750
751 if(hoverButton == 'b')
752 return CanBuy() && ShipCanBuy(ship, selectedOutfit);
753 else if(hoverButton == 's')
754 return CanSell() && ShipCanSell(ship, selectedOutfit);
755
756 return false;
757 }
758
759
760
DrawKey()761 void OutfitterPanel::DrawKey()
762 {
763 const Sprite *back = SpriteSet::Get("ui/outfitter key");
764 SpriteShader::Draw(back, Screen::BottomLeft() + .5 * Point(back->Width(), -back->Height()));
765
766 const Font &font = FontSet::Get(14);
767 Color color[2] = {*GameData::Colors().Get("medium"), *GameData::Colors().Get("bright")};
768 const Sprite *box[2] = {SpriteSet::Get("ui/unchecked"), SpriteSet::Get("ui/checked")};
769
770 Point pos = Screen::BottomLeft() + Point(10., -30.);
771 Point off = Point(10., -.5 * font.Height());
772 SpriteShader::Draw(box[showForSale], pos);
773 font.Draw("Show outfits for sale", pos + off, color[showForSale]);
774 AddZone(Rectangle(pos + Point(80., 0.), Point(180., 20.)), [this](){ ToggleForSale(); });
775
776 bool showCargo = !playerShip;
777 pos.Y() += 20.;
778 SpriteShader::Draw(box[showCargo], pos);
779 font.Draw("Show outfits in cargo", pos + off, color[showCargo]);
780 AddZone(Rectangle(pos + Point(80., 0.), Point(180., 20.)), [this](){ ToggleCargo(); });
781 }
782
783
784
ToggleForSale()785 void OutfitterPanel::ToggleForSale()
786 {
787 showForSale = !showForSale;
788
789 ShopPanel::ToggleForSale();
790 }
791
792
793
ToggleCargo()794 void OutfitterPanel::ToggleCargo()
795 {
796 if(playerShip)
797 {
798 previousShip = playerShip;
799 playerShip = nullptr;
800 previousShips = playerShips;
801 playerShips.clear();
802 }
803 else if(previousShip)
804 {
805 playerShip = previousShip;
806 playerShips = previousShips;
807 }
808 else
809 {
810 playerShip = player.Flagship();
811 if(playerShip)
812 playerShips.insert(playerShip);
813 }
814
815 ShopPanel::ToggleCargo();
816 }
817
818
819
ShipCanBuy(const Ship * ship,const Outfit * outfit)820 bool OutfitterPanel::ShipCanBuy(const Ship *ship, const Outfit *outfit)
821 {
822 return (ship->Attributes().CanAdd(*outfit, 1) > 0);
823 }
824
825
826
ShipCanSell(const Ship * ship,const Outfit * outfit)827 bool OutfitterPanel::ShipCanSell(const Ship *ship, const Outfit *outfit)
828 {
829 if(!ship->OutfitCount(outfit))
830 return false;
831
832 // If this outfit requires ammo, check if we could sell it if we sold all
833 // the ammo for it first.
834 const Outfit *ammo = outfit->Ammo();
835 if(ammo && ship->OutfitCount(ammo))
836 {
837 Outfit attributes = ship->Attributes();
838 attributes.Add(*ammo, -ship->OutfitCount(ammo));
839 return attributes.CanAdd(*outfit, -1);
840 }
841
842 // Now, check whether this ship can sell this outfit.
843 return ship->Attributes().CanAdd(*outfit, -1);
844 }
845
846
847
DrawOutfit(const Outfit & outfit,const Point & center,bool isSelected,bool isOwned)848 void OutfitterPanel::DrawOutfit(const Outfit &outfit, const Point ¢er, bool isSelected, bool isOwned)
849 {
850 const Sprite *thumbnail = outfit.Thumbnail();
851 const Sprite *back = SpriteSet::Get(
852 isSelected ? "ui/outfitter selected" : "ui/outfitter unselected");
853 SpriteShader::Draw(back, center);
854 SpriteShader::Draw(thumbnail, center);
855
856 // Draw the outfit name.
857 const string &name = outfit.Name();
858 const Font &font = FontSet::Get(14);
859 Point offset(-.5 * OUTFIT_SIZE, -.5 * OUTFIT_SIZE + 10.);
860 font.Draw({name, {OUTFIT_SIZE, Alignment::CENTER, Truncate::MIDDLE}},
861 center + offset, Color((isSelected | isOwned) ? .8 : .5, 0.));
862 }
863
864
865
HasMapped(int mapSize) const866 bool OutfitterPanel::HasMapped(int mapSize) const
867 {
868 DistanceMap distance(player.GetSystem(), mapSize);
869 for(const System *system : distance.Systems())
870 if(!player.HasVisited(*system))
871 return false;
872
873 return true;
874 }
875
876
877
IsLicense(const string & name) const878 bool OutfitterPanel::IsLicense(const string &name) const
879 {
880 static const string &LICENSE = " License";
881 if(name.length() < LICENSE.length())
882 return false;
883 if(name.compare(name.length() - LICENSE.length(), LICENSE.length(), LICENSE))
884 return false;
885
886 return true;
887 }
888
889
890
HasLicense(const string & name) const891 bool OutfitterPanel::HasLicense(const string &name) const
892 {
893 return (IsLicense(name) && player.GetCondition(LicenseName(name)) > 0);
894 }
895
896
897
LicenseName(const string & name) const898 string OutfitterPanel::LicenseName(const string &name) const
899 {
900 static const string &LICENSE = " License";
901 return "license: " + name.substr(0, name.length() - LICENSE.length());
902 }
903
904
905
CheckRefill()906 void OutfitterPanel::CheckRefill()
907 {
908 if(checkedRefill)
909 return;
910 checkedRefill = true;
911
912 int count = 0;
913 map<const Outfit *, int> needed;
914 for(const shared_ptr<Ship> &ship : player.Ships())
915 {
916 if(ship->GetSystem() != player.GetSystem() || ship->IsDisabled())
917 continue;
918
919 ++count;
920 set<const Outfit *> toRefill;
921 for(const Hardpoint &it : ship->Weapons())
922 if(it.GetOutfit() && it.GetOutfit()->Ammo())
923 toRefill.insert(it.GetOutfit()->Ammo());
924
925 for(const Outfit *outfit : toRefill)
926 {
927 int amount = ship->Attributes().CanAdd(*outfit, numeric_limits<int>::max());
928 if(amount > 0 && (outfitter.Has(outfit) || player.Stock(outfit) > 0 || player.Cargo().Get(outfit)))
929 needed[outfit] += amount;
930 }
931 }
932
933 int64_t cost = 0;
934 for(auto &it : needed)
935 {
936 // Don't count cost of anything installed from cargo.
937 it.second = max(0, it.second - player.Cargo().Get(it.first));
938 if(!outfitter.Has(it.first))
939 it.second = min(it.second, max(0, player.Stock(it.first)));
940 cost += player.StockDepreciation().Value(it.first, day, it.second);
941 }
942 if(!needed.empty() && cost < player.Accounts().Credits())
943 {
944 string message = "Do you want to reload all the ammunition for your ship";
945 message += (count == 1) ? "?" : "s?";
946 if(cost)
947 message += " It will cost " + Format::Credits(cost) + " credits.";
948 GetUI()->Push(new Dialog(this, &OutfitterPanel::Refill, message));
949 }
950 }
951
952
953
Refill()954 void OutfitterPanel::Refill()
955 {
956 for(const shared_ptr<Ship> &ship : player.Ships())
957 {
958 if(ship->GetSystem() != player.GetSystem() || ship->IsDisabled())
959 continue;
960
961 set<const Outfit *> toRefill;
962 for(const Hardpoint &it : ship->Weapons())
963 if(it.GetOutfit() && it.GetOutfit()->Ammo())
964 toRefill.insert(it.GetOutfit()->Ammo());
965
966 for(const Outfit *outfit : toRefill)
967 {
968 int neededAmmo = ship->Attributes().CanAdd(*outfit, numeric_limits<int>::max());
969 if(neededAmmo > 0)
970 {
971 // Fill first from any stockpiles in cargo.
972 int fromCargo = player.Cargo().Remove(outfit, neededAmmo);
973 neededAmmo -= fromCargo;
974 // Then, buy at reduced (or full) price.
975 int available = outfitter.Has(outfit) ? neededAmmo : min<int>(neededAmmo, max<int>(0, player.Stock(outfit)));
976 if(neededAmmo && available > 0)
977 {
978 int64_t price = player.StockDepreciation().Value(outfit, day, available);
979 player.Accounts().AddCredits(-price);
980 player.AddStock(outfit, -available);
981 }
982 ship->AddOutfit(outfit, available + fromCargo);
983 }
984 }
985 }
986 }
987
988
989
990 // Determine which ships of the selected ships should be referenced in this
991 // iteration of Buy / Sell.
GetShipsToOutfit(bool isBuy) const992 const vector<Ship *> OutfitterPanel::GetShipsToOutfit(bool isBuy) const
993 {
994 vector<Ship *> shipsToOutfit;
995 int compareValue = isBuy ? numeric_limits<int>::max() : 0;
996 int compareMod = 2 * isBuy - 1;
997 for(Ship *ship : playerShips)
998 {
999 if((isBuy && !ShipCanBuy(ship, selectedOutfit))
1000 || (!isBuy && !ShipCanSell(ship, selectedOutfit)))
1001 continue;
1002
1003 int count = ship->OutfitCount(selectedOutfit);
1004 if(compareMod * count < compareMod * compareValue)
1005 {
1006 shipsToOutfit.clear();
1007 compareValue = count;
1008 }
1009 if(count == compareValue)
1010 shipsToOutfit.push_back(ship);
1011 }
1012
1013 return shipsToOutfit;
1014 }
1015