1 /* ShipInfoPanel.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 "ShipInfoPanel.h"
14 
15 #include "text/alignment.hpp"
16 #include "Command.h"
17 #include "Dialog.h"
18 #include "text/DisplayText.h"
19 #include "text/Font.h"
20 #include "text/FontSet.h"
21 #include "text/Format.h"
22 #include "GameData.h"
23 #include "Information.h"
24 #include "Interface.h"
25 #include "LineShader.h"
26 #include "LogbookPanel.h"
27 #include "Messages.h"
28 #include "MissionPanel.h"
29 #include "OutlineShader.h"
30 #include "PlayerInfo.h"
31 #include "PlayerInfoPanel.h"
32 #include "Rectangle.h"
33 #include "Ship.h"
34 #include "Sprite.h"
35 #include "SpriteShader.h"
36 #include "text/Table.h"
37 #include "text/truncate.hpp"
38 #include "UI.h"
39 
40 #include <algorithm>
41 
42 using namespace std;
43 
44 namespace {
45 	constexpr double WIDTH = 250.;
46 	constexpr int COLUMN_WIDTH = static_cast<int>(WIDTH) - 20;
47 }
48 
49 
ShipInfoPanel(PlayerInfo & player,int index)50 ShipInfoPanel::ShipInfoPanel(PlayerInfo &player, int index)
51 	: player(player), shipIt(player.Ships().begin()), canEdit(player.GetPlanet())
52 {
53 	SetInterruptible(false);
54 
55 	// If a valid ship index was given, show that ship.
56 	if(static_cast<unsigned>(index) < player.Ships().size())
57 		shipIt += index;
58 	else if(player.Flagship())
59 	{
60 		// Find the player's flagship. It may not be first in the list, if the
61 		// first item in the list cannot be a flagship.
62 		while(shipIt != player.Ships().end() && shipIt->get() != player.Flagship())
63 			++shipIt;
64 	}
65 
66 	UpdateInfo();
67 }
68 
69 
70 
Step()71 void ShipInfoPanel::Step ()
72 {
73 	DoHelp("ship info");
74 }
75 
76 
77 
Draw()78 void ShipInfoPanel::Draw()
79 {
80 	// Dim everything behind this panel.
81 	DrawBackdrop();
82 
83 	// Fill in the information for how this interface should be drawn.
84 	Information interfaceInfo;
85 	interfaceInfo.SetCondition("ship tab");
86 	if(canEdit && (shipIt != player.Ships().end())
87 			&& (shipIt->get() != player.Flagship() || (*shipIt)->IsParked()))
88 	{
89 		if(!(*shipIt)->IsDisabled())
90 			interfaceInfo.SetCondition("can park");
91 		interfaceInfo.SetCondition((*shipIt)->IsParked() ? "show unpark" : "show park");
92 		interfaceInfo.SetCondition("show disown");
93 	}
94 	else if(!canEdit)
95 	{
96 		interfaceInfo.SetCondition("show dump");
97 		if(CanDump())
98 			interfaceInfo.SetCondition("enable dump");
99 	}
100 	if(player.Ships().size() > 1)
101 		interfaceInfo.SetCondition("five buttons");
102 	else
103 		interfaceInfo.SetCondition("three buttons");
104 	if(player.HasLogs())
105 		interfaceInfo.SetCondition("enable logbook");
106 
107 	// Draw the interface.
108 	const Interface *infoPanelUi = GameData::Interfaces().Get("info panel");
109 	infoPanelUi->Draw(interfaceInfo, this);
110 
111 	// Draw all the different information sections.
112 	ClearZones();
113 	Rectangle cargoBounds = infoPanelUi->GetBox("cargo");
114 	DrawShipStats(infoPanelUi->GetBox("stats"));
115 	DrawOutfits(infoPanelUi->GetBox("outfits"), cargoBounds);
116 	DrawWeapons(infoPanelUi->GetBox("weapons"));
117 	DrawCargo(cargoBounds);
118 
119 	// If the player hovers their mouse over a ship attribute, show its tooltip.
120 	info.DrawTooltips();
121 }
122 
123 
124 
KeyDown(SDL_Keycode key,Uint16 mod,const Command & command,bool isNewPress)125 bool ShipInfoPanel::KeyDown(SDL_Keycode key, Uint16 mod, const Command &command, bool isNewPress)
126 {
127 	bool shift = (mod & KMOD_SHIFT);
128 	if(key == 'd' || key == SDLK_ESCAPE || (key == 'w' && (mod & (KMOD_CTRL | KMOD_GUI))))
129 		GetUI()->Pop(this);
130 	else if(!player.Ships().empty() && ((key == 'p' && !shift) || key == SDLK_LEFT || key == SDLK_UP))
131 	{
132 		if(shipIt == player.Ships().begin())
133 			shipIt = player.Ships().end();
134 		--shipIt;
135 		UpdateInfo();
136 	}
137 	else if(!player.Ships().empty() && (key == 'n' || key == SDLK_RIGHT || key == SDLK_DOWN))
138 	{
139 		++shipIt;
140 		if(shipIt == player.Ships().end())
141 			shipIt = player.Ships().begin();
142 		UpdateInfo();
143 	}
144 	else if(key == 'i' || command.Has(Command::INFO))
145 	{
146 		GetUI()->Pop(this);
147 		GetUI()->Push(new PlayerInfoPanel(player));
148 	}
149 	else if(key == 'R' || (key == 'r' && shift))
150 		GetUI()->Push(new Dialog(this, &ShipInfoPanel::Rename, "Change this ship's name?", (*shipIt)->Name()));
151 	else if(canEdit && (key == 'P' || (key == 'p' && shift)))
152 	{
153 		if(shipIt->get() != player.Flagship() || (*shipIt)->IsParked())
154 			player.ParkShip(shipIt->get(), !(*shipIt)->IsParked());
155 	}
156 	else if(canEdit && key == 'D')
157 	{
158 		if(shipIt->get() != player.Flagship())
159 			GetUI()->Push(new Dialog(this, &ShipInfoPanel::Disown, "Are you sure you want to disown \""
160 				+ shipIt->get()->Name()
161 				+ "\"? Disowning a ship rather than selling it means you will not get any money for it."));
162 	}
163 	else if(key == 'c' && CanDump())
164 	{
165 		int commodities = (*shipIt)->Cargo().CommoditiesSize();
166 		int amount = (*shipIt)->Cargo().Get(selectedCommodity);
167 		int plunderAmount = (*shipIt)->Cargo().Get(selectedPlunder);
168 		if(amount)
169 		{
170 			GetUI()->Push(new Dialog(this, &ShipInfoPanel::DumpCommodities,
171 				"How many tons of " + Format::LowerCase(selectedCommodity)
172 					+ " do you want to jettison?", amount));
173 		}
174 		else if(plunderAmount > 0 && selectedPlunder->Get("installable") < 0.)
175 		{
176 			GetUI()->Push(new Dialog(this, &ShipInfoPanel::DumpPlunder,
177 				"How many tons of " + Format::LowerCase(selectedPlunder->Name())
178 					+ " do you want to jettison?", plunderAmount));
179 		}
180 		else if(plunderAmount == 1)
181 		{
182 			GetUI()->Push(new Dialog(this, &ShipInfoPanel::Dump,
183 				"Are you sure you want to jettison a " + selectedPlunder->Name() + "?"));
184 		}
185 		else if(plunderAmount > 1)
186 		{
187 			GetUI()->Push(new Dialog(this, &ShipInfoPanel::DumpPlunder,
188 				"How many " + selectedPlunder->PluralName() + " do you want to jettison?",
189 				plunderAmount));
190 		}
191 		else if(commodities)
192 		{
193 			GetUI()->Push(new Dialog(this, &ShipInfoPanel::Dump,
194 				"Are you sure you want to jettison all of this ship's regular cargo?"));
195 		}
196 		else
197 		{
198 			GetUI()->Push(new Dialog(this, &ShipInfoPanel::Dump,
199 				"Are you sure you want to jettison all of this ship's cargo?"));
200 		}
201 	}
202 	else if(command.Has(Command::MAP) || key == 'm')
203 		GetUI()->Push(new MissionPanel(player));
204 	else if(key == 'l' && player.HasLogs())
205 		GetUI()->Push(new LogbookPanel(player));
206 	else
207 		return false;
208 
209 	return true;
210 }
211 
212 
213 
Click(int x,int y,int clicks)214 bool ShipInfoPanel::Click(int x, int y, int clicks)
215 {
216 	if(shipIt == player.Ships().end())
217 		return true;
218 
219 	draggingIndex = -1;
220 	if(canEdit && hoverIndex >= 0 && (**shipIt).GetSystem() == player.GetSystem() && !(**shipIt).IsDisabled())
221 		draggingIndex = hoverIndex;
222 
223 	selectedCommodity.clear();
224 	selectedPlunder = nullptr;
225 	Point point(x, y);
226 	for(const auto &zone : commodityZones)
227 		if(zone.Contains(point))
228 			selectedCommodity = zone.Value();
229 	for(const auto &zone : plunderZones)
230 		if(zone.Contains(point))
231 			selectedPlunder = zone.Value();
232 
233 	return true;
234 }
235 
236 
237 
Hover(int x,int y)238 bool ShipInfoPanel::Hover(int x, int y)
239 {
240 	Point point(x, y);
241 	info.Hover(point);
242 	return Hover(point);
243 }
244 
245 
246 
Drag(double dx,double dy)247 bool ShipInfoPanel::Drag(double dx, double dy)
248 {
249 	return Hover(hoverPoint + Point(dx, dy));
250 }
251 
252 
253 
Release(int x,int y)254 bool ShipInfoPanel::Release(int x, int y)
255 {
256 	if(draggingIndex >= 0 && hoverIndex >= 0 && hoverIndex != draggingIndex)
257 		(**shipIt).GetArmament().Swap(hoverIndex, draggingIndex);
258 
259 	draggingIndex = -1;
260 	return true;
261 }
262 
263 
264 
UpdateInfo()265 void ShipInfoPanel::UpdateInfo()
266 {
267 	draggingIndex = -1;
268 	hoverIndex = -1;
269 	ClearZones();
270 	if(shipIt == player.Ships().end())
271 		return;
272 
273 	const Ship &ship = **shipIt;
274 	info.Update(ship, player.FleetDepreciation(), player.GetDate().DaysSinceEpoch());
275 	if(player.Flagship() && ship.GetSystem() == player.GetSystem() && &ship != player.Flagship())
276 		player.Flagship()->SetTargetShip(*shipIt);
277 
278 	outfits.clear();
279 	for(const auto &it : ship.Outfits())
280 		outfits[it.first->Category()].push_back(it.first);
281 }
282 
283 
284 
ClearZones()285 void ShipInfoPanel::ClearZones()
286 {
287 	zones.clear();
288 	commodityZones.clear();
289 	plunderZones.clear();
290 }
291 
292 
293 
DrawShipStats(const Rectangle & bounds)294 void ShipInfoPanel::DrawShipStats(const Rectangle &bounds)
295 {
296 	// Check that the specified area is big enough.
297 	if(bounds.Width() < WIDTH)
298 		return;
299 
300 	// Colors to draw with.
301 	Color dim = *GameData::Colors().Get("medium");
302 	Color bright = *GameData::Colors().Get("bright");
303 	const Ship &ship = **shipIt;
304 
305 	// Two columns of opposite alignment are used to simulate a single visual column.
306 	Table table;
307 	table.AddColumn(0, {COLUMN_WIDTH, Alignment::LEFT});
308 	table.AddColumn(COLUMN_WIDTH, {COLUMN_WIDTH, Alignment::RIGHT, Truncate::MIDDLE});
309 	table.SetUnderline(0, COLUMN_WIDTH);
310 	table.DrawAt(bounds.TopLeft() + Point(10., 8.));
311 
312 	table.DrawTruncatedPair("ship:", dim, ship.Name(), bright, Truncate::MIDDLE, true);
313 	table.DrawTruncatedPair("model:", dim, ship.ModelName(), bright, Truncate::MIDDLE, true);
314 
315 	info.DrawAttributes(table.GetRowBounds().TopLeft() - Point(10., 10.));
316 }
317 
318 
319 
DrawOutfits(const Rectangle & bounds,Rectangle & cargoBounds)320 void ShipInfoPanel::DrawOutfits(const Rectangle &bounds, Rectangle &cargoBounds)
321 {
322 	// Check that the specified area is big enough.
323 	if(bounds.Width() < WIDTH)
324 		return;
325 
326 	// Colors to draw with.
327 	Color dim = *GameData::Colors().Get("medium");
328 	Color bright = *GameData::Colors().Get("bright");
329 	const Ship &ship = **shipIt;
330 
331 	// Two columns of opposite alignment are used to simulate a single visual column.
332 	Table table;
333 	table.AddColumn(0, {COLUMN_WIDTH, Alignment::LEFT});
334 	table.AddColumn(COLUMN_WIDTH, {COLUMN_WIDTH, Alignment::RIGHT});
335 	table.SetUnderline(0, COLUMN_WIDTH);
336 	Point start = bounds.TopLeft() + Point(10., 8.);
337 	table.DrawAt(start);
338 
339 	// Draw the outfits in the same order used in the outfitter.
340 	for(const string &category : Outfit::CATEGORIES)
341 	{
342 		auto it = outfits.find(category);
343 		if(it == outfits.end())
344 			continue;
345 
346 		// Skip to the next column if there is not space for this category label
347 		// plus at least one outfit.
348 		if(table.GetRowBounds().Bottom() + 40. > bounds.Bottom())
349 		{
350 			start += Point(WIDTH, 0.);
351 			if(start.X() + COLUMN_WIDTH > bounds.Right())
352 				break;
353 			table.DrawAt(start);
354 		}
355 
356 		// Draw the category label.
357 		table.Draw(category, bright);
358 		table.Advance();
359 		for(const Outfit *outfit : it->second)
360 		{
361 			// Check if we've gone below the bottom of the bounds.
362 			if(table.GetRowBounds().Bottom() > bounds.Bottom())
363 			{
364 				start += Point(WIDTH, 0.);
365 				if(start.X() + COLUMN_WIDTH > bounds.Right())
366 					break;
367 				table.DrawAt(start);
368 				table.Draw(category, bright);
369 				table.Advance();
370 			}
371 
372 			// Draw the outfit name and count.
373 			table.DrawTruncatedPair(outfit->Name(), dim,
374 				to_string(ship.OutfitCount(outfit)), bright, Truncate::BACK, false);
375 		}
376 		// Add an extra gap in between categories.
377 		table.DrawGap(10.);
378 	}
379 
380 	// Check if this information spilled over into the cargo column.
381 	if(table.GetPoint().X() >= cargoBounds.Left())
382 	{
383 		double startY = table.GetRowBounds().Top() - 8.;
384 		cargoBounds = Rectangle::WithCorners(
385 			Point(cargoBounds.Left(), startY),
386 			Point(cargoBounds.Right(), max(startY, cargoBounds.Bottom())));
387 	}
388 }
389 
390 
391 
DrawWeapons(const Rectangle & bounds)392 void ShipInfoPanel::DrawWeapons(const Rectangle &bounds)
393 {
394 	// Colors to draw with.
395 	Color dim = *GameData::Colors().Get("medium");
396 	Color bright = *GameData::Colors().Get("bright");
397 	const Font &font = FontSet::Get(14);
398 	const Ship &ship = **shipIt;
399 
400 	// Figure out how much to scale the sprite by.
401 	const Sprite *sprite = ship.GetSprite();
402 	double scale = 0.;
403 	if(sprite)
404 		scale = min(1., min((WIDTH - 10) / sprite->Width(), (WIDTH - 10) / sprite->Height()));
405 
406 	// Figure out the left- and right-most hardpoints on the ship. If they are
407 	// too far apart, the scale may need to be reduced.
408 	// Also figure out how many weapons of each type are on each side.
409 	double maxX = 0.;
410 	int count[2][2] = {{0, 0}, {0, 0}};
411 	for(const Hardpoint &hardpoint : ship.Weapons())
412 	{
413 		// Multiply hardpoint X by 2 to convert to sprite pixels.
414 		maxX = max(maxX, fabs(2. * hardpoint.GetPoint().X()));
415 		++count[hardpoint.GetPoint().X() >= 0.][hardpoint.IsTurret()];
416 	}
417 	// If necessary, shrink the sprite to keep the hardpoints inside the labels.
418 	// The width of this UI block will be 2 * (LABEL_WIDTH + HARDPOINT_DX).
419 	static const double LABEL_WIDTH = 150.;
420 	static const double LABEL_DX = 95.;
421 	static const double LABEL_PAD = 5.;
422 	if(maxX > (LABEL_DX - LABEL_PAD))
423 		scale = min(scale, (LABEL_DX - LABEL_PAD) / (2. * maxX));
424 
425 	// Draw the ship, using the black silhouette swizzle.
426 	SpriteShader::Draw(sprite, bounds.Center(), scale, 8);
427 	OutlineShader::Draw(sprite, bounds.Center(), scale * Point(sprite->Width(), sprite->Height()), Color(.5f));
428 
429 	// Figure out how tall each part of the weapon listing will be.
430 	int gunRows = max(count[0][0], count[1][0]);
431 	int turretRows = max(count[0][1], count[1][1]);
432 	// If there are both guns and turrets, add a gap of ten pixels.
433 	double height = 20. * (gunRows + turretRows) + 10. * (gunRows && turretRows);
434 
435 	double gunY = bounds.Top() + .5 * (bounds.Height() - height);
436 	double turretY = gunY + 20. * gunRows + 10. * (gunRows != 0);
437 	double nextY[2][2] = {
438 		{gunY + 20. * (gunRows - count[0][0]), turretY + 20. * (turretRows - count[0][1])},
439 		{gunY + 20. * (gunRows - count[1][0]), turretY + 20. * (turretRows - count[1][1])}};
440 
441 	int index = 0;
442 	const double centerX = bounds.Center().X();
443 	const double labelCenter[2] = {-.5 * LABEL_WIDTH - LABEL_DX, LABEL_DX + .5 * LABEL_WIDTH};
444 	const double fromX[2] = {-LABEL_DX + LABEL_PAD, LABEL_DX - LABEL_PAD};
445 	static const double LINE_HEIGHT = 20.;
446 	static const double TEXT_OFF = .5 * (LINE_HEIGHT - font.Height());
447 	static const Point LINE_SIZE(LABEL_WIDTH, LINE_HEIGHT);
448 	Point topFrom;
449 	Point topTo;
450 	Color topColor;
451 	bool hasTop = false;
452 	auto layout = Layout(static_cast<int>(LABEL_WIDTH), Truncate::BACK);
453 	for(const Hardpoint &hardpoint : ship.Weapons())
454 	{
455 		string name = "[empty]";
456 		if(hardpoint.GetOutfit())
457 			name = hardpoint.GetOutfit()->Name();
458 
459 		bool isRight = (hardpoint.GetPoint().X() >= 0.);
460 		bool isTurret = hardpoint.IsTurret();
461 
462 		double &y = nextY[isRight][isTurret];
463 		double x = centerX + (isRight ? LABEL_DX : -LABEL_DX - LABEL_WIDTH);
464 		bool isHover = (index == hoverIndex);
465 		layout.align = isRight ? Alignment::LEFT : Alignment::RIGHT;
466 		font.Draw({name, layout}, Point(x, y + TEXT_OFF), isHover ? bright : dim);
467 		Point zoneCenter(labelCenter[isRight], y + .5 * LINE_HEIGHT);
468 		zones.emplace_back(zoneCenter, LINE_SIZE, index);
469 
470 		// Determine what color to use for the line.
471 		float high = (index == hoverIndex ? .8f : .5f);
472 		Color color(high, .75f * high, 0.f, 1.f);
473 		if(isTurret)
474 			color = Color(0.f, .75f * high, high, 1.f);
475 
476 		// Draw the line.
477 		Point from(fromX[isRight], zoneCenter.Y());
478 		Point to = bounds.Center() + (2. * scale) * hardpoint.GetPoint();
479 		DrawLine(from, to, color);
480 		if(isHover)
481 		{
482 			topFrom = from;
483 			topTo = to;
484 			topColor = color;
485 			hasTop = true;
486 		}
487 
488 		y += LINE_HEIGHT;
489 		++index;
490 	}
491 	// Make sure the line for whatever hardpoint we're hovering is always on top.
492 	if(hasTop)
493 		DrawLine(topFrom, topTo, topColor);
494 
495 	// Re-positioning weapons.
496 	if(draggingIndex >= 0)
497 	{
498 		const Outfit *outfit = ship.Weapons()[draggingIndex].GetOutfit();
499 		string name = outfit ? outfit->Name() : "[empty]";
500 		Point pos(hoverPoint.X() - .5 * font.Width(name), hoverPoint.Y());
501 		font.Draw(name, pos + Point(1., 1.), Color(0., 1.));
502 		font.Draw(name, pos, bright);
503 	}
504 }
505 
506 
507 
DrawCargo(const Rectangle & bounds)508 void ShipInfoPanel::DrawCargo(const Rectangle &bounds)
509 {
510 	Color dim = *GameData::Colors().Get("medium");
511 	Color bright = *GameData::Colors().Get("bright");
512 	Color backColor = *GameData::Colors().Get("faint");
513 	const Ship &ship = **shipIt;
514 
515 	// Cargo list.
516 	const CargoHold &cargo = (player.Cargo().Used() ? player.Cargo() : ship.Cargo());
517 	Table table;
518 	table.AddColumn(0, {COLUMN_WIDTH, Alignment::LEFT});
519 	table.AddColumn(COLUMN_WIDTH, {COLUMN_WIDTH, Alignment::RIGHT});
520 	table.SetUnderline(-5, COLUMN_WIDTH + 5);
521 	table.DrawAt(bounds.TopLeft() + Point(10., 8.));
522 
523 	double endY = bounds.Bottom() - 30. * (cargo.Passengers() != 0);
524 	bool hasSpace = (table.GetRowBounds().Bottom() < endY);
525 	if((cargo.CommoditiesSize() || cargo.HasOutfits() || cargo.MissionCargoSize()) && hasSpace)
526 	{
527 		table.Draw("Cargo", bright);
528 		table.Advance();
529 		hasSpace = (table.GetRowBounds().Bottom() < endY);
530 	}
531 	if(cargo.CommoditiesSize() && hasSpace)
532 	{
533 		for(const auto &it : cargo.Commodities())
534 		{
535 			if(!it.second)
536 				continue;
537 
538 			commodityZones.emplace_back(table.GetCenterPoint(), table.GetRowSize(), it.first);
539 			if(it.first == selectedCommodity)
540 				table.DrawHighlight(backColor);
541 
542 			table.Draw(it.first, dim);
543 			table.Draw(to_string(it.second), bright);
544 
545 			// Truncate the list if there is not enough space.
546 			if(table.GetRowBounds().Bottom() >= endY)
547 			{
548 				hasSpace = false;
549 				break;
550 			}
551 		}
552 		table.DrawGap(10.);
553 	}
554 	if(cargo.HasOutfits() && hasSpace)
555 	{
556 		for(const auto &it : cargo.Outfits())
557 		{
558 			if(!it.second)
559 				continue;
560 
561 			plunderZones.emplace_back(table.GetCenterPoint(), table.GetRowSize(), it.first);
562 			if(it.first == selectedPlunder)
563 				table.DrawHighlight(backColor);
564 
565 			// For outfits, show how many of them you have and their total mass.
566 			bool isSingular = (it.second == 1 || it.first->Get("installable") < 0.);
567 			string name = (isSingular ? it.first->Name() : it.first->PluralName());
568 			if(!isSingular)
569 				name += " (" + to_string(it.second) + "x)";
570 			table.Draw(name, dim);
571 
572 			double mass = it.first->Mass() * it.second;
573 			table.Draw(Format::Number(mass), bright);
574 
575 			// Truncate the list if there is not enough space.
576 			if(table.GetRowBounds().Bottom() >= endY)
577 			{
578 				hasSpace = false;
579 				break;
580 			}
581 		}
582 		table.DrawGap(10.);
583 	}
584 	if(cargo.HasMissionCargo() && hasSpace)
585 	{
586 		for(const auto &it : cargo.MissionCargo())
587 		{
588 			// Capitalize the name of the cargo.
589 			table.Draw(Format::Capitalize(it.first->Cargo()), dim);
590 			table.Draw(to_string(it.second), bright);
591 
592 			// Truncate the list if there is not enough space.
593 			if(table.GetRowBounds().Bottom() >= endY)
594 				break;
595 		}
596 		table.DrawGap(10.);
597 	}
598 	if(cargo.Passengers() && endY >= bounds.Top())
599 	{
600 		table.DrawAt(Point(bounds.Left(), endY) + Point(10., 8.));
601 		table.Draw("passengers:", dim);
602 		table.Draw(to_string(cargo.Passengers()), bright);
603 	}
604 }
605 
606 
607 
DrawLine(const Point & from,const Point & to,const Color & color) const608 void ShipInfoPanel::DrawLine(const Point &from, const Point &to, const Color &color) const
609 {
610 	Color black(0.f, 1.f);
611 	Point mid(to.X(), from.Y());
612 
613 	LineShader::Draw(from, mid, 3.5f, black);
614 	LineShader::Draw(mid, to, 3.5f, black);
615 	LineShader::Draw(from, mid, 1.5f, color);
616 	LineShader::Draw(mid, to, 1.5f, color);
617 }
618 
619 
620 
Hover(const Point & point)621 bool ShipInfoPanel::Hover(const Point &point)
622 {
623 	if(shipIt == player.Ships().end())
624 		return true;
625 
626 	hoverPoint = point;
627 
628 	hoverIndex = -1;
629 	const vector<Hardpoint> &weapons = (**shipIt).Weapons();
630 	bool dragIsTurret = (draggingIndex >= 0 && weapons[draggingIndex].IsTurret());
631 	for(const auto &zone : zones)
632 	{
633 		bool isTurret = weapons[zone.Value()].IsTurret();
634 		if(zone.Contains(hoverPoint) && (draggingIndex == -1 || isTurret == dragIsTurret))
635 			hoverIndex = zone.Value();
636 	}
637 
638 	return true;
639 }
640 
641 
642 
Rename(const string & name)643 void ShipInfoPanel::Rename(const string &name)
644 {
645 	if(shipIt != player.Ships().end() && !name.empty())
646 	{
647 		player.RenameShip(shipIt->get(), name);
648 		UpdateInfo();
649 	}
650 }
651 
652 
653 
CanDump() const654 bool ShipInfoPanel::CanDump() const
655 {
656 	if(canEdit || shipIt == player.Ships().end())
657 		return false;
658 
659 	CargoHold &cargo = (*shipIt)->Cargo();
660 	return (selectedPlunder && cargo.Get(selectedPlunder) > 0) || cargo.CommoditiesSize() || cargo.OutfitsSize();
661 }
662 
663 
664 
Dump()665 void ShipInfoPanel::Dump()
666 {
667 	if(!CanDump())
668 		return;
669 
670 	CargoHold &cargo = (*shipIt)->Cargo();
671 	int commodities = (*shipIt)->Cargo().CommoditiesSize();
672 	int amount = cargo.Get(selectedCommodity);
673 	int plunderAmount = cargo.Get(selectedPlunder);
674 	int64_t loss = 0;
675 	if(amount)
676 	{
677 		int64_t basis = player.GetBasis(selectedCommodity, amount);
678 		loss += basis;
679 		player.AdjustBasis(selectedCommodity, -basis);
680 		(*shipIt)->Jettison(selectedCommodity, amount);
681 	}
682 	else if(plunderAmount > 0)
683 	{
684 		loss += plunderAmount * selectedPlunder->Cost();
685 		(*shipIt)->Jettison(selectedPlunder, plunderAmount);
686 	}
687 	else if(commodities)
688 	{
689 		for(const auto &it : cargo.Commodities())
690 		{
691 			int64_t basis = player.GetBasis(it.first, it.second);
692 			loss += basis;
693 			player.AdjustBasis(it.first, -basis);
694 			(*shipIt)->Jettison(it.first, it.second);
695 		}
696 	}
697 	else
698 	{
699 		for(const auto &it : cargo.Outfits())
700 		{
701 			loss += it.first->Cost() * max(0, it.second);
702 			(*shipIt)->Jettison(it.first, it.second);
703 		}
704 	}
705 	selectedCommodity.clear();
706 	selectedPlunder = nullptr;
707 
708 	info.Update(**shipIt, player.FleetDepreciation(), player.GetDate().DaysSinceEpoch());
709 	if(loss)
710 		Messages::Add("You jettisoned " + Format::Credits(loss) + " credits worth of cargo.");
711 }
712 
713 
714 
DumpPlunder(int count)715 void ShipInfoPanel::DumpPlunder(int count)
716 {
717 	int64_t loss = 0;
718 	count = min(count, (*shipIt)->Cargo().Get(selectedPlunder));
719 	if(count > 0)
720 	{
721 		loss += count * selectedPlunder->Cost();
722 		(*shipIt)->Jettison(selectedPlunder, count);
723 		info.Update(**shipIt, player.FleetDepreciation(), player.GetDate().DaysSinceEpoch());
724 
725 		if(loss)
726 			Messages::Add("You jettisoned " + Format::Credits(loss) + " credits worth of cargo.");
727 	}
728 }
729 
730 
731 
DumpCommodities(int count)732 void ShipInfoPanel::DumpCommodities(int count)
733 {
734 	int64_t loss = 0;
735 	count = min(count, (*shipIt)->Cargo().Get(selectedCommodity));
736 	if(count > 0)
737 	{
738 		int64_t basis = player.GetBasis(selectedCommodity, count);
739 		loss += basis;
740 		player.AdjustBasis(selectedCommodity, -basis);
741 		(*shipIt)->Jettison(selectedCommodity, count);
742 		info.Update(**shipIt, player.FleetDepreciation(), player.GetDate().DaysSinceEpoch());
743 
744 		if(loss)
745 			Messages::Add("You jettisoned " + Format::Credits(loss) + " credits worth of cargo.");
746 	}
747 }
748 
749 
750 
Disown()751 void ShipInfoPanel::Disown()
752 {
753 	// Make sure a ship really is selected.
754 	if(shipIt == player.Ships().end() || shipIt->get() == player.Flagship())
755 		return;
756 
757 	// Because you can never disown your flagship, the player's ship list will
758 	// never become empty as a result of disowning a ship.
759 	const Ship *ship = shipIt->get();
760 	if(shipIt != player.Ships().begin())
761 		--shipIt;
762 
763 	player.DisownShip(ship);
764 	UpdateInfo();
765 }
766