Merge lp:~widelands-dev/widelands/choose-attack-soldiers into lp:widelands

Proposed by Benedikt Straub
Status: Merged
Merged at revision: 9103
Proposed branch: lp:~widelands-dev/widelands/choose-attack-soldiers
Merge into: lp:widelands
Diff against target: 805 lines (+440/-92)
11 files modified
src/ai/defaultai_warfare.cc (+8/-2)
src/logic/game.cc (+4/-3)
src/logic/game.h (+1/-1)
src/logic/map_objects/tribes/militarysite.cc (+1/-0)
src/logic/player.cc (+7/-10)
src/logic/player.h (+1/-1)
src/logic/playercommand.cc (+43/-11)
src/logic/playercommand.h (+4/-4)
src/wui/attack_box.cc (+287/-51)
src/wui/attack_box.h (+79/-3)
src/wui/fieldaction.cc (+5/-6)
To merge this branch: bzr merge lp:~widelands-dev/widelands/choose-attack-soldiers
Reviewer Review Type Date Requested Status
GunChleoc Approve
Toni Förster Approve
Review via email: mp+367041@code.launchpad.net

Commit message

Allow the player to choose the soldiers to send in the attack box

Description of the change

The attack box contains two lists of soldiers: One for the attacking soldiers and one for the rest. Click on a soldier to move him to the other list. Ctrl-Click to move all soldiers.

If you just want to attack quickly and don´t care about soldier choice, you can still use the slider or the more/less buttons.

Note that one soldier will always remain in every militarysite. Currently the engine decides which soldier this is, and he will not be shown in the attack box. Ideally, all available soldiers should be shown, grouped by their building, and the player can then choose which soldier(s) remain(s) behind, but this would clutter up the interface too much in my opinion if there are many own militarysites, especially small ones, nearby.

To post a comment you must log in.
Revision history for this message
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 4908. State: failed. Details: https://travis-ci.org/widelands/widelands/builds/529279191.
Appveyor build 4689. State: success. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_choose_attack_soldiers-4689.

Revision history for this message
GunChleoc (gunchleoc) :
Revision history for this message
Toni Förster (stonerl) wrote :
Revision history for this message
Benedikt Straub (nordfriese) wrote :

Formatted the tooltips and replied to diff comments.

> Did you remove the changes from this branch? […]

No, I didn´t even touch the file where that code is located?

Revision history for this message
Toni Förster (stonerl) wrote :

Sorry my mistake. But I do have a request. Could you add the CTRL-key behaviour to set the soldiers to max or minimum?

This is the only window left where this isn't possible.

Revision history for this message
Benedikt Straub (nordfriese) wrote :

OK, I´ll add this :)
By the way, you can also Ctrl-click one of the ListOfSoldiers to put all soldiers on the other list.

Revision history for this message
Toni Förster (stonerl) wrote :

Tested 9101 and it works perfect.

Just one more thing. There are only 8 Soldiers shown per row. Which means there is a huge space below the "Start Attack" button. Is it somehow possible to use this space as well and show 9 soldiers per row?

Revision history for this message
GunChleoc (gunchleoc) wrote :

How about removing the slider and replacing it with some buttons next to the lists? I hate fiddling with my mouse to position it over that tiny thing.

Revision history for this message
GunChleoc (gunchleoc) wrote :

Another idea: Add soldier levels to the tooltips. Good idea/bad idea?

Revision history for this message
Benedikt Straub (nordfriese) wrote :

I´m in favour of keeping the slider. When you have a massive army of similar-strength soldiers and you want to attack with about half of them, the slider is the most efficient way to do this. A spinner, even with big-step buttons, would be too slow IMHO if you have really huge armies.

Since the tooltip already contains the description what happens on click and Ctrl-click, the soldier level shouldn´t be shown there as well. But I can add it to a textbox below the list like in militarysites…

Revision history for this message
Toni Förster (stonerl) wrote :

> How about removing the slider and replacing it with some buttons next to the
> lists? I hate fiddling with my mouse to position it over that tiny thing.

Why not just make the slider bigger?

Revision history for this message
GunChleoc (gunchleoc) wrote :

How about using Shift-click for adding all soldiers from the start of the list to the one that you are clicking?

> But I can add it to a textbox below the list like in militarysites…

Sounds good :)

Revision history for this message
Toni Förster (stonerl) wrote :

Some changes I'd like to propose.

Do we need the word "soldiers" left to the slider? Shouldn't this be
obvious? We could get more space by removing text. Secondly make the
slider buttons as big as the attack button.

Here is how it could look like:

https://fosuta.org/pics/attack.png

I can attach a diff to the bug report if you want me to.

Is it possible to move the "Start Attack" Button into the attack_box?
We would then get rid of the space below.

Revision history for this message
Toni Förster (stonerl) wrote :
Revision history for this message
Toni Förster (stonerl) wrote :

Sorry for spamming, but I think this would be the ideal solution, IMHO.

https://fosuta.org/pics/attack2.png

Revision history for this message
Benedikt Straub (nordfriese) wrote :

Un nu de Weddervörhersaag: Een bannig dichter Hagel vun Vörslagens ;)
How do you like the one I implemented now?

Revision history for this message
Toni Förster (stonerl) wrote :

> Un nu de Weddervörhersaag: Een bannig dichter Hagel vun Vörslagens ;)

:D

> How do you like the one I implemented now?

Almost awesome. :)

Some nits though:

The tooltips for the lists hide the line were the soldier’s strength is shown.

If you could change the values to the ones in the diff-comments and add the
space it would look perfect.

See the screenshot:

https://fosuta.org/pics/attack3.png

And don't forget to remove or comment out this line in fieldaction.cc:

static const char* const pic_attack = "images/wui/buildings/menu_attack.png";

Revision history for this message
Toni Förster (stonerl) wrote :

