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 &center)
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 &center, 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