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