Oh I forgot, If you could make the slider 17 pix high instead of 20 it would look more elegant, IMHO.

Revision history for this message
Benedikt Straub (nordfriese) wrote :

Implemented the changes as you suggested :)

> The tooltips for the lists hide the line were the soldier’s strength is shown.

I´m not sure how to solve this best: putting the textarea with the info on top wouldn´t look good IMHO, and the tooltip is long enough and shouldn´t contain this as well.
Perhaps I could swap the contents of the textarea and the tooltip, so the explanation what happens when clicking is displayed in the bottom line, and the info of the soldier under the mouse is shown as a tooltip that contains nothing else. What do you think?

Revision history for this message
Toni Förster (stonerl) wrote :

Sorry to bother you once more :(

Could you make the slider 210px wide, please? Then we would have a maximum of 10 soldiers per row, which looks even better. :)

Regarding the tooltip. Putting them in the text area where the soldier's info are shown is too intrusive, IMHO. Can text areas also hold tooltips?

On the other hand these tooltips are quite massive. Why don't we add them to the controls-list in the encyclopedia?

Revision history for this message
Benedikt Straub (nordfriese) wrote :

> Sorry to bother you once more :(
 No problem :)

> Could you make the slider 210px wide, please
 Done

> Can text areas also hold tooltips?
 Every UI element can have a tooltip, so it could be moved to the bottom line. Problem with this is that nobody has a reason to check out a textarea´s tooltip, and since the behaviour of Shift-click is not obvious, people would notice this feature only by chance.

> On the other hand these tooltips are quite massive.
 No more than those on the inputqueue buttons ;)

> Why don't we add them to the controls-list in the encyclopedia?
 How many players actually read that frequently? ;) Again there´s the danger that many people won´t notice if it isn´t explained in the attackbox. Besides, this would be inconsistent as Ctrl-/Shift modifiers are explained in a tooltip for all other controls.

Revision history for this message
Toni Förster (stonerl) wrote :

Hmmm, what if we added them to the "Attackers" and "Not attacking" fields? Players would definitely hover over these areas.

Revision history for this message
Benedikt Straub (nordfriese) wrote :

Good idea, implemented it like this then :)

Revision history for this message
Toni Förster (stonerl) wrote :

Looks good

review: Approve
Revision history for this message
Toni Förster (stonerl) wrote :

a/this

Revision history for this message
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 4932. State: passed. Details: https://travis-ci.org/widelands/widelands/builds/530921209.
Appveyor build 4713. State: success. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_choose_attack_soldiers-4713.

Revision history for this message
GunChleoc (gunchleoc) wrote :

I have pushed a commit with i18n fixes. Please merge this branch if you agree with them.

I replaced "x / y soldiers" with "x soldiers", because the total is already on the button next to the slider, and we don't need the information twice.

review: Approve
Revision history for this message
Benedikt Straub (nordfriese) wrote :

Thanks for the fixes :)

@bunnybot merge

Revision history for this message
Toni Förster (stonerl) wrote :

src/wui/attack_box.cc:109:75: warning: unused parameter 'max_attackers' [-Wunused-parameter]
static inline std::string slider_heading(uint32_t num_attackers, uint32_t max_attackers)

Revision history for this message
GunChleoc (gunchleoc) wrote :

