1 /*
2  * Copyright (C) 2002-2020 by the Widelands Development Team
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public License
6  * as published by the Free Software Foundation; either version 2
7  * of the License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17  *
18  */
19 
20 #include "wui/soldierlist.h"
21 
22 #include <functional>
23 
24 #include <SDL_mouse.h>
25 #include <SDL_timer.h>
26 
27 #include "base/macros.h"
28 #include "graphic/font_handler.h"
29 #include "graphic/graphic.h"
30 #include "graphic/rendertarget.h"
31 #include "graphic/text_layout.h"
32 #include "logic/map_objects/tribes/building.h"
33 #include "logic/map_objects/tribes/militarysite.h"
34 #include "logic/map_objects/tribes/soldier.h"
35 #include "logic/map_objects/tribes/soldiercontrol.h"
36 #include "logic/player.h"
37 #include "ui_basic/box.h"
38 #include "ui_basic/button.h"
39 #include "ui_basic/textarea.h"
40 #include "wui/interactive_gamebase.h"
41 #include "wui/soldiercapacitycontrol.h"
42 
43 using Widelands::Soldier;
44 using Widelands::SoldierControl;
45 
46 namespace {
47 
48 constexpr uint32_t kMaxColumns = 6;
49 constexpr uint32_t kAnimateSpeed = 300;  ///< in pixels per second
50 constexpr int kIconBorder = 2;
51 
52 }  // namespace
53 
54 /**
55  * Iconic representation of soldiers, including their levels and current health.
56  */
57 struct SoldierPanel : UI::Panel {
58 	using SoldierFn = std::function<void(const Soldier*)>;
59 
60 	SoldierPanel(UI::Panel& parent,
61 	             Widelands::EditorGameBase& egbase,
62 	             Widelands::Building& building);
63 
egbaseSoldierPanel64 	Widelands::EditorGameBase& egbase() const {
65 		return egbase_;
66 	}
67 
68 	void think() override;
69 	void draw(RenderTarget&) override;
70 
71 	void set_mouseover(const SoldierFn& fn);
72 	void set_click(const SoldierFn& fn);
73 
74 protected:
75 	void handle_mousein(bool inside) override;
76 	bool
77 	handle_mousemove(uint8_t state, int32_t x, int32_t y, int32_t xdiff, int32_t ydiff) override;
78 	bool handle_mousepress(uint8_t btn, int32_t x, int32_t y) override;
79 
80 private:
81 	Vector2i calc_pos(uint32_t row, uint32_t col) const;
82 	const Soldier* find_soldier(int32_t x, int32_t y) const;
83 
84 	struct Icon {
85 		Widelands::OPtr<Soldier> soldier;
86 		uint32_t row;
87 		uint32_t col;
88 		Vector2i pos = Vector2i::zero();
89 
90 		/**
91 		 * Keep track of how we last rendered this soldier,
92 		 * so that we can update when its status changes.
93 		 */
94 		/*@{*/
95 		uint32_t cache_level = 0;
96 		uint32_t cache_health = 0;
97 		/*@}*/
98 	};
99 
100 	Widelands::EditorGameBase& egbase_;
101 	const SoldierControl* soldier_control_;
102 
103 	SoldierFn mouseover_fn_;
104 	SoldierFn click_fn_;
105 
106 	std::vector<Icon> icons_;
107 
108 	uint32_t rows_;
109 	uint32_t cols_;
110 
111 	int icon_width_;
112 	int icon_height_;
113 
114 	int32_t last_animate_time_;
115 };
116 
SoldierPanel(UI::Panel & parent,Widelands::EditorGameBase & gegbase,Widelands::Building & building)117 SoldierPanel::SoldierPanel(UI::Panel& parent,
118                            Widelands::EditorGameBase& gegbase,
119                            Widelands::Building& building)
120    : Panel(&parent, 0, 0, 0, 0),
121      egbase_(gegbase),
122      soldier_control_(building.soldier_control()),
123      last_animate_time_(0) {
124 	assert(soldier_control_ != nullptr);
125 	Soldier::calc_info_icon_size(building.owner().tribe(), icon_width_, icon_height_);
126 	icon_width_ += 2 * kIconBorder;
127 	icon_height_ += 2 * kIconBorder;
128 
129 	Widelands::Quantity maxcapacity = soldier_control_->max_soldier_capacity();
130 	if (maxcapacity <= kMaxColumns) {
131 		cols_ = maxcapacity;
132 		rows_ = 1;
133 	} else {
134 		cols_ = kMaxColumns;
135 		rows_ = (maxcapacity + cols_ - 1) / cols_;
136 	}
137 
138 	set_size(cols_ * icon_width_, rows_ * icon_height_);
139 	set_desired_size(cols_ * icon_width_, rows_ * icon_height_);
140 	set_thinks(true);
141 
142 	// Initialize the icons
143 	uint32_t row = 0;
144 	uint32_t col = 0;
145 	for (Soldier* soldier : soldier_control_->present_soldiers()) {
146 		Icon icon;
147 		icon.soldier = soldier;
148 		icon.row = row;
149 		icon.col = col;
150 		icon.pos = calc_pos(row, col);
151 		icon.cache_health = 0;
152 		icon.cache_level = 0;
153 		icons_.push_back(icon);
154 
155 		if (++col >= cols_) {
156 			col = 0;
157 			row++;
158 		}
159 	}
160 }
161 
162 /**
163  * Set the callback function that indicates which soldier the mouse is over.
164  */
set_mouseover(const SoldierPanel::SoldierFn & fn)165 void SoldierPanel::set_mouseover(const SoldierPanel::SoldierFn& fn) {
166 	mouseover_fn_ = fn;
167 }
168 
169 /**
170  * Set the callback function that is called when a soldier is clicked.
171  */
set_click(const SoldierPanel::SoldierFn & fn)172 void SoldierPanel::set_click(const SoldierPanel::SoldierFn& fn) {
173 	click_fn_ = fn;
174 }
175 
think()176 void SoldierPanel::think() {
177 	bool changes = false;
178 	uint32_t capacity = soldier_control_->soldier_capacity();
179 
180 	// Update soldier list and target row/col:
181 	std::vector<Soldier*> soldierlist = soldier_control_->present_soldiers();
182 	std::vector<uint32_t> row_occupancy;
183 	row_occupancy.resize(rows_);
184 
185 	// First pass: check whether existing icons are still valid, and compact them
186 	for (uint32_t idx = 0; idx < icons_.size(); ++idx) {
187 		Icon& icon = icons_[idx];
188 		Soldier* soldier = icon.soldier.get(egbase());
189 		if (soldier) {
190 			std::vector<Soldier*>::iterator it =
191 			   std::find(soldierlist.begin(), soldierlist.end(), soldier);
192 			if (it != soldierlist.end())
193 				soldierlist.erase(it);
194 			else
195 				soldier = nullptr;
196 		}
197 
198 		if (!soldier) {
199 			icons_.erase(icons_.begin() + idx);
200 			idx--;
201 			changes = true;
202 			continue;
203 		}
204 
205 		while (icon.row && (row_occupancy[icon.row] >= kMaxColumns ||
206 		                    icon.row * kMaxColumns + row_occupancy[icon.row] >= capacity))
207 			icon.row--;
208 
209 		icon.col = row_occupancy[icon.row]++;
210 	}
211 
212 	// Second pass: add new soldiers
213 	while (!soldierlist.empty()) {
214 		Icon icon;
215 		icon.soldier = soldierlist.back();
216 		soldierlist.pop_back();
217 		icon.row = 0;
218 		while (row_occupancy[icon.row] >= kMaxColumns)
219 			icon.row++;
220 		icon.col = row_occupancy[icon.row]++;
221 		icon.pos = calc_pos(icon.row, icon.col);
222 
223 		// Let soldiers slide in from the right border
224 		icon.pos.x = get_w();
225 
226 		std::vector<Icon>::iterator insertpos = icons_.begin();
227 
228 		for (std::vector<Icon>::iterator icon_iter = icons_.begin(); icon_iter != icons_.end();
229 		     ++icon_iter) {
230 
231 			if (icon_iter->row <= icon.row)
232 				insertpos = icon_iter + 1;
233 
234 			icon.pos.x = std::max<int32_t>(icon.pos.x, icon_iter->pos.x + icon_width_);
235 		}
236 
237 		icons_.insert(insertpos, icon);
238 		changes = true;
239 	}
240 
241 	// Third pass: animate icons
242 	int32_t curtime = SDL_GetTicks();
243 	int32_t dt = std::min(std::max(curtime - last_animate_time_, 0), 1000);
244 	int32_t maxdist = dt * kAnimateSpeed / 1000;
245 	last_animate_time_ = curtime;
246 
247 	for (Icon& icon : icons_) {
248 		Vector2i goal = calc_pos(icon.row, icon.col);
249 		Vector2i dp = goal - icon.pos;
250 
251 		dp.x = std::min(std::max(dp.x, -maxdist), maxdist);
252 		dp.y = std::min(std::max(dp.y, -maxdist), maxdist);
253 
254 		if (dp.x != 0 || dp.y != 0)
255 			changes = true;
256 
257 		icon.pos += dp;
258 
259 		// Check whether health and/or level of the soldier has changed
260 		Soldier* soldier = icon.soldier.get(egbase());
261 		uint32_t level = soldier->get_attack_level();
262 		level = level * (soldier->descr().get_max_defense_level() + 1) + soldier->get_defense_level();
263 		level = level * (soldier->descr().get_max_evade_level() + 1) + soldier->get_evade_level();
264 		level = level * (soldier->descr().get_max_health_level() + 1) + soldier->get_health_level();
265 
266 		uint32_t health = soldier->get_current_health();
267 
268 		if (health != icon.cache_health || level != icon.cache_level) {
269 			icon.cache_level = level;
270 			icon.cache_health = health;
271 			changes = true;
272 		}
273 	}
274 
275 	if (changes) {
276 		Vector2i mousepos = get_mouse_position();
277 		mouseover_fn_(find_soldier(mousepos.x, mousepos.y));
278 	}
279 }
280 
draw(RenderTarget & dst)281 void SoldierPanel::draw(RenderTarget& dst) {
282 	// Fill a region matching the current site capacity with black
283 	uint32_t capacity = soldier_control_->soldier_capacity();
284 	uint32_t fullrows = capacity / kMaxColumns;
285 
286 	if (fullrows) {
287 		dst.fill_rect(Recti(0, 0, get_w(), icon_height_ * fullrows), RGBAColor(0, 0, 0, 0));
288 	}
289 	if (capacity % kMaxColumns) {
290 		dst.fill_rect(
291 		   Recti(0, icon_height_ * fullrows, icon_width_ * (capacity % kMaxColumns), icon_height_),
292 		   RGBAColor(0, 0, 0, 0));
293 	}
294 
295 	// Draw icons
296 	for (const Icon& icon : icons_) {
297 		const Soldier* soldier = icon.soldier.get(egbase());
298 		if (!soldier)
299 			continue;
300 
301 		constexpr float kNoZoom = 1.f;
302 		soldier->draw_info_icon(icon.pos + Vector2i(kIconBorder, kIconBorder), kNoZoom,
303 		                        Soldier::InfoMode::kInBuilding, InfoToDraw::kSoldierLevels, &dst);
304 	}
305 }
306 
calc_pos(uint32_t row,uint32_t col) const307 Vector2i SoldierPanel::calc_pos(uint32_t row, uint32_t col) const {
308 	return Vector2i(col * icon_width_, row * icon_height_);
309 }
310 
311 /**
312  * Return the soldier (if any) at the given coordinates.
313  */
find_soldier(int32_t x,int32_t y) const314 const Soldier* SoldierPanel::find_soldier(int32_t x, int32_t y) const {
315 	for (const Icon& icon : icons_) {
316 		Recti r(icon.pos, icon_width_, icon_height_);
317 		if (r.contains(Vector2i(x, y))) {
318 			return icon.soldier.get(egbase());
319 		}
320 	}
321 
322 	return nullptr;
323 }
324 
handle_mousein(bool inside)325 void SoldierPanel::handle_mousein(bool inside) {
326 	if (!inside && mouseover_fn_)
327 		mouseover_fn_(nullptr);
328 }
329 
handle_mousemove(uint8_t,int32_t x,int32_t y,int32_t,int32_t)330 bool SoldierPanel::handle_mousemove(
331    uint8_t /* state */, int32_t x, int32_t y, int32_t /* xdiff */, int32_t /* ydiff */) {
332 	if (mouseover_fn_)
333 		mouseover_fn_(find_soldier(x, y));
334 	return true;
335 }
336 
handle_mousepress(uint8_t btn,int32_t x,int32_t y)337 bool SoldierPanel::handle_mousepress(uint8_t btn, int32_t x, int32_t y) {
338 	if (btn == SDL_BUTTON_LEFT) {
339 		if (click_fn_) {
340 			if (const Soldier* soldier = find_soldier(x, y))
341 				click_fn_(soldier);
342 		}
343 		return true;
344 	}
345 
346 	return false;
347 }
348 
349 /**
350  * List of soldiers \ref MilitarySiteWindow and \ref TrainingSiteWindow
351  */
352 struct SoldierList : UI::Box {
353 	SoldierList(UI::Panel& parent, InteractiveGameBase& igb, Widelands::Building& building);
354 
355 	const SoldierControl* soldiers() const;
356 
357 private:
358 	void mouseover(const Soldier* soldier);
359 	void eject(const Soldier* soldier);
360 	void set_soldier_preference(int32_t changed_to);
361 	void think() override;
362 
363 	InteractiveGameBase& igbase_;
364 	Widelands::Building& building_;
365 	const UI::FontStyle font_style_;
366 	SoldierPanel soldierpanel_;
367 	UI::Radiogroup soldier_preference_;
368 	UI::Textarea infotext_;
369 };
370 
SoldierList(UI::Panel & parent,InteractiveGameBase & igb,Widelands::Building & building)371 SoldierList::SoldierList(UI::Panel& parent, InteractiveGameBase& igb, Widelands::Building& building)
372    : UI::Box(&parent, 0, 0, UI::Box::Vertical),
373 
374      igbase_(igb),
375      building_(building),
376      font_style_(UI::FontStyle::kLabel),
377      soldierpanel_(*this, igb.egbase(), building),
378      infotext_(this, _("Click soldier to send away")) {
379 	add(&soldierpanel_, UI::Box::Resizing::kAlign, UI::Align::kCenter);
380 
381 	add_space(2);
382 
383 	add(&infotext_, UI::Box::Resizing::kAlign, UI::Align::kCenter);
384 
385 	soldierpanel_.set_mouseover([this](const Soldier* s) { mouseover(s); });
386 	soldierpanel_.set_click([this](const Soldier* s) { eject(s); });
387 
388 	// We don't want translators to translate this twice, so it's a bit involved.
389 	int w = UI::g_fh
390 	           ->render(as_richtext_paragraph(
391 	              (boost::format("%s ")  // We need some extra space to fix bug 724169
392 	               % (boost::format(
393 	                     /** TRANSLATORS: Health, Attack, Defense, Evade */
394 	                     _("HP: %1$u/%2$u  AT: %3$u/%4$u  DE: %5$u/%6$u  EV: %7$u/%8$u")) %
395 	                  8 % 8 % 8 % 8 % 8 % 8 % 8 % 8))
396 	                 .str(),
397 	              font_style_))
398 	           ->width();
399 	uint32_t maxtextwidth = std::max(
400 	   w, UI::g_fh->render(as_richtext_paragraph(_("Click soldier to send away"), font_style_))
401 	         ->width());
402 	set_min_desired_breadth(maxtextwidth + 4);
403 
404 	UI::Box* buttons = new UI::Box(this, 0, 0, UI::Box::Horizontal);
405 
406 	bool can_act = igbase_.can_act(building_.owner().player_number());
407 	if (upcast(Widelands::MilitarySite, ms, &building)) {
408 		soldier_preference_.add_button(buttons, Vector2i::zero(),
409 		                               g_gr->images().get("images/wui/buildings/prefer_rookies.png"),
410 		                               _("Prefer rookies"));
411 		soldier_preference_.add_button(buttons, Vector2i(32, 0),
412 		                               g_gr->images().get("images/wui/buildings/prefer_heroes.png"),
413 		                               _("Prefer heroes"));
414 		UI::Radiobutton* button = soldier_preference_.get_first_button();
415 		while (button) {
416 			buttons->add(button);
417 			button = button->next_button();
418 		}
419 
420 		soldier_preference_.set_state(0);
421 		if (ms->get_soldier_preference() == Widelands::SoldierPreference::kHeroes) {
422 			soldier_preference_.set_state(1);
423 		}
424 		if (can_act) {
425 			soldier_preference_.changedto.connect([this](int32_t a) { set_soldier_preference(a); });
426 		} else {
427 			soldier_preference_.set_enabled(false);
428 		}
429 	}
430 	buttons->add_inf_space();
431 	buttons->add(create_soldier_capacity_control(*buttons, igb, building));
432 	add(buttons, UI::Box::Resizing::kFullSize);
433 }
434 
soldiers() const435 const SoldierControl* SoldierList::soldiers() const {
436 	return building_.soldier_control();
437 }
438 
think()439 void SoldierList::think() {
440 	// Only update the soldiers pref radio if player is spectator
441 	if (igbase_.can_act(building_.owner().player_number())) {
442 		return;
443 	}
444 	if (upcast(Widelands::MilitarySite, ms, &building_)) {
445 		switch (ms->get_soldier_preference()) {
446 		case Widelands::SoldierPreference::kRookies:
447 			soldier_preference_.set_state(0);
448 			break;
449 		case Widelands::SoldierPreference::kHeroes:
450 			soldier_preference_.set_state(1);
451 			break;
452 		}
453 	}
454 }
455 
mouseover(const Soldier * soldier)456 void SoldierList::mouseover(const Soldier* soldier) {
457 	if (!soldier) {
458 		infotext_.set_text(_("Click soldier to send away"));
459 		return;
460 	}
461 
462 	infotext_.set_text(
463 	   (boost::format(_("HP: %1$u/%2$u  AT: %3$u/%4$u  DE: %5$u/%6$u  EV: %7$u/%8$u")) %
464 	    soldier->get_health_level() % soldier->descr().get_max_health_level() %
465 	    soldier->get_attack_level() % soldier->descr().get_max_attack_level() %
466 	    soldier->get_defense_level() % soldier->descr().get_max_defense_level() %
467 	    soldier->get_evade_level() % soldier->descr().get_max_evade_level())
468 	      .str());
469 }
470 
eject(const Soldier * soldier)471 void SoldierList::eject(const Soldier* soldier) {
472 	uint32_t const capacity_min = soldiers()->min_soldier_capacity();
473 	bool can_act = igbase_.can_act(building_.owner().player_number());
474 	bool over_min = capacity_min < soldiers()->present_soldiers().size();
475 
476 	if (can_act && over_min)
477 		igbase_.game().send_player_drop_soldier(building_, soldier->serial());
478 }
479 
set_soldier_preference(int32_t changed_to)480 void SoldierList::set_soldier_preference(int32_t changed_to) {
481 #ifndef NDEBUG
482 	upcast(Widelands::MilitarySite, ms, &building_);
483 	assert(ms);
484 #endif
485 	igbase_.game().send_player_militarysite_set_soldier_preference(
486 	   building_, changed_to == 0 ? Widelands::SoldierPreference::kRookies :
487 	                                Widelands::SoldierPreference::kHeroes);
488 }
489 
490 UI::Panel*
create_soldier_list(UI::Panel & parent,InteractiveGameBase & igb,Widelands::Building & building)491 create_soldier_list(UI::Panel& parent, InteractiveGameBase& igb, Widelands::Building& building) {
492 	return new SoldierList(parent, igb, building);
493 }
494