1 #include "FleetButton.h"
2
3 #include "FleetWnd.h"
4 #include "MapWnd.h"
5 #include "Sound.h"
6 #include "CUIDrawUtil.h"
7 #include "CUIControls.h"
8 #include "../util/i18n.h"
9 #include "../util/Logger.h"
10 #include "../util/OptionsDB.h"
11 #include "../client/human/HumanClientApp.h"
12 #include "../universe/Fleet.h"
13 #include "../universe/Ship.h"
14 #include "../universe/System.h"
15 #include "../universe/Enums.h"
16 #include "../Empire/Empire.h"
17
18 #include <GG/GLClientAndServerBuffer.h>
19
20 #include <algorithm>
21
22
23 namespace {
24 /* returns number of fleet icon size texture to use to represent fleet(s) with the passed number of ships */
FleetSizeIconNumber(int number_ships)25 int FleetSizeIconNumber(int number_ships) {
26 // one ship (or zero?) has no marker. more marker levels are used for each doubling in the number of ships
27 number_ships = std::min(std::max(number_ships, 1), 129); // smallest size indicator is for 1 ship, largest is for 128 or greater
28 if (number_ships < 2)
29 return 0;
30 else if (number_ships < 4)
31 return 1;
32 else if (number_ships < 8)
33 return 2;
34 else if (number_ships < 16)
35 return 3;
36 else if (number_ships < 32)
37 return 4;
38 else if (number_ships < 64)
39 return 5;
40 else if (number_ships < 128)
41 return 6;
42 else //(number_ships >= 128)
43 return 7;
44 }
45
46 /* returns prefix of filename used for icons for the indicated fleet button size type */
FleetIconSizePrefix(FleetButton::SizeType size_type)47 std::string FleetIconSizePrefix(FleetButton::SizeType size_type) {
48 if (size_type == FleetButton::SizeType::LARGE)
49 return "big-";
50 else if (size_type == FleetButton::SizeType::MEDIUM)
51 return "med-";
52 else if (size_type == FleetButton::SizeType::SMALL)
53 return "sml-";
54 else
55 return "";
56 }
57
AddOptions(OptionsDB & db)58 void AddOptions(OptionsDB& db) {
59 db.Add("ui.map.fleet.select.indicator.size", UserStringNop("OPTIONS_DB_UI_FLEET_SELECTION_INDICATOR_SIZE"), 1.625, RangedStepValidator<double>(0.125, 0.5, 5));
60 }
61 bool temp_bool = RegisterOptions(&AddOptions);
62
63 const float TWO_PI = 2.0*3.14159f;
64 }
65
66 ///////////////////////////
67 // FleetButton //
68 ///////////////////////////
FleetButton(int fleet_id,SizeType size_type)69 FleetButton::FleetButton(int fleet_id, SizeType size_type) :
70 FleetButton(std::vector<int>(1, fleet_id), size_type)
71 {}
72
FleetButton(const std::vector<int> & fleet_IDs,SizeType size_type)73 FleetButton::FleetButton(const std::vector<int>& fleet_IDs, SizeType size_type) :
74 GG::Button("", nullptr, GG::CLR_ZERO)
75 {
76 std::vector<std::shared_ptr<const Fleet>> fleets;
77 fleets.reserve(fleet_IDs.size());
78 for (const auto& fleet : Objects().find<Fleet>(fleet_IDs)) {
79 if (!fleet)
80 continue;
81 m_fleets.push_back(fleet->ID());
82 fleets.push_back(fleet);
83 }
84
85 // determine owner(s) of fleet(s). Only care whether or not there is more than one owner, as owner
86 // is used to determine colouration
87 int owner_id = ALL_EMPIRES;
88 int multiple_owners = false;
89 if (fleets.empty()) {
90 // leave as ALL_EMPIRES
91 } else if (fleets.size() == 1) {
92 owner_id = (*fleets.begin())->Owner();
93 } else {
94 owner_id = (*fleets.begin())->Owner();
95 // use ALL_EMPIRES if there are multiple owners (including no owner and an owner)
96 for (auto& fleet : fleets) {
97 if (fleet->Owner() != owner_id) {
98 owner_id = ALL_EMPIRES;
99 multiple_owners = true;
100 break;
101 }
102 }
103 }
104
105
106 // get fleet colour
107 if (multiple_owners) {
108 SetColor(GG::CLR_WHITE);
109 } else if (owner_id == ALL_EMPIRES) {
110 // all ships owned by now empire
111 bool monsters = true;
112 // find if any ship in fleets in button is not a monster
113 for (auto& fleet : fleets) {
114 for (const auto& ship : Objects().find<Ship>(fleet->ShipIDs())) {
115 if (!ship)
116 continue;
117 if (!ship->IsMonster()) {
118 monsters = false;
119 break;
120 }
121 }
122 }
123
124 if (monsters)
125 SetColor(GG::CLR_RED);
126 else
127 SetColor(GG::CLR_WHITE);
128 } else {
129 // single empire owner
130 if (const Empire* empire = GetEmpire(owner_id))
131 SetColor(empire->Color());
132 else
133 SetColor(GG::CLR_GRAY); // should never be necessary... but just in case
134 }
135
136
137 // determine direction button should be rotated to orient along a starlane
138 GLfloat pointing_angle = 0.0f;
139
140 std::shared_ptr<const Fleet> first_fleet;
141 if (!m_fleets.empty())
142 first_fleet = *fleets.begin();
143 if (first_fleet && first_fleet->SystemID() == INVALID_OBJECT_ID && first_fleet->NextSystemID() != INVALID_OBJECT_ID) {
144 int next_sys_id = first_fleet->NextSystemID();
145 if (auto obj = Objects().get(next_sys_id)) {
146 // fleet is not in a system and has a valid next destination, so can orient it in that direction
147 // fleet icons might not appear on the screen in the exact place corresponding to their
148 // actual universe position, but if they're moving along a starlane, this code will assume
149 // their apparent position will only be different from their true position in a direction
150 // parallel with the starlane, so the direction from their true position to their destination
151 // position can be used to get a direction vector to orient the icon
152 float dest_x = obj->X(), dest_y = obj->Y();
153 float cur_x = first_fleet->X(), cur_y = first_fleet->Y();
154 const auto& map_wnd = ClientUI::GetClientUI()->GetMapWnd();
155 GG::Pt dest = map_wnd->ScreenCoordsFromUniversePosition(dest_x, dest_y);
156 GG::Pt cur = map_wnd->ScreenCoordsFromUniversePosition(cur_x, cur_y);
157 GG::Pt direction_vector = dest - cur;
158
159 if (direction_vector.x != GG::X0 || direction_vector.y != GG::Y0)
160 pointing_angle = 360.0f / TWO_PI * std::atan2(static_cast<float>(Value(direction_vector.y)), static_cast<float>(Value(direction_vector.x))) + 90;
161 }
162 }
163
164 // select icon(s) for fleet(s)
165 int num_ships = 0;
166 m_fleet_blockaded = false;
167 for (auto& fleet : fleets) {
168 if (fleet) {
169 num_ships += fleet->NumShips();
170 if (!m_fleet_blockaded && fleet->Blockaded())
171 m_fleet_blockaded = true;
172 }
173 }
174
175 // add graphics for all icons needed
176 m_icons.reserve(4);
177
178 if (m_fleet_blockaded) {
179 if (auto blockaded_texture = FleetBlockadedIcon(size_type)) {
180 auto icon = GG::Wnd::Create<GG::StaticGraphic>(blockaded_texture, GG::GRAPHIC_FITGRAPHIC | GG::GRAPHIC_PROPSCALE);
181 GG::Clr opposite_clr(255 - this->Color().r, 255 - this->Color().g, 255 - this->Color().b, this->Color().a);
182 icon->SetColor(opposite_clr);
183 m_icons.push_back(icon);
184 }
185 }
186
187 if (auto size_texture = FleetSizeIcon(num_ships, size_type)) {
188 auto icon = GG::Wnd::Create<RotatingGraphic>(size_texture, GG::GRAPHIC_FITGRAPHIC | GG::GRAPHIC_PROPSCALE);
189 icon->SetPhaseOffset(pointing_angle);
190 icon->SetRPM(0.0f);
191 icon->SetColor(this->Color());
192 m_icons.push_back(icon);
193 Resize(GG::Pt(size_texture->DefaultWidth(), size_texture->DefaultHeight()));
194 }
195
196 for (auto& texture : FleetHeadIcons(fleets, size_type)) {
197 auto icon = GG::Wnd::Create<RotatingGraphic>(texture, GG::GRAPHIC_FITGRAPHIC | GG::GRAPHIC_PROPSCALE);
198 icon->SetPhaseOffset(pointing_angle);
199 icon->SetRPM(0.0f);
200 icon->SetColor(this->Color());
201 m_icons.push_back(icon);
202 if (Width() < texture->DefaultWidth())
203 Resize(GG::Pt(texture->DefaultWidth(), texture->DefaultHeight()));
204 }
205
206 // set up selection indicator
207 m_selection_indicator = GG::Wnd::Create<RotatingGraphic>(FleetSelectionIndicatorIcon(), GG::GRAPHIC_FITGRAPHIC | GG::GRAPHIC_PROPSCALE);
208 m_selection_indicator->SetRPM(ClientUI::SystemSelectionIndicatorRPM());
209
210 LayoutIcons();
211
212 // Scanlines for not currently-visible objects?
213 int empire_id = HumanClientApp::GetApp()->EmpireID();
214 if (empire_id == ALL_EMPIRES || !GetOptionsDB().Get<bool>("ui.map.scanlines.shown"))
215 return;
216
217 // Create scanline renderer control, use opposite color of fleet btn
218 GG::Clr opposite_clr(255 - Color().r, 255 - Color().g, 255 - Color().b, 64);
219 m_scanline_control = GG::Wnd::Create<ScanlineControl>(GG::X0, GG::Y0, Width(), Height(), false, opposite_clr);
220 }
221
CompleteConstruction()222 void FleetButton::CompleteConstruction() {
223 Button::CompleteConstruction();
224
225 for (auto& icon: m_icons)
226 AttachChild(icon);
227
228 // Scanlines for not currently-visible objects?
229 int empire_id = HumanClientApp::GetApp()->EmpireID();
230 if (empire_id == ALL_EMPIRES || !GetOptionsDB().Get<bool>("ui.map.scanlines.shown"))
231 return;
232
233 bool at_least_one_fleet_visible = false;
234 for (int fleet_id : m_fleets) {
235 if (GetUniverse().GetObjectVisibilityByEmpire(fleet_id, empire_id) >= VIS_BASIC_VISIBILITY) {
236 at_least_one_fleet_visible = true;
237 break;
238 }
239 }
240
241 if (!at_least_one_fleet_visible)
242 AttachChild(m_scanline_control);
243
244 SetBrowseModeTime(GetOptionsDB().Get<int>("ui.tooltip.delay"));
245 }
246
~FleetButton()247 FleetButton::~FleetButton()
248 {}
249
InWindow(const GG::Pt & pt) const250 bool FleetButton::InWindow(const GG::Pt& pt) const {
251 // find if cursor is within required distance of centre of icon
252 GG::Pt ul = UpperLeft(), lr = LowerRight();
253 const float midX = Value(ul.x + lr.x)/2.0f;
254 const float midY = Value(ul.y + lr.y)/2.0f;
255
256 const float RADIUS2 = Value(Width())*Value(Width())/4.0f;
257
258 const float ptX = Value(pt.x);
259 const float ptY = Value(pt.y);
260
261 const float distx = ptX - midX, disty = ptY - midY;
262
263 return distx*distx + disty*disty <= RADIUS2;
264 }
265
MouseHere(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)266 void FleetButton::MouseHere(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) {
267 const auto& map_wnd = ClientUI::GetClientUI()->GetMapWnd();
268 if (!Disabled() && (!map_wnd || !map_wnd->InProductionViewMode())) {
269 if (State() != BN_ROLLOVER)
270 PlayFleetButtonRolloverSound();
271 SetState(BN_ROLLOVER);
272 }
273 }
274
SizeMove(const GG::Pt & ul,const GG::Pt & lr)275 void FleetButton::SizeMove(const GG::Pt& ul, const GG::Pt& lr) {
276 GG::Pt sz = Size();
277
278 Button::SizeMove(ul, lr);
279
280 if (sz == Size())
281 return;
282
283 LayoutIcons();
284 }
285
LayoutIcons()286 void FleetButton::LayoutIcons() {
287 GG::Pt middle = GG::Pt(Width() / 2, Height() / 2);
288 for (auto& graphic : m_icons) {
289 GG::SubTexture subtexture = graphic->GetTexture();
290 GG::Pt subtexture_sz = GG::Pt(subtexture.Width(), subtexture.Height());
291 GG::Pt graphic_ul = middle - GG::Pt(subtexture_sz.x / 2, subtexture_sz.y / 2);
292 graphic->SizeMove(graphic_ul, graphic_ul + subtexture_sz);
293 }
294
295 if (m_selection_indicator) {
296 //GG::SubTexture subtexture = m_selection_indicator->GetTexture();
297 //GG::Pt subtexture_sz = GG::Pt(subtexture.Width(), subtexture.Height());
298 double sel_ind_scale = GetOptionsDB().Get<double>("ui.map.fleet.select.indicator.size");
299 GG::Pt subtexture_sz = Size() * sel_ind_scale;
300 GG::Pt graphic_ul = middle - subtexture_sz / 2;
301 m_selection_indicator->SizeMove(graphic_ul, graphic_ul + subtexture_sz);
302 }
303
304 // refresh fleet button tooltip
305 if (m_fleet_blockaded) {
306 std::shared_ptr<Fleet> fleet;
307 std::string available_exits = "";
308 int available_exits_count = 0;
309
310 if (!m_fleets.empty())
311 // can just pick first fleet because all fleets in system should have same exits
312 fleet = Objects().get<Fleet>(*m_fleets.begin());
313 else return;
314
315 for (const auto& target_system_id : Objects().get<System>(fleet->SystemID())->StarlanesWormholes()) {
316 if (fleet->BlockadedAtSystem(fleet->SystemID(), target_system_id.first))
317 continue;
318
319 auto target_system = Objects().get<System>(target_system_id.first);
320 if (target_system) {
321 available_exits += "\n" + target_system->ApparentName(HumanClientApp::GetApp()->EmpireID());
322 available_exits_count++;
323 }
324 }
325
326 if (fleet->Owner() == ALL_EMPIRES) // as above, if first fleet of fleet-button is "monster-owned", all fleets are
327 SetBrowseText(UserString("FB_TOOLTIP_BLOCKADE_MONSTER"));
328 else if (available_exits_count >= 1)
329 SetBrowseText(UserString("FB_TOOLTIP_BLOCKADE_WITH_EXIT") + available_exits);
330 else
331 SetBrowseText(UserString("FB_TOOLTIP_BLOCKADE_NO_EXIT"));
332 } else {
333 ClearBrowseInfoWnd();
334 }
335 }
336
SetSelected(bool selected)337 void FleetButton::SetSelected(bool selected) {
338 m_selected = selected;
339
340 if (!m_selected) {
341 DetachChild(m_selection_indicator);
342 m_selection_indicator->Hide();
343 return;
344 }
345
346 AttachChild(m_selection_indicator);
347 m_selection_indicator->Show();
348 MoveChildDown(m_selection_indicator);
349 LayoutIcons();
350 }
351
RenderUnpressed()352 void FleetButton::RenderUnpressed() {
353 // GG::Pt ul = UpperLeft(), lr = LowerRight();
354 // const float midX = Value(ul.x + lr.x)/2.0f;
355 // const float midY = Value(ul.y + lr.y)/2.0f;
356
357 //// debug
358 //GG::FlatRectangle(ul, lr, GG::CLR_ZERO, GG::CLR_RED, 2);
359 //// end debug
360 }
361
RenderPressed()362 void FleetButton::RenderPressed() {
363 glDisable(GL_TEXTURE_2D);
364 glColor(Color());
365 CircleArc(UpperLeft(), LowerRight(), 0.0, TWO_PI, true);
366 glEnable(GL_TEXTURE_2D);
367
368 RenderUnpressed();
369 }
370
RenderRollover()371 void FleetButton::RenderRollover() {
372 glDisable(GL_TEXTURE_2D);
373 glColor(GG::CLR_WHITE);
374 CircleArc(UpperLeft(), LowerRight(), 0.0, TWO_PI, true);
375 glEnable(GL_TEXTURE_2D);
376
377 RenderUnpressed();
378 }
379
PlayFleetButtonRolloverSound()380 void FleetButton::PlayFleetButtonRolloverSound()
381 { Sound::GetSound().PlaySound(GetOptionsDB().Get<std::string>("ui.map.fleet.button.rollover.sound.path"), true); }
382
PlayFleetButtonOpenSound()383 void FleetButton::PlayFleetButtonOpenSound()
384 { Sound::GetSound().PlaySound(GetOptionsDB().Get<std::string>("ui.map.fleet.button.press.sound.path"), true); }
385
386 /////////////////////
387 // Free Functions
388 /////////////////////
FleetHeadIcons(std::shared_ptr<const Fleet> fleet,FleetButton::SizeType size_type)389 std::vector<std::shared_ptr<GG::Texture>> FleetHeadIcons(std::shared_ptr<const Fleet> fleet, FleetButton::SizeType size_type) {
390 std::vector<std::shared_ptr<const Fleet>> fleets(1U, fleet);
391 return FleetHeadIcons(fleets, size_type);
392 }
393
FleetHeadIcons(const std::vector<std::shared_ptr<const Fleet>> & fleets,FleetButton::SizeType size_type)394 std::vector<std::shared_ptr<GG::Texture>> FleetHeadIcons(const std::vector<std::shared_ptr<const Fleet>>& fleets, FleetButton::SizeType size_type) {
395 if (size_type == FleetButton::SizeType::NONE || size_type == FleetButton::SizeType::TINY)
396 return std::vector<std::shared_ptr<GG::Texture>>();
397
398 // get file name prefix for appropriate size of icon
399 std::string size_prefix = FleetIconSizePrefix(size_type);
400 if (size_prefix.empty())
401 return std::vector<std::shared_ptr<GG::Texture>>();
402
403 // the set of fleets is treated like a fleet that contains all the ships
404 bool hasColonyShips = false; bool hasOutpostShips = false; bool hasTroopShips = false; bool hasMonsters = false; bool hasArmedShips = false;
405 for (auto& fleet : fleets) {
406 if (!fleet)
407 continue;
408
409 hasColonyShips = hasColonyShips || fleet->HasColonyShips();
410 hasOutpostShips = hasOutpostShips || fleet->HasOutpostShips();
411 hasTroopShips = hasTroopShips || fleet->HasTroopShips();
412 hasMonsters = hasMonsters || fleet->HasMonsters();
413 hasArmedShips = hasArmedShips || fleet->HasArmedShips() || fleet->HasFighterShips();
414 }
415
416 // get file name main part depending on type of fleet
417 // symbol type prioritized by the ship type arbitrarily deemed "most important"
418 std::vector<std::string> main_filenames;
419 if (hasMonsters) {
420 if (hasArmedShips) { main_filenames.push_back("head-monster.png"); }
421 else { main_filenames.push_back("head-monster-harmless.png"); }
422 } else {
423 main_filenames.reserve(4);
424 if (hasArmedShips) { main_filenames.push_back("head-warship.png"); }
425 if (hasColonyShips) { main_filenames.push_back("head-colony.png"); }
426 if (hasOutpostShips) { main_filenames.push_back("head-outpost.png"); }
427 if (hasTroopShips) { main_filenames.push_back("head-lander.png"); }
428 }
429 if (main_filenames.empty()) { main_filenames.push_back("head-scout.png"); }
430
431 std::vector<std::shared_ptr<GG::Texture>> result;
432 result.reserve(main_filenames.size());
433 for (const std::string& name : main_filenames) {
434 std::shared_ptr<GG::Texture> texture_temp = ClientUI::GetTexture(
435 ClientUI::ArtDir() / "icons" / "fleet" / (size_prefix + name), false);
436 glBindTexture(GL_TEXTURE_2D, texture_temp->OpenGLId());
437 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
438 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
439 glBindTexture(GL_TEXTURE_2D, 0);
440
441 result.push_back(texture_temp);
442 }
443
444 return result;
445 }
446
FleetSizeIcon(std::shared_ptr<const Fleet> fleet,FleetButton::SizeType size_type)447 std::shared_ptr<GG::Texture> FleetSizeIcon(std::shared_ptr<const Fleet> fleet, FleetButton::SizeType size_type) {
448 if (!fleet)
449 return FleetSizeIcon(1u, size_type);
450 return FleetSizeIcon(fleet->NumShips(), size_type);
451 }
452
FleetSizeIcon(unsigned int fleet_size,FleetButton::SizeType size_type)453 std::shared_ptr<GG::Texture> FleetSizeIcon(unsigned int fleet_size, FleetButton::SizeType size_type) {
454 if (fleet_size < 1u)
455 fleet_size = 1u; // because there's no zero-ship icon, and the one-ship icon is (as of this writing) blank, so is fitting for zero ships
456
457 if (size_type == FleetButton::SizeType::NONE)
458 return nullptr;
459
460 if (size_type == FleetButton::SizeType::TINY) {
461 if (fleet_size > 1u)
462 return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "fleet" / "tiny-fleet-multi.png", false);
463 else
464 return ClientUI::GetTexture(ClientUI::ArtDir() / "icons" / "fleet" / "tiny-fleet.png", false);
465 }
466
467 std::string size_prefix = FleetIconSizePrefix(size_type);
468
469 if (size_prefix.empty())
470 return nullptr;
471
472 std::shared_ptr<GG::Texture> texture_temp = ClientUI::GetClientUI()->GetModuloTexture(
473 ClientUI::ArtDir() / "icons" / "fleet", (size_prefix + "tail-"), FleetSizeIconNumber(fleet_size), false);
474 glBindTexture(GL_TEXTURE_2D, texture_temp->OpenGLId());
475 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
476 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
477 glBindTexture(GL_TEXTURE_2D, 0);
478
479 return texture_temp;
480 }
481
FleetBlockadedIcon(FleetButton::SizeType size_type)482 std::shared_ptr<GG::Texture> FleetBlockadedIcon(FleetButton::SizeType size_type) {
483 if (size_type == FleetButton::SizeType::NONE)
484 return nullptr;
485
486 std::string size_prefix = FleetIconSizePrefix(size_type);
487 if (size_type == FleetButton::SizeType::TINY)
488 size_prefix = "tiny-";
489
490 std::shared_ptr<GG::Texture> retval = ClientUI::GetClientUI()->GetTexture(ClientUI::ArtDir() / "icons" / "fleet" / (size_prefix + "blockade.png"), false);
491 glBindTexture(GL_TEXTURE_2D, retval->OpenGLId());
492 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
493 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
494 glBindTexture(GL_TEXTURE_2D, 0);
495
496 return retval;
497 }
498
FleetSelectionIndicatorIcon()499 std::shared_ptr<GG::Texture> FleetSelectionIndicatorIcon() {
500 static std::shared_ptr<GG::Texture> retval;
501 if (!retval) {
502 retval = ClientUI::GetClientUI()->GetTexture(ClientUI::ArtDir() / "icons" / "fleet" / "fleet_selection.png", false);
503 glBindTexture(GL_TEXTURE_2D, retval->OpenGLId());
504 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
505 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
506 glBindTexture(GL_TEXTURE_2D, 0);
507 }
508 return retval;
509 }
510