1 /* EscortDisplay.cpp
2 Copyright (c) 2015 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 "EscortDisplay.h"
14 
15 #include "Color.h"
16 #include "text/Font.h"
17 #include "text/FontSet.h"
18 #include "GameData.h"
19 #include "Government.h"
20 #include "LineShader.h"
21 #include "OutlineShader.h"
22 #include "Point.h"
23 #include "Rectangle.h"
24 #include "Ship.h"
25 #include "Sprite.h"
26 #include "System.h"
27 
28 #include <algorithm>
29 #include <set>
30 
31 using namespace std;
32 
33 namespace {
34 	//  Horizontal layout of each escort icon:
35 	// (PAD) ICON (BAR_PAD) BARS (BAR_PAD) (PAD)
36 	const double PAD = 10.;
37 	const double ICON_SIZE = 20.;
38 	const double BAR_PAD = 5.;
39 	const double WIDTH = 120.;
40 	const double BAR_WIDTH = WIDTH - ICON_SIZE - 2. * PAD - 2. * BAR_PAD;
41 }
42 
43 
44 
Clear()45 void EscortDisplay::Clear()
46 {
47 	icons.clear();
48 }
49 
50 
51 
Add(const Ship & ship,bool isHere,bool fleetIsJumping,bool isSelected)52 void EscortDisplay::Add(const Ship &ship, bool isHere, bool fleetIsJumping, bool isSelected)
53 {
54 	icons.emplace_back(ship, isHere, fleetIsJumping, isSelected);
55 }
56 
57 
58 
59 // Draw as many escort icons as will fit in the given bounding box.
Draw(const Rectangle & bounds) const60 void EscortDisplay::Draw(const Rectangle &bounds) const
61 {
62 	// Figure out how much space there is for the icons.
63 	int maxColumns = max(1., bounds.Width() / WIDTH);
64 	MergeStacks(maxColumns * bounds.Height());
65 	icons.sort();
66 	stacks.clear();
67 	zones.clear();
68 	static const Set<Color> &colors = GameData::Colors();
69 
70 	// Draw escort status.
71 	const Font &font = FontSet::Get(14);
72 	// Top left corner of the current escort icon.
73 	Point corner = Point(bounds.Left(), bounds.Bottom());
74 	const Color &elsewhereColor = *colors.Get("escort elsewhere");
75 	const Color &cannotJumpColor = *colors.Get("escort blocked");
76 	const Color &notReadyToJumpColor = *colors.Get("escort not ready");
77 	const Color &selectedColor = *colors.Get("escort selected");
78 	const Color &hereColor = *colors.Get("escort present");
79 	const Color &hostileColor = *colors.Get("escort hostile");
80 	for(const Icon &escort : icons)
81 	{
82 		if(!escort.sprite)
83 			continue;
84 
85 		corner.Y() -= escort.Height();
86 		// Show only as many escorts as we have room for on screen.
87 		if(corner.Y() <= bounds.Top())
88 		{
89 			corner.X() += WIDTH;
90 			if(corner.X() + WIDTH > bounds.Right())
91 				break;
92 			corner.Y() = bounds.Bottom() - escort.Height();
93 		}
94 		Point pos = corner + Point(PAD + .5 * ICON_SIZE, .5 * ICON_SIZE);
95 
96 		// Draw the system name for any escort not in the current system.
97 		if(!escort.system.empty())
98 			font.Draw(escort.system, pos + Point(-10., 10.), elsewhereColor);
99 
100 		Color color;
101 		if(escort.isHostile)
102 			color = hostileColor;
103 		else if(!escort.isHere)
104 			color = elsewhereColor;
105 		else if(escort.cannotJump)
106 			color = cannotJumpColor;
107 		else if(escort.notReadyToJump)
108 			color = notReadyToJumpColor;
109 		else if(escort.isSelected)
110 			color = selectedColor;
111 		else
112 			color = hereColor;
113 
114 		// Figure out what scale should be applied to the ship sprite.
115 		float scale = min(ICON_SIZE / escort.sprite->Width(), ICON_SIZE / escort.sprite->Height());
116 		Point size(escort.sprite->Width() * scale, escort.sprite->Height() * scale);
117 		OutlineShader::Draw(escort.sprite, pos, size, color);
118 		zones.push_back(pos);
119 		stacks.push_back(escort.ships);
120 		// Draw the number of ships in this stack.
121 		double width = BAR_WIDTH;
122 		if(escort.ships.size() > 1)
123 		{
124 			string number = to_string(escort.ships.size());
125 
126 			Point numberPos = pos;
127 			numberPos.X() += 15. + width - font.Width(number);
128 			numberPos.Y() -= .5 * font.Height();
129 			font.Draw(number, numberPos, elsewhereColor);
130 			width -= 20.;
131 		}
132 
133 		// Draw the status bars.
134 		static const Color fullColor[5] = {
135 			colors.Get("shields")->Additive(1.), colors.Get("hull")->Additive(1.),
136 			colors.Get("energy")->Additive(1.), colors.Get("heat")->Additive(1.), colors.Get("fuel")->Additive(1.)
137 		};
138 		static const Color halfColor[5] = {
139 			fullColor[0].Additive(.5), fullColor[1].Additive(.5),
140 			fullColor[2].Additive(.5), fullColor[3].Additive(.5), fullColor[4].Additive(.5),
141 		};
142 		Point from(pos.X() + .5 * ICON_SIZE + BAR_PAD, pos.Y() - 8.5);
143 		for(int i = 0; i < 5; ++i)
144 		{
145 			// If the low and high levels are different, draw a fully opaque bar up
146 			// to the low limit, and half-transparent up to the high limit.
147 			if(escort.high[i] > 0.)
148 			{
149 				bool isSplit = (escort.low[i] != escort.high[i]);
150 				const Color &color = (isSplit ? halfColor : fullColor)[i];
151 
152 				Point to = from + Point(width * min(1., escort.high[i]), 0.);
153 				LineShader::Draw(from, to, 1.5f, color);
154 
155 				if(isSplit)
156 				{
157 					Point to = from + Point(width * max(0., escort.low[i]), 0.);
158 					LineShader::Draw(from, to, 1.5f, color);
159 				}
160 			}
161 			from.Y() += 4.;
162 			if(i == 1)
163 			{
164 				from.X() += 5.;
165 				width -= 5.;
166 			}
167 		}
168 	}
169 }
170 
171 
172 
173 // Check if the given point is a click on an escort icon. If so, return the
174 // stack of ships represented by the icon. Otherwise, return an empty stack.
Click(const Point & point) const175 const vector<const Ship *> &EscortDisplay::Click(const Point &point) const
176 {
177 	for(unsigned i = 0; i < zones.size(); ++i)
178 		if(point.Distance(zones[i]) < 15.)
179 			return stacks[i];
180 
181 	static const vector<const Ship *> empty;
182 	return empty;
183 }
184 
185 
186 
Icon(const Ship & ship,bool isHere,bool fleetIsJumping,bool isSelected)187 EscortDisplay::Icon::Icon(const Ship &ship, bool isHere, bool fleetIsJumping, bool isSelected)
188 	: sprite(ship.GetSprite()),
189 	isHere(isHere && !ship.IsDisabled()),
190 	isHostile(ship.GetGovernment() && ship.GetGovernment()->IsEnemy()),
191 	notReadyToJump(fleetIsJumping && !ship.IsHyperspacing() && !ship.IsReadyToJump(true)),
192 	cannotJump(fleetIsJumping && !ship.IsHyperspacing() && !ship.JumpsRemaining()),
193 	isSelected(isSelected),
194 	cost(ship.Cost()),
195 	system((!isHere && ship.GetSystem()) ? ship.GetSystem()->Name() : ""),
196 	low{ship.Shields(), ship.Hull(), ship.Energy(), ship.Heat(), ship.Fuel()},
197 	high(low),
198 	ships(1, &ship)
199 {
200 }
201 
202 
203 
204 // Sorting operator. It comes sooner if it costs more.
operator <(const Icon & other) const205 bool EscortDisplay::Icon::operator<(const Icon &other) const
206 {
207 	return (cost > other.cost);
208 }
209 
210 
211 
Height() const212 int EscortDisplay::Icon::Height() const
213 {
214 	return 30 + 15 * !system.empty();
215 }
216 
217 
218 
Merge(const Icon & other)219 void EscortDisplay::Icon::Merge(const Icon &other)
220 {
221 	isHere &= other.isHere;
222 	isHostile |= other.isHostile;
223 	notReadyToJump |= other.notReadyToJump;
224 	cannotJump |= other.cannotJump;
225 	isSelected |= other.isSelected;
226 	if(system.empty() && !other.system.empty())
227 		system = other.system;
228 
229 	for(unsigned i = 0; i < low.size(); ++i)
230 	{
231 		low[i] = min(low[i], other.low[i]);
232 		high[i] = max(high[i], other.high[i]);
233 	}
234 	ships.insert(ships.end(), other.ships.begin(), other.ships.end());
235 }
236 
237 
238 
MergeStacks(int maxHeight) const239 void EscortDisplay::MergeStacks(int maxHeight) const
240 {
241 	if(icons.empty())
242 		return;
243 
244 	set<const Sprite *> unstackable;
245 	while(true)
246 	{
247 		Icon *cheapest = nullptr;
248 
249 		int height = 0;
250 		for(Icon &icon : icons)
251 		{
252 			if(!unstackable.count(icon.sprite) && (!cheapest || *cheapest < icon))
253 				cheapest = &icon;
254 
255 			height += icon.Height();
256 		}
257 
258 		if(height < maxHeight || !cheapest)
259 			break;
260 
261 		// Merge together each group of escorts that have this icon and are in
262 		// the same system and have the same attitude towards the player.
263 		map<const bool, map<string, Icon *>> merged;
264 
265 		// The "cheapest" element in the list may be removed to merge it with an
266 		// earlier ship of the same type, so store a copy of its sprite pointer:
267 		const Sprite *sprite = cheapest->sprite;
268 		list<Icon>::iterator it = icons.begin();
269 		while(it != icons.end())
270 		{
271 			if(it->sprite != sprite)
272 			{
273 				++it;
274 				continue;
275 			}
276 
277 			// If this is the first escort we've seen so far in its system, it
278 			// is the one we will merge all others in this system into.
279 			auto mit = merged[it->isHostile].find(it->system);
280 			if(mit == merged[it->isHostile].end())
281 			{
282 				merged[it->isHostile][it->system] = &*it;
283 				++it;
284 			}
285 			else
286 			{
287 				mit->second->Merge(*it);
288 				it = icons.erase(it);
289 			}
290 		}
291 		unstackable.insert(sprite);
292 	}
293 }
294