Oops. I have cleaned this up now.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/ai/defaultai_warfare.cc'
2--- src/ai/defaultai_warfare.cc 2019-04-09 16:43:49 +0000
3+++ src/ai/defaultai_warfare.cc 2019-05-11 10:49:50 +0000
4@@ -481,7 +481,8 @@
5 }
6
7 // how many attack soldiers we can send?
8- int32_t attackers = player_->find_attack_soldiers(*flag);
9+ std::vector<Soldier*> soldiers;
10+ int32_t attackers = player_->find_attack_soldiers(*flag, &soldiers);
11 assert(attackers < 500);
12
13 if (attackers > 5) {
14@@ -499,7 +500,12 @@
15 player_number(), flag->get_position().x, flag->get_position().y, best_score, attackers,
16 enemy_sites[best_target].attack_counter + 1,
17 (gametime - enemy_sites[best_target].last_time_attacked) / 1000);
18- game().send_player_enemyflagaction(*flag, player_number(), static_cast<uint16_t>(attackers));
19+ std::vector<Serial> attacking_soldiers;
20+ for (int a = 0; a < attackers; ++a) {
21+ // TODO(Nordfriese): We could now choose the soldiers we want to send
22+ attacking_soldiers.push_back(soldiers[a]->serial());
23+ }
24+ game().send_player_enemyflagaction(*flag, player_number(), attacking_soldiers);
25 assert(1 <
26 player_->vision(Map::get_index(flag->get_building()->get_position(), map.get_width())));
27 attackers_count_ += attackers;
28
29=== modified file 'src/logic/game.cc'
30--- src/logic/game.cc 2019-05-05 18:53:14 +0000
31+++ src/logic/game.cc 2019-05-11 10:49:50 +0000
32@@ -820,10 +820,11 @@
33
34 void Game::send_player_enemyflagaction(const Flag& flag,
35 PlayerNumber const who_attacks,
36- uint32_t const num_soldiers) {
37+ const std::vector<Serial>& soldiers) {
38 if (1 < player(who_attacks)
39- .vision(Map::get_index(flag.get_building()->get_position(), map().get_width())))
40- send_player_command(*new CmdEnemyFlagAction(get_gametime(), who_attacks, flag, num_soldiers));
41+ .vision(Map::get_index(flag.get_building()->get_position(), map().get_width()))) {
42+ send_player_command(*new CmdEnemyFlagAction(get_gametime(), who_attacks, flag, soldiers));
43+ }
44 }
45
46 void Game::send_player_ship_scouting_direction(Ship& ship, WalkingDir direction) {
47
48=== modified file 'src/logic/game.h'
49--- src/logic/game.h 2019-03-01 16:24:48 +0000
50+++ src/logic/game.h 2019-05-11 10:49:50 +0000
51@@ -273,7 +273,7 @@
52 void send_player_change_training_options(TrainingSite&, TrainingAttribute, int32_t);
53 void send_player_drop_soldier(Building&, int32_t);
54 void send_player_change_soldier_capacity(Building&, int32_t);
55- void send_player_enemyflagaction(const Flag&, PlayerNumber, uint32_t count);
56+ void send_player_enemyflagaction(const Flag&, PlayerNumber, const std::vector<Serial>&);
57
58 void send_player_ship_scouting_direction(Ship&, WalkingDir);
59 void send_player_ship_construct_port(Ship&, Coords);
60
61=== modified file 'src/logic/map_objects/tribes/militarysite.cc'
62--- src/logic/map_objects/tribes/militarysite.cc 2019-03-01 04:19:53 +0000
63+++ src/logic/map_objects/tribes/militarysite.cc 2019-05-11 10:49:50 +0000
64@@ -374,6 +374,7 @@
65 stationed % (capacity_ - stationed))
66 .str();
67 } else {
68+ /** TRANSLATORS: Number of soldiers stationed at a militarysite. */
69 *s = (boost::format(ngettext("%u soldier", "%u soldiers", stationed)) % stationed).str();
70 }
71 } else {
72
73=== modified file 'src/logic/player.cc'
74--- src/logic/player.cc 2019-05-04 10:47:44 +0000
75+++ src/logic/player.cc 2019-05-11 10:49:50 +0000
76@@ -950,20 +950,17 @@
77
78 // TODO(unknown): Clean this mess up. The only action we really have right now is
79 // to attack, so pretending we have more types is pointless.
80-void Player::enemyflagaction(Flag& flag, PlayerNumber const attacker, uint32_t const count) {
81- if (attacker != player_number())
82+void Player::enemyflagaction(Flag& flag, PlayerNumber const attacker,
83+ const std::vector<Widelands::Soldier*>& soldiers) {
84+ if (attacker != player_number()) {
85 log("Player (%d) is not the sender of an attack (%d)\n", attacker, player_number());
86- else if (count == 0)
87- log("enemyflagaction: count is 0\n");
88- else if (is_hostile(flag.owner())) {
89+ } else if (soldiers.empty()) {
90+ log("enemyflagaction: no soldiers given\n");
91+ } else if (is_hostile(flag.owner())) {
92 if (Building* const building = flag.get_building()) {
93 if (const AttackTarget* attack_target = building->attack_target()) {
94 if (attack_target->can_be_attacked()) {
95- std::vector<Soldier*> attackers;
96- find_attack_soldiers(flag, &attackers, count);
97- assert(attackers.size() <= count);
98-
99- for (Soldier* temp_attacker : attackers) {
100+ for (Soldier* temp_attacker : soldiers) {
101 upcast(MilitarySite, ms, temp_attacker->get_location(egbase()));
102 ms->send_attacker(*temp_attacker, *building);
103 }
104
105=== modified file 'src/logic/player.h'
106--- src/logic/player.h 2019-04-26 16:52:39 +0000
107+++ src/logic/player.h 2019-05-11 10:49:50 +0000
108@@ -537,7 +537,7 @@
109 uint32_t find_attack_soldiers(Flag&,
110 std::vector<Soldier*>* soldiers = nullptr,
111 uint32_t max = std::numeric_limits<uint32_t>::max());
112- void enemyflagaction(Flag&, PlayerNumber attacker, uint32_t count);
113+ void enemyflagaction(Flag&, PlayerNumber attacker, const std::vector<Widelands::Soldier*>&);
114
115 uint32_t casualties() const {
116 return casualties_;
117
118=== modified file 'src/logic/playercommand.cc'
119--- src/logic/playercommand.cc 2019-03-09 08:58:52 +0000
120+++ src/logic/playercommand.cc 2019-05-11 10:49:50 +0000
121@@ -1589,7 +1589,11 @@
122 des.unsigned_8();
123 serial = des.unsigned_32();
124 des.unsigned_8();
125- number = des.unsigned_8();
126+ const uint32_t number = des.unsigned_32();
127+ soldiers.clear();
128+ for (uint32_t i = 0; i < number; ++i) {
129+ soldiers.push_back(des.unsigned_32());
130+ }
131 }
132
133 void CmdEnemyFlagAction::execute(Game& game) {
134@@ -1597,16 +1601,21 @@
135
136 if (upcast(Flag, flag, game.objects().get_object(serial))) {
137 log("Cmd_EnemyFlagAction::execute player(%u): flag->owner(%d) "
138- "number=%u\n",
139- player->player_number(), flag->owner().player_number(), number);
140+ "number=%" PRIuS "\n",
141+ player->player_number(), flag->owner().player_number(), soldiers.size());
142
143 if (const Building* const building = flag->get_building()) {
144 if (player->is_hostile(flag->owner()) &&
145- 1 < player->vision(Map::get_index(building->get_position(), game.map().get_width())))
146- player->enemyflagaction(*flag, sender(), number);
147- else
148+ 1 < player->vision(Map::get_index(building->get_position(), game.map().get_width()))) {
149+ std::vector<Soldier*> result;
150+ for (Serial s : soldiers) {
151+ result.push_back(dynamic_cast<Soldier*>(game.objects().get_object(s)));
152+ }
153+ player->enemyflagaction(*flag, sender(), result);
154+ } else {
155 log("Cmd_EnemyFlagAction::execute: ERROR: wrong player target not "
156 "seen or not hostile.\n");
157+ }
158 }
159 }
160 }
161@@ -1617,20 +1626,40 @@
162 ser.unsigned_8(1);
163 ser.unsigned_32(serial);
164 ser.unsigned_8(sender());
165- ser.unsigned_8(number);
166+ ser.unsigned_32(soldiers.size());
167+ for (Serial s : soldiers) {
168+ ser.unsigned_32(s);
169+ }
170 }
171
172-constexpr uint16_t kCurrentPacketVersionCmdEnemyFlagAction = 3;
173+constexpr uint16_t kCurrentPacketVersionCmdEnemyFlagAction = 4;
174
175 void CmdEnemyFlagAction::read(FileRead& fr, EditorGameBase& egbase, MapObjectLoader& mol) {
176 try {
177 const uint16_t packet_version = fr.unsigned_16();
178- if (packet_version == kCurrentPacketVersionCmdEnemyFlagAction) {
179+ if (packet_version <= kCurrentPacketVersionCmdEnemyFlagAction && packet_version >= 3) {
180 PlayerCommand::read(fr, egbase, mol);
181 fr.unsigned_8();
182 serial = get_object_serial_or_zero<Flag>(fr.unsigned_32(), mol);
183 fr.unsigned_8();
184- number = fr.unsigned_8();
185+
186+ soldiers.clear();
187+ if (packet_version == kCurrentPacketVersionCmdEnemyFlagAction) {
188+ const uint32_t number = fr.unsigned_32();
189+ for (uint32_t i = 0; i < number; ++i) {
190+ soldiers.push_back(mol.get<Soldier>(fr.unsigned_32()).serial());
191+ }
192+ } else {
193+ const uint8_t number = fr.unsigned_8();
194+ upcast(Flag, flag, egbase.objects().get_object(serial));
195+ assert(flag);
196+ std::vector<Soldier*> result;
197+ egbase.get_player(sender())->find_attack_soldiers(*flag, &result, number);
198+ assert(result.size() == number);
199+ for (const auto& s : result) {
200+ soldiers.push_back(s->serial());
201+ }
202+ }
203 } else {
204 throw UnhandledVersionError(
205 "CmdEnemyFlagAction", packet_version, kCurrentPacketVersionCmdEnemyFlagAction);
206@@ -1653,7 +1682,10 @@
207
208 // Now param
209 fw.unsigned_8(sender());
210- fw.unsigned_8(number);
211+ fw.unsigned_32(soldiers.size());
212+ for (Serial s : soldiers) {
213+ fw.unsigned_32(mos.get_object_file_index(*egbase.objects().get_object(s)));
214+ }
215 }
216
217 /*** struct PlayerMessageCommand ***/
218
219=== modified file 'src/logic/playercommand.h'
220--- src/logic/playercommand.h 2019-02-23 11:00:49 +0000
221+++ src/logic/playercommand.h 2019-05-11 10:49:50 +0000
222@@ -733,10 +733,10 @@
223 };
224
225 struct CmdEnemyFlagAction : public PlayerCommand {
226- CmdEnemyFlagAction() : PlayerCommand(), serial(0), number(0) {
227+ CmdEnemyFlagAction() : PlayerCommand(), serial(0) {
228 } // For savegame loading
229- CmdEnemyFlagAction(uint32_t t, int32_t p, const Flag& f, uint32_t num)
230- : PlayerCommand(t, p), serial(f.serial()), number(num) {
231+ CmdEnemyFlagAction(uint32_t t, int32_t p, const Flag& f, const std::vector<Serial>& s)
232+ : PlayerCommand(t, p), serial(f.serial()), soldiers(s) {
233 }
234
235 // Write these commands to a file (for savegames)
236@@ -754,7 +754,7 @@
237
238 private:
239 Serial serial;
240- uint8_t number;
241+ std::vector<Serial> soldiers;
242 };
243
244 /// Abstract base for commands about a message.
245
246=== modified file 'src/wui/attack_box.cc'
247--- src/wui/attack_box.cc 2019-02-23 11:00:49 +0000
248+++ src/wui/attack_box.cc 2019-05-11 10:49:50 +0000
249@@ -30,7 +30,7 @@
250 #include "graphic/text_constants.h"
251 #include "logic/map_objects/tribes/soldier.h"
252
253-constexpr int32_t kUpdateTimeInGametimeMs = 1000; // 1 second, gametime
254+constexpr int32_t kUpdateTimeInGametimeMs = 500; // half a second, gametime
255
256 AttackBox::AttackBox(UI::Panel* parent,
257 Widelands::Player* player,
258@@ -45,19 +45,24 @@
259 init();
260 }
261
262-uint32_t AttackBox::get_max_attackers() {
263+std::vector<Widelands::Soldier*> AttackBox::get_max_attackers() {
264 assert(player_);
265
266 if (upcast(Building, building, map_.get_immovable(*node_coordinates_))) {
267- if (player_->vision(map_.get_index(building->get_position(), map_.get_width())) <= 1) {
268- // Player can't see the buildings door, so it can't be attacked
269- // This is the same check as done later on in send_player_enemyflagaction()
270- return 0;
271+ if (player_->vision(map_.get_index(building->get_position(), map_.get_width())) > 1) {
272+ std::vector<Widelands::Soldier*> v;
273+ // TODO(Nordfriese): This method decides by itself which soldier remains in the building.
274+ // This soldier will not show up in the result vector. Perhaps we should show all
275+ // available soldiers, grouped by building, so the player can choose between all soldiers
276+ // knowing that at least one of each group will have to stay at home. However, this
277+ // could clutter up the screen a lot. Especially if you have many small buildings.
278+ player_->find_attack_soldiers(building->base_flag(), &v);
279+ return v;
280 }
281-
282- return player_->find_attack_soldiers(building->base_flag());
283+ // Player can't see the buildings door, so it can't be attacked
284+ // This is the same check as done later on in send_player_enemyflagaction()
285 }
286- return 0;
287+ return std::vector<Widelands::Soldier*>();
288 }
289
290 std::unique_ptr<UI::HorizontalSlider> AttackBox::add_slider(UI::Box& parent,
291@@ -86,7 +91,7 @@
292 void (AttackBox::*fn)(),
293 const std::string& tooltip_text) {
294 std::unique_ptr<UI::Button> button(new UI::Button(
295- &parent, text, 8, 8, 26, 26, UI::ButtonStyle::kWuiPrimary, text, tooltip_text));
296+ &parent, text, 8, 8, 34, 34, UI::ButtonStyle::kWuiPrimary, text, tooltip_text));
297 button->sigclicked.connect(boost::bind(fn, boost::ref(*this)));
298 parent.add(button.get());
299 return button;
300@@ -96,31 +101,78 @@
301 * Update available soldiers
302 */
303 void AttackBox::think() {
304- const int32_t gametime = player_->egbase().get_gametime();
305- if ((gametime - lastupdate_) > kUpdateTimeInGametimeMs) {
306- update_attack();
307- lastupdate_ = gametime;
308+ if ((player_->egbase().get_gametime() - lastupdate_) > kUpdateTimeInGametimeMs) {
309+ update_attack(false);
310 }
311 }
312
313-void AttackBox::update_attack() {
314+static inline std::string slider_heading(uint32_t num_attackers) {
315+ /** TRANSLATORS: Number of soldiers that should attack. Used in Attack box. */
316+ return (boost::format(ngettext("%u soldier", "%u soldiers", num_attackers)) % num_attackers).str();
317+}
318+
319+void AttackBox::update_attack(bool action_on_panel) {
320+ lastupdate_ = player_->egbase().get_gametime();
321+
322 assert(soldiers_slider_.get());
323 assert(soldiers_text_.get());
324 assert(less_soldiers_.get());
325 assert(more_soldiers_.get());
326-
327- int32_t max_attackers = get_max_attackers();
328-
329+ assert(attacking_soldiers_.get());
330+ assert(remaining_soldiers_.get());
331+
332+ std::vector<Widelands::Soldier*> all_attackers = get_max_attackers();
333+ const int max_attackers = all_attackers.size();
334+
335+ // Update number of available soldiers
336 if (soldiers_slider_->get_max_value() != max_attackers) {
337 soldiers_slider_->set_max_value(max_attackers);
338 }
339
340+ // Add new soldiers and remove missing soldiers to/from the list
341+ for (const auto& s : all_attackers) {
342+ if (!attacking_soldiers_->contains(s) && !remaining_soldiers_->contains(s)) {
343+ remaining_soldiers_->add(s);
344+ }
345+ }
346+ for (const auto& s : remaining_soldiers_->get_soldiers()) {
347+ if (std::find(all_attackers.begin(), all_attackers.end(), s) == all_attackers.end()) {
348+ remaining_soldiers_->remove(s);
349+ }
350+ }
351+ for (const auto& s : attacking_soldiers_->get_soldiers()) {
352+ if (std::find(all_attackers.begin(), all_attackers.end(), s) == all_attackers.end()) {
353+ attacking_soldiers_->remove(s);
354+ }
355+ }
356+
357+ if (action_on_panel) {
358+ // The player clicked on soldiers in the list – update slider
359+ soldiers_slider_->set_value(attacking_soldiers_->count_soldiers());
360+ } else {
361+ // The slider was moved or we were called from think() – shift lacking/extra soldiers between the lists
362+ const int32_t lacking = soldiers_slider_->get_value() - attacking_soldiers_->count_soldiers();
363+ if (lacking > 0) {
364+ for (int32_t i = 0; i < lacking; ++i) {
365+ const Widelands::Soldier* s = remaining_soldiers_->get_soldier();
366+ remaining_soldiers_->remove(s);
367+ attacking_soldiers_->add(s);
368+ }
369+ } else if (lacking < 0) {
370+ for (int32_t i = 0; i > lacking; --i) {
371+ const Widelands::Soldier* s = attacking_soldiers_->get_soldier();
372+ attacking_soldiers_->remove(s);
373+ remaining_soldiers_->add(s);
374+ }
375+ }
376+ }
377+
378+ // Update slider, buttons and texts
379 soldiers_slider_->set_enabled(max_attackers > 0);
380 more_soldiers_->set_enabled(max_attackers > soldiers_slider_->get_value());
381 less_soldiers_->set_enabled(soldiers_slider_->get_value() > 0);
382- soldiers_text_->set_text(
383- /** TRANSLATORS: %1% of %2% soldiers. Used in Attack box. */
384- (boost::format(_("%1% / %2%")) % soldiers_slider_->get_value() % max_attackers).str());
385+
386+ soldiers_text_->set_text(slider_heading(soldiers_slider_->get_value()));
387
388 more_soldiers_->set_title(std::to_string(max_attackers));
389 }
390@@ -128,48 +180,232 @@
391 void AttackBox::init() {
392 assert(node_coordinates_);
393
394- uint32_t max_attackers = get_max_attackers();
395-
396- UI::Box& linebox = *new UI::Box(this, 0, 0, UI::Box::Horizontal);
397- add(&linebox);
398- add_text(linebox, _("Soldiers:"));
399- linebox.add_space(8);
400-
401- less_soldiers_ =
402- add_button(linebox, "0", &AttackBox::send_less_soldiers, _("Send less soldiers"));
403-
404- // Spliter of soldiers
405+ std::vector<Widelands::Soldier*> all_attackers = get_max_attackers();
406+ const size_t max_attackers = all_attackers.size();
407+
408+ UI::Box& mainbox = *new UI::Box(this, 0, 0, UI::Box::Vertical);
409+ add(&mainbox);
410+
411+ UI::Box& linebox = *new UI::Box(&mainbox, 0, 0, UI::Box::Horizontal);
412+ mainbox.add(&linebox);
413+
414+ less_soldiers_ = add_button(linebox, "0", &AttackBox::send_less_soldiers,
415+ _("Send less soldiers. Hold down Ctrl to send no soldiers"));
416+
417 UI::Box& columnbox = *new UI::Box(&linebox, 0, 0, UI::Box::Vertical);
418 linebox.add(&columnbox);
419
420- const std::string attack_string =
421- (boost::format(_("%1% / %2%")) % (max_attackers > 0 ? 1 : 0) % max_attackers).str();
422-
423- soldiers_text_.reset(
424- &add_text(columnbox, attack_string, UI::Align::kCenter, UI_FONT_SIZE_ULTRASMALL));
425+ soldiers_text_.reset(&add_text(columnbox, slider_heading(max_attackers > 0 ? 1 : 0),
426+ UI::Align::kCenter, UI_FONT_SIZE_ULTRASMALL));
427
428 soldiers_slider_ = add_slider(
429- columnbox, 100, 10, 0, max_attackers, max_attackers > 0 ? 1 : 0, _("Number of soldiers"));
430-
431- soldiers_slider_->changed.connect(boost::bind(&AttackBox::update_attack, this));
432- more_soldiers_ = add_button(linebox, std::to_string(max_attackers),
433- &AttackBox::send_more_soldiers, _("Send more soldiers"));
434+ columnbox, 210, 17, 0, max_attackers, max_attackers > 0 ? 1 : 0, _("Number of soldiers"));
435+ soldiers_slider_->changed.connect([this]() { update_attack(false); });
436+
437+ more_soldiers_ = add_button(linebox, std::to_string(max_attackers), &AttackBox::send_more_soldiers,
438+ _("Send more soldiers. Hold down Ctrl to send as many soldiers as possible"));
439+ linebox.add_space(8);
440+
441+ attack_button_.reset(new UI::Button(&linebox, "attack", 8, 8, 34, 34, UI::ButtonStyle::kWuiPrimary,
442+ g_gr->images().get("images/wui/buildings/menu_attack.png"), _("Start attack")));
443+ linebox.add(attack_button_.get());
444+
445+ attacking_soldiers_.reset(new ListOfSoldiers(&mainbox, this, 0, 0, 30, 30));
446+ remaining_soldiers_.reset(new ListOfSoldiers(&mainbox, this, 0, 0, 30, 30));
447+ attacking_soldiers_->set_complement(remaining_soldiers_.get());
448+ remaining_soldiers_->set_complement(attacking_soldiers_.get());
449+ for (const auto& s : all_attackers) {
450+ remaining_soldiers_->add(s);
451+ }
452+
453+ boost::format tooltip_format("%s<br><p><font size=%d bold=0>%s<br>%s</font></p>");
454+ {
455+ UI::Textarea& txt = add_text(mainbox, _("Attackers:"));
456+ // Needed so we can get tooltips
457+ txt.set_handle_mouse(true);
458+ txt.set_tooltip((tooltip_format
459+ % _("Click on a soldier to remove him from the list of attackers")
460+ % UI_FONT_SIZE_MESSAGE
461+ % _("Hold down Ctrl to remove all soldiers from the list")
462+ % _("Hold down Shift to remove all soldiers up to the one you’re pointing at"))
463+ .str());
464+ mainbox.add(attacking_soldiers_.get(), UI::Box::Resizing::kFullSize);
465+ }
466+
467+ {
468+ UI::Textarea& txt = add_text(mainbox, _("Not attacking:"));
469+ txt.set_handle_mouse(true);
470+ txt.set_tooltip((tooltip_format
471+ % _("Click on a soldier to add him to the list of attackers")
472+ % UI_FONT_SIZE_MESSAGE
473+ % _("Hold down Ctrl to add all soldiers to the list")
474+ % _("Hold down Shift to add all soldiers up to the one you’re pointing at"))
475+ .str());
476+ mainbox.add(remaining_soldiers_.get(), UI::Box::Resizing::kFullSize);
477+ }
478+
479+ current_soldier_stats_.reset(new UI::Textarea(&mainbox, "", UI::Align::kCenter));
480+ mainbox.add(current_soldier_stats_.get(), UI::Box::Resizing::kFullSize, UI::Align::kCenter);
481
482 soldiers_slider_->set_enabled(max_attackers > 0);
483 more_soldiers_->set_enabled(max_attackers > 0);
484- less_soldiers_->set_enabled(max_attackers > 0);
485 }
486
487 void AttackBox::send_less_soldiers() {
488 assert(soldiers_slider_.get());
489- soldiers_slider_->set_value(soldiers_slider_->get_value() - 1);
490+ soldiers_slider_->set_value((SDL_GetModState() & KMOD_CTRL) ? 0 : soldiers_slider_->get_value() - 1);
491 }
492
493 void AttackBox::send_more_soldiers() {
494- soldiers_slider_->set_value(soldiers_slider_->get_value() + 1);
495-}
496-
497-uint32_t AttackBox::soldiers() const {
498- assert(soldiers_slider_.get());
499- return soldiers_slider_->get_value();
500-}
501+ soldiers_slider_->set_value((SDL_GetModState() & KMOD_CTRL) ? soldiers_slider_->get_max_value() :
502+ soldiers_slider_->get_value() + 1);
503+}
504+
505+size_t AttackBox::count_soldiers() const {
506+ return attacking_soldiers_->count_soldiers();
507+}
508+
509+std::vector<Widelands::Serial> AttackBox::soldiers() const {
510+ std::vector<Widelands::Serial> result;
511+ for (const auto& s : attacking_soldiers_->get_soldiers()) {
512+ result.push_back(s->serial());
513+ }
514+ return result;
515+}
516+
517+constexpr int kSoldierIconWidth = 32;
518+constexpr int kSoldierIconHeight = 30;
519+
520+AttackBox::ListOfSoldiers::ListOfSoldiers(UI::Panel* const parent,
521+ AttackBox* parent_box,
522+ int32_t const x,
523+ int32_t const y,
524+ int const w,
525+ int const h,
526+ bool restrict_rows)
527+ : UI::Panel(parent, x, y, w, h),
528+ restricted_row_number_(restrict_rows),
529+ attack_box_(parent_box) {
530+ update_desired_size();
531+}
532+
533+bool AttackBox::ListOfSoldiers::handle_mousepress(uint8_t btn, int32_t x, int32_t y) {
534+ if (btn != SDL_BUTTON_LEFT || !other_) {
535+ return UI::Panel::handle_mousepress(btn, x, y);
536+ }
537+ if (SDL_GetModState() & KMOD_CTRL) {
538+ for (const auto& s : get_soldiers()) {
539+ remove(s);
540+ other_->add(s);
541+ }
542+ } else {
543+ const Widelands::Soldier* soldier = soldier_at(x, y);
544+ if (!soldier) {
545+ return UI::Panel::handle_mousepress(btn, x, y);
546+ }
547+ if (SDL_GetModState() & KMOD_SHIFT) {
548+ for (const auto& s : get_soldiers()) {
549+ remove(s);
550+ other_->add(s);
551+ if (s == soldier) {
552+ break;
553+ }
554+ }
555+ } else {
556+ remove(soldier);
557+ other_->add(soldier);
558+ }
559+ }
560+ attack_box_->update_attack(true);
561+ return true;
562+}
563+
564+void AttackBox::ListOfSoldiers::handle_mousein(bool) {
565+ attack_box_->set_soldier_info_text();
566+}
567+
568+bool AttackBox::ListOfSoldiers::handle_mousemove(uint8_t, int32_t x, int32_t y, int32_t, int32_t) {
569+ if (const Widelands::Soldier* soldier = soldier_at(x, y)) {
570+ attack_box_->set_soldier_info_text(
571+ (boost::format(_("HP: %1$u/%2$u AT: %3$u/%4$u DE: %5$u/%6$u EV: %7$u/%8$u")) %
572+ soldier->get_health_level() % soldier->descr().get_max_health_level() %
573+ soldier->get_attack_level() % soldier->descr().get_max_attack_level() %
574+ soldier->get_defense_level() % soldier->descr().get_max_defense_level() %
575+ soldier->get_evade_level() % soldier->descr().get_max_evade_level())
576+ .str());
577+ } else {
578+ attack_box_->set_soldier_info_text();
579+ }
580+ return true;
581+}
582+
583+Widelands::Extent AttackBox::ListOfSoldiers::size() const {
584+ const size_t nr_soldiers = count_soldiers();
585+ uint32_t rows = nr_soldiers / current_size_;
586+ if (nr_soldiers == 0 || rows * current_size_ < nr_soldiers) {
587+ ++rows;
588+ }
589+ if (restricted_row_number_) {
590+ return Widelands::Extent(rows, current_size_);
591+ } else {
592+ return Widelands::Extent(current_size_, rows);
593+ }
594+}
595+
596+void AttackBox::ListOfSoldiers::update_desired_size() {
597+ current_size_ = std::max(1, restricted_row_number_ ? get_h() / kSoldierIconHeight : get_w() / kSoldierIconWidth);
598+ const Widelands::Extent e = size();
599+ set_desired_size(e.w * kSoldierIconWidth, e.h * kSoldierIconHeight);
600+}
601+
602+const Widelands::Soldier* AttackBox::ListOfSoldiers::soldier_at(int32_t x, int32_t y) const {
603+ if (x < 0 || y < 0 || soldiers_.empty()) {
604+ return nullptr;
605+ }
606+ const int32_t col = x / kSoldierIconWidth;
607+ const int32_t row = y / kSoldierIconHeight;
608+ assert(col >= 0);
609+ assert(row >= 0);
610+ if ((restricted_row_number_ ? row : col) >= current_size_) {
611+ return nullptr;
612+ }
613+ const int index = restricted_row_number_ ? current_size_ * col + row : current_size_ * row + col;
614+ assert(index >= 0);
615+ return static_cast<unsigned int>(index) < soldiers_.size() ? soldiers_[index] : nullptr;
616+}
617+
618+void AttackBox::ListOfSoldiers::add(const Widelands::Soldier* s) {
619+ soldiers_.push_back(s);
620+ update_desired_size();
621+}
622+
623+void AttackBox::ListOfSoldiers::remove(const Widelands::Soldier* s) {
624+ const auto it = std::find(soldiers_.begin(), soldiers_.end(), s);
625+ assert(it != soldiers_.end());
626+ soldiers_.erase(it);
627+ update_desired_size();
628+}
629+
630+void AttackBox::ListOfSoldiers::draw(RenderTarget& dst) {
631+ const size_t nr_soldiers = soldiers_.size();
632+ int32_t column = 0;
633+ int32_t row = 0;
634+ for (uint32_t i = 0; i < nr_soldiers; ++i) {
635+ Vector2i location(column * kSoldierIconWidth, row * kSoldierIconHeight);
636+ soldiers_[i]->draw_info_icon(location, 1.0f, false, &dst);
637+ if (restricted_row_number_) {
638+ ++row;
639+ if (row >= current_size_) {
640+ row = 0;
641+ ++column;
642+ }
643+ } else {
644+ ++column;
645+ if (column >= current_size_) {
646+ column = 0;
647+ ++row;
648+ }
649+ }
650+ }
651+}
652+
653
654=== modified file 'src/wui/attack_box.h'
655--- src/wui/attack_box.h 2019-02-23 11:00:49 +0000
656+++ src/wui/attack_box.h 2019-05-11 10:49:50 +0000
657@@ -22,6 +22,8 @@
658
659 #include <list>
660 #include <memory>
661+#include <set>
662+#include <vector>
663
664 #include "graphic/font_handler.h"
665 #include "graphic/text/font_set.h"
666@@ -51,10 +53,18 @@
667
668 void init();
669
670- uint32_t soldiers() const;
671+ size_t count_soldiers() const;
672+ std::vector<Widelands::Serial> soldiers() const;
673+ void set_soldier_info_text(std::string text = "") {
674+ current_soldier_stats_->set_text(text);
675+ }
676+
677+ UI::Button* get_attack_button() const {
678+ return attack_button_.get();
679+ }
680
681 private:
682- uint32_t get_max_attackers();
683+ std::vector<Widelands::Soldier*> get_max_attackers();
684 std::unique_ptr<UI::HorizontalSlider> add_slider(UI::Box& parent,
685 uint32_t width,
686 uint32_t height,
687@@ -73,7 +83,7 @@
688 const std::string& tooltip_text);
689
690 void think() override;
691- void update_attack();
692+ void update_attack(bool);
693 void send_less_soldiers();
694 void send_more_soldiers();
695
696@@ -88,6 +98,72 @@
697 std::unique_ptr<UI::Button> less_soldiers_;
698 std::unique_ptr<UI::Button> more_soldiers_;
699
700+ // A SoldierPanel is not applicable here as it's keyed to a building and thinks too much
701+ struct ListOfSoldiers : public UI::Panel {
702+ ListOfSoldiers(UI::Panel* const parent,
703+ AttackBox* parent_box,
704+ int32_t const x,
705+ int32_t const y,
706+ int const w,
707+ int const h,
708+ bool restrict_rows = false);
709+
710+ bool handle_mousepress(uint8_t btn, int32_t x, int32_t y) override;
711+ void handle_mousein(bool) override;
712+ bool handle_mousemove(uint8_t, int32_t, int32_t, int32_t, int32_t) override;
713+
714+ const Widelands::Soldier* soldier_at(int32_t x, int32_t y) const;
715+ void add(const Widelands::Soldier*);
716+ void remove(const Widelands::Soldier*);
717+ bool contains(const Widelands::Soldier* soldier) const {
718+ for (const auto& s : soldiers_) {
719+ if (s == soldier) {
720+ return true;
721+ }
722+ }
723+ return false;
724+ }
725+
726+ std::vector<const Widelands::Soldier*> get_soldiers() const {
727+ return soldiers_;
728+ }
729+ const Widelands::Soldier* get_soldier() const {
730+ return soldiers_.back();
731+ }
732+
733+ size_t count_soldiers() const {
734+ return soldiers_.size();
735+ }
736+ Widelands::Extent size() const;
737+ bool row_number_restricted() const {
738+ return restricted_row_number_;
739+ }
740+ void set_row_number_restricted(bool r) {
741+ restricted_row_number_ = r;
742+ }
743+
744+ void draw(RenderTarget& dst) override;
745+
746+ void set_complement(ListOfSoldiers* o) {
747+ other_ = o;
748+ }
749+
750+ private:
751+ bool restricted_row_number_;
752+ uint16_t current_size_; // Current number of rows or columns
753+ std::vector<const Widelands::Soldier*> soldiers_;
754+
755+ ListOfSoldiers* other_;
756+ AttackBox* attack_box_;
757+
758+ void update_desired_size() override;
759+ };
760+
761+ std::unique_ptr<ListOfSoldiers> attacking_soldiers_;
762+ std::unique_ptr<ListOfSoldiers> remaining_soldiers_;
763+ std::unique_ptr<UI::Textarea> current_soldier_stats_;
764+ std::unique_ptr<UI::Button> attack_button_;
765+
766 /// The last time the information in this Panel got updated
767 uint32_t lastupdate_;
768 };
769
770=== modified file 'src/wui/fieldaction.cc'
771--- src/wui/fieldaction.cc 2019-02-23 11:00:49 +0000
772+++ src/wui/fieldaction.cc 2019-05-11 10:49:50 +0000
773@@ -232,7 +232,6 @@
774 static const char* const pic_geologist = "images/wui/fieldaction/menu_geologist.png";
775
776 static const char* const pic_tab_attack = "images/wui/fieldaction/menu_tab_attack.png";
777-static const char* const pic_attack = "images/wui/buildings/menu_attack.png";
778
779 /*
780 ===============
781@@ -382,8 +381,9 @@
782 attack_box_ = new AttackBox(&a_box, player_, &node_, 0, 0);
783 a_box.add(attack_box_);
784
785- set_fastclick_panel(&add_button(
786- &a_box, "attack", pic_attack, &FieldActionWindow::act_attack, _("Start attack")));
787+ UI::Button* attack_button = attack_box_->get_attack_button();
788+ attack_button->sigclicked.connect(boost::bind(&FieldActionWindow::act_attack, this));
789+ set_fastclick_panel(attack_button);
790 }
791 }
792 }
793@@ -720,10 +720,9 @@
794 assert(attack_box_);
795 upcast(Game, game, &ibase().egbase());
796 if (upcast(Building, building, game->map().get_immovable(node_)))
797- if (attack_box_->soldiers() > 0) {
798+ if (attack_box_->count_soldiers() > 0) {
799 upcast(InteractivePlayer const, iaplayer, &ibase());
800- game->send_player_enemyflagaction(building->base_flag(), iaplayer->player_number(),
801- attack_box_->soldiers() /* number of soldiers */);
802+ game->send_player_enemyflagaction(building->base_flag(), iaplayer->player_number(), attack_box_->soldiers());
803 }
804 reset_mouse_and_die();
805 }

Subscribers

People subscribed via source and target branches

to status/vote changes: