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 "logic/map_objects/tribes/soldier.h"
21
22 #include <memory>
23
24 #include "base/macros.h"
25 #include "base/math.h"
26 #include "base/wexception.h"
27 #include "economy/economy.h"
28 #include "economy/flag.h"
29 #include "graphic/animation/animation_manager.h"
30 #include "graphic/graphic.h"
31 #include "graphic/rendertarget.h"
32 #include "io/fileread.h"
33 #include "io/filewrite.h"
34 #include "logic/editor_game_base.h"
35 #include "logic/game.h"
36 #include "logic/game_controller.h"
37 #include "logic/game_data_error.h"
38 #include "logic/map_objects/checkstep.h"
39 #include "logic/map_objects/findbob.h"
40 #include "logic/map_objects/findimmovable.h"
41 #include "logic/map_objects/findnode.h"
42 #include "logic/map_objects/tribes/battle.h"
43 #include "logic/map_objects/tribes/building.h"
44 #include "logic/map_objects/tribes/militarysite.h"
45 #include "logic/map_objects/tribes/tribe_descr.h"
46 #include "logic/map_objects/tribes/warehouse.h"
47 #include "logic/message_queue.h"
48 #include "logic/player.h"
49 #include "map_io/map_object_loader.h"
50 #include "map_io/map_object_saver.h"
51 #include "wui/mapviewpixelconstants.h"
52
53 namespace Widelands {
54
55 namespace {
56 constexpr int kSoldierHealthBarWidth = 13;
57 constexpr int kRetreatWhenHealthDropsBelowThisPercentage = 50;
58 } // namespace
59
SoldierLevelRange()60 SoldierLevelRange::SoldierLevelRange()
61 : min_health(-1),
62 min_attack(-1),
63 min_defense(-1),
64 min_evade(-1),
65 max_health(-1),
66 max_attack(-1),
67 max_defense(-1),
68 max_evade(-1) {
69 }
70
SoldierLevelRange(const LuaTable & t)71 SoldierLevelRange::SoldierLevelRange(const LuaTable& t) {
72 min_health = t.get_int("min_health");
73 min_attack = t.get_int("min_attack");
74 min_defense = t.get_int("min_defense");
75 min_evade = t.get_int("min_evade");
76 max_health = t.get_int("max_health");
77 max_attack = t.get_int("max_attack");
78 max_defense = t.get_int("max_defense");
79 max_evade = t.get_int("max_evade");
80 }
81
matches(int32_t health,int32_t attack,int32_t defense,int32_t evade) const82 bool SoldierLevelRange::matches(int32_t health,
83 int32_t attack,
84 int32_t defense,
85 int32_t evade) const {
86 return (health >= min_health && health <= max_health && attack >= min_attack &&
87 attack <= max_attack && defense >= min_defense && defense <= max_defense &&
88 evade >= min_evade && evade <= max_evade);
89 }
90
matches(const Soldier * soldier) const91 bool SoldierLevelRange::matches(const Soldier* soldier) const {
92 return matches(soldier->get_health_level(), soldier->get_attack_level(),
93 soldier->get_defense_level(), soldier->get_evade_level());
94 }
95
SoldierDescr(const std::string & init_descname,const LuaTable & table,const Tribes & tribes)96 SoldierDescr::SoldierDescr(const std::string& init_descname,
97 const LuaTable& table,
98 const Tribes& tribes)
99 : WorkerDescr(init_descname, MapObjectType::SOLDIER, table, tribes),
100 health_(table.get_table("health")),
101 attack_(table.get_table("attack")),
102 defense_(table.get_table("defense")),
103 evade_(table.get_table("evade")) {
104 add_attribute(MapObject::Attribute::SOLDIER);
105
106 // Battle animations
107 // attack_success_*-> soldier is attacking and hit his opponent
108 add_battle_animation(table.get_table("attack_success_w"), &attack_success_w_name_);
109 add_battle_animation(table.get_table("attack_success_e"), &attack_success_e_name_);
110
111 // attack_failure_*-> soldier is attacking and miss hit, defender evades
112 add_battle_animation(table.get_table("attack_failure_w"), &attack_failure_w_name_);
113 add_battle_animation(table.get_table("attack_failure_e"), &attack_failure_e_name_);
114
115 // evade_success_* -> soldier is defending and opponent misses
116 add_battle_animation(table.get_table("evade_success_w"), &evade_success_w_name_);
117 add_battle_animation(table.get_table("evade_success_e"), &evade_success_e_name_);
118
119 // evade_failure_* -> soldier is defending and opponent hits
120 add_battle_animation(table.get_table("evade_failure_w"), &evade_failure_w_name_);
121 add_battle_animation(table.get_table("evade_failure_e"), &evade_failure_e_name_);
122
123 // die_* -> soldier is dying
124 add_battle_animation(table.get_table("die_w"), &die_w_name_);
125 add_battle_animation(table.get_table("die_e"), &die_e_name_);
126
127 // per-level walking and idle animations
128 add_battle_animation(table.get_table("idle"), &idle_name_);
129 {
130 std::unique_ptr<LuaTable> walk_table = table.get_table("walk");
131 for (const auto& entry : walk_table->keys<int>()) {
132 std::unique_ptr<LuaTable> range_table = walk_table->get_table(entry);
133 // I would prefer to use the SoldierLevelRange as key in the table,
134 // but LuaTable can handle only string keys :(
135 SoldierLevelRange* range = nullptr;
136 std::map<uint8_t, std::string> map;
137 for (const std::string& dir_name : range_table->keys<std::string>()) {
138 uint8_t dir;
139 if (dir_name == "range") {
140 range = new SoldierLevelRange(*range_table->get_table(dir_name));
141 continue;
142 } else if (dir_name == "sw") {
143 dir = WALK_SW;
144 } else if (dir_name == "se") {
145 dir = WALK_SE;
146 } else if (dir_name == "nw") {
147 dir = WALK_NW;
148 } else if (dir_name == "ne") {
149 dir = WALK_NE;
150 } else if (dir_name == "e") {
151 dir = WALK_E;
152 } else if (dir_name == "w") {
153 dir = WALK_W;
154 } else {
155 throw GameDataError("Invalid walking direction: %s", dir_name.c_str());
156 }
157 const std::string anim_name = range_table->get_string(dir_name);
158 if (!is_animation_known(anim_name)) {
159 throw GameDataError(
160 "Trying to add unknown soldier walking animation: %s", anim_name.c_str());
161 }
162 map.emplace(dir, anim_name);
163 }
164 walk_name_.emplace(std::make_pair(std::unique_ptr<SoldierLevelRange>(range), map));
165 }
166 }
167 }
168
BattleAttribute(std::unique_ptr<LuaTable> table)169 SoldierDescr::BattleAttribute::BattleAttribute(std::unique_ptr<LuaTable> table) {
170 base = table->get_int("base");
171
172 if (table->has_key("maximum")) {
173 maximum = table->get_int("maximum");
174 if (base > maximum) {
175 throw GameDataError(
176 "Base %d is greater than maximum %d for a soldier's battle attribute.", base, maximum);
177 }
178 } else {
179 maximum = base;
180 }
181 increase = table->get_int("increase_per_level");
182 max_level = table->get_int("max_level");
183
184 // Load Graphics
185 std::vector<std::string> image_filenames =
186 table->get_table("pictures")->array_entries<std::string>();
187 if (image_filenames.size() != max_level + 1) {
188 throw GameDataError(
189 "Soldier needs to have %u pictures for battle attribute, but found %" PRIuS, max_level + 1,
190 image_filenames.size());
191 }
192 for (const std::string& image_filename : image_filenames) {
193 images.push_back(g_gr->images().get(image_filename));
194 }
195 }
196
197 /**
198 * Get random animation of specified type
199 */
get_rand_anim(Game & game,const std::string & animation_name,const Soldier * soldier) const200 uint32_t SoldierDescr::get_rand_anim(Game& game,
201 const std::string& animation_name,
202 const Soldier* soldier) const {
203 std::string run = animation_name;
204
205 const SoldierAnimationsList* animations = nullptr;
206 if (animation_name == "attack_success_w") {
207 animations = &attack_success_w_name_;
208 } else if (animation_name == "attack_success_e") {
209 animations = &attack_success_e_name_;
210 } else if (animation_name == "attack_failure_w") {
211 animations = &attack_failure_w_name_;
212 } else if (animation_name == "attack_failure_e") {
213 animations = &attack_failure_e_name_;
214 } else if (animation_name == "evade_success_w") {
215 animations = &evade_success_w_name_;
216 } else if (animation_name == "evade_success_e") {
217 animations = &evade_success_e_name_;
218 } else if (animation_name == "evade_failure_w") {
219 animations = &evade_failure_w_name_;
220 } else if (animation_name == "evade_failure_e") {
221 animations = &evade_failure_e_name_;
222 } else if (animation_name == "die_w") {
223 animations = &die_w_name_;
224 } else if (animation_name == "die_e") {
225 animations = &die_e_name_;
226 } else {
227 throw GameDataError("Unknown soldier battle animation: %s", animation_name.c_str());
228 }
229
230 assert(!animations->empty());
231 uint32_t nr_animations = 0;
232 for (const auto& pair : *animations) {
233 if (pair.second.matches(soldier)) {
234 nr_animations++;
235 }
236 }
237 if (nr_animations < 1) {
238 throw GameDataError("No battle animations for %s found!", animation_name.c_str());
239 }
240 uint32_t i = game.logic_rand() % nr_animations;
241 for (const auto& pair : *animations) {
242 if (pair.second.matches(soldier)) {
243 if (i == 0) {
244 run = pair.first;
245 break;
246 }
247 i--;
248 }
249 }
250
251 if (!is_animation_known(run)) {
252 log("Missing animation '%s' for soldier %s. Reverting to idle.\n", run.c_str(),
253 name().c_str());
254 run = "idle";
255 }
256 return get_animation(run, soldier);
257 }
258
get_animation(const std::string & anim,const MapObject * mo) const259 uint32_t SoldierDescr::get_animation(const std::string& anim, const MapObject* mo) const {
260 const Soldier* soldier = dynamic_cast<const Soldier*>(mo);
261 if (!soldier || anim != "idle") {
262 // We only need to check for a level-dependent idle animation.
263 // The walking anims can also be level-dependent,
264 // but that is taken care of by get_right_walk_anims().
265 // For battle animations, the level is already taken into account by the random selector.
266 return WorkerDescr::get_animation(anim, mo);
267 }
268 for (const auto& pair : idle_name_) {
269 if (pair.second.matches(soldier)) {
270 // Use the parent method here, so we don't end up in
271 // an endless loop if the idle anim is called "idle"
272 return WorkerDescr::get_animation(pair.first, mo);
273 }
274 }
275 throw GameDataError("This soldier does not have an idle animation for this training level!");
276 }
277
get_right_walk_anims(bool const ware,Worker * worker) const278 const DirAnimations& SoldierDescr::get_right_walk_anims(bool const ware, Worker* worker) const {
279 Soldier* soldier = dynamic_cast<Soldier*>(worker);
280 if (!soldier) {
281 return WorkerDescr::get_right_walk_anims(ware, worker);
282 }
283 auto& cache = soldier->get_walking_animations_cache();
284 if (cache.first && cache.first->matches(soldier)) {
285 return *cache.second;
286 }
287 for (const auto& pair : walk_name_) {
288 if (pair.first->matches(soldier)) {
289 cache.first.reset(new SoldierLevelRange(*pair.first));
290 cache.second.reset(new DirAnimations());
291 for (uint8_t dir = 1; dir <= 6; ++dir) {
292 cache.second->set_animation(dir, get_animation(pair.second.at(dir), worker));
293 }
294 return *cache.second;
295 }
296 }
297 throw GameDataError(
298 "Soldier %s does not have walking animations for his level!", name().c_str());
299 }
300
301 /**
302 * Create a new soldier
303 */
create_object() const304 Bob& SoldierDescr::create_object() const {
305 return *new Soldier(*this);
306 }
307
add_battle_animation(std::unique_ptr<LuaTable> table,SoldierAnimationsList * result)308 void SoldierDescr::add_battle_animation(std::unique_ptr<LuaTable> table,
309 SoldierAnimationsList* result) {
310 for (const std::string& anim_name : table->keys<std::string>()) {
311 if (!is_animation_known(anim_name)) {
312 throw GameDataError("Trying to add unknown battle animation: %s", anim_name.c_str());
313 }
314 result->emplace(anim_name, SoldierLevelRange(*table->get_table(anim_name)));
315 }
316 }
317
318 /*
319 ==============================
320
321 IMPLEMENTATION
322
323 ==============================
324 */
325
326 /// all done through init
Soldier(const SoldierDescr & soldier_descr)327 Soldier::Soldier(const SoldierDescr& soldier_descr) : Worker(soldier_descr) {
328 battle_ = nullptr;
329 health_level_ = 0;
330 attack_level_ = 0;
331 defense_level_ = 0;
332 evade_level_ = 0;
333
334 current_health_ = get_max_health();
335 retreat_health_ = 0;
336
337 combat_walking_ = CD_NONE;
338 combat_walkstart_ = 0;
339 combat_walkend_ = 0;
340 }
341
init(EditorGameBase & egbase)342 bool Soldier::init(EditorGameBase& egbase) {
343 health_level_ = 0;
344 attack_level_ = 0;
345 defense_level_ = 0;
346 evade_level_ = 0;
347 retreat_health_ = 0;
348
349 current_health_ = get_max_health();
350
351 combat_walking_ = CD_NONE;
352 combat_walkstart_ = 0;
353 combat_walkend_ = 0;
354
355 return Worker::init(egbase);
356 }
357
cleanup(EditorGameBase & egbase)358 void Soldier::cleanup(EditorGameBase& egbase) {
359 Worker::cleanup(egbase);
360 }
361
is_evict_allowed()362 bool Soldier::is_evict_allowed() {
363 return !is_on_battlefield();
364 }
365
366 /*
367 * Set this soldiers level. Automatically sets the new values
368 */
set_level(uint32_t const health,uint32_t const attack,uint32_t const defense,uint32_t const evade)369 void Soldier::set_level(uint32_t const health,
370 uint32_t const attack,
371 uint32_t const defense,
372 uint32_t const evade) {
373 set_health_level(health);
374 set_attack_level(attack);
375 set_defense_level(defense);
376 set_evade_level(evade);
377 }
set_health_level(const uint32_t health)378 void Soldier::set_health_level(const uint32_t health) {
379 assert(health_level_ <= health);
380 assert(health <= descr().get_max_health_level());
381
382 uint32_t oldmax = get_max_health();
383
384 health_level_ = health;
385
386 uint32_t newmax = get_max_health();
387 current_health_ = current_health_ * newmax / oldmax;
388 }
set_attack_level(const uint32_t attack)389 void Soldier::set_attack_level(const uint32_t attack) {
390 assert(attack_level_ <= attack);
391 assert(attack <= descr().get_max_attack_level());
392
393 attack_level_ = attack;
394 }
set_defense_level(const uint32_t defense)395 void Soldier::set_defense_level(const uint32_t defense) {
396 assert(defense_level_ <= defense);
397 assert(defense <= descr().get_max_defense_level());
398
399 defense_level_ = defense;
400 }
set_evade_level(const uint32_t evade)401 void Soldier::set_evade_level(const uint32_t evade) {
402 assert(evade_level_ <= evade);
403 assert(evade <= descr().get_max_evade_level());
404
405 evade_level_ = evade;
406 }
set_retreat_health(const uint32_t retreat)407 void Soldier::set_retreat_health(const uint32_t retreat) {
408 assert(retreat <= get_max_health());
409
410 retreat_health_ = retreat;
411 }
412
get_level(TrainingAttribute const at) const413 uint32_t Soldier::get_level(TrainingAttribute const at) const {
414 switch (at) {
415 case TrainingAttribute::kHealth:
416 return health_level_;
417 case TrainingAttribute::kAttack:
418 return attack_level_;
419 case TrainingAttribute::kDefense:
420 return defense_level_;
421 case TrainingAttribute::kEvade:
422 return evade_level_;
423 case TrainingAttribute::kTotal:
424 return health_level_ + attack_level_ + defense_level_ + evade_level_;
425 }
426 NEVER_HERE();
427 }
428
get_training_attribute(TrainingAttribute const attr) const429 int32_t Soldier::get_training_attribute(TrainingAttribute const attr) const {
430 switch (attr) {
431 case TrainingAttribute::kHealth:
432 return health_level_;
433 case TrainingAttribute::kAttack:
434 return attack_level_;
435 case TrainingAttribute::kDefense:
436 return defense_level_;
437 case TrainingAttribute::kEvade:
438 return evade_level_;
439 case TrainingAttribute::kTotal:
440 return health_level_ + attack_level_ + defense_level_ + evade_level_;
441 }
442 return Worker::get_training_attribute(attr);
443 }
444
get_max_health() const445 uint32_t Soldier::get_max_health() const {
446 return descr().get_base_health() + health_level_ * descr().get_health_incr_per_level();
447 }
448
get_min_attack() const449 uint32_t Soldier::get_min_attack() const {
450 return descr().get_base_min_attack() + attack_level_ * descr().get_attack_incr_per_level();
451 }
452
get_max_attack() const453 uint32_t Soldier::get_max_attack() const {
454 return descr().get_base_max_attack() + attack_level_ * descr().get_attack_incr_per_level();
455 }
456
get_defense() const457 uint32_t Soldier::get_defense() const {
458 return descr().get_base_defense() + defense_level_ * descr().get_defense_incr_per_level();
459 }
460
get_evade() const461 uint32_t Soldier::get_evade() const {
462 return descr().get_base_evade() + evade_level_ * descr().get_evade_incr_per_level();
463 }
464
465 // Unsignedness ensures that we can only heal, not hurt through this method.
heal(const uint32_t health)466 void Soldier::heal(const uint32_t health) {
467 molog("[soldier] healing (%d+)%d/%d\n", health, current_health_, get_max_health());
468 assert(health);
469 assert(current_health_ < get_max_health());
470 current_health_ += std::min(health, get_max_health() - current_health_);
471 assert(current_health_ <= get_max_health());
472 }
473
474 /**
475 * This only subs the specified number of health points, don't do anything more.
476 */
damage(const uint32_t value)477 void Soldier::damage(const uint32_t value) {
478 assert(current_health_ > 0);
479
480 molog("[soldier] damage %d(-%d)/%d\n", current_health_, value, get_max_health());
481 if (current_health_ < value)
482 current_health_ = 0;
483 else
484 current_health_ -= value;
485 }
486
487 /// Calculates the actual position to draw on from the base node position.
488 /// This function takes battling into account.
489 ///
490 /// pos is the location, in pixels, of the node position_ (height is already
491 /// taken into account).
calc_drawpos(const EditorGameBase & game,const Vector2f & field_on_dst,const float scale) const492 Vector2f Soldier::calc_drawpos(const EditorGameBase& game,
493 const Vector2f& field_on_dst,
494 const float scale) const {
495 if (combat_walking_ == CD_NONE) {
496 return Bob::calc_drawpos(game, field_on_dst, scale);
497 }
498
499 bool moving = false;
500 Vector2f spos = field_on_dst, epos = field_on_dst;
501
502 const float triangle_width = kTriangleWidth * scale;
503 switch (combat_walking_) {
504 case CD_WALK_W:
505 moving = true;
506 epos.x -= triangle_width / 4;
507 break;
508 case CD_WALK_E:
509 moving = true;
510 epos.x += triangle_width / 4;
511 break;
512 case CD_RETURN_W:
513 moving = true;
514 spos.x -= triangle_width / 4;
515 break;
516 case CD_RETURN_E:
517 moving = true;
518 spos.x += triangle_width / 4;
519 break;
520 case CD_COMBAT_W:
521 moving = false;
522 epos.x -= triangle_width / 4;
523 break;
524 case CD_COMBAT_E:
525 moving = false;
526 epos.x += triangle_width / 4;
527 break;
528 case CD_NONE:
529 break;
530 }
531
532 if (moving) {
533 const float f = math::clamp(static_cast<float>(game.get_gametime() - combat_walkstart_) /
534 (combat_walkend_ - combat_walkstart_),
535 0.f, 1.f);
536 assert(combat_walkstart_ <= game.get_gametime());
537 assert(combat_walkstart_ < combat_walkend_);
538 epos.x = f * epos.x + (1 - f) * spos.x;
539 }
540 return epos;
541 }
542
543 /*
544 * Draw this soldier. This basically draws him as a worker, but add health points
545 */
draw(const EditorGameBase & game,const InfoToDraw & info_to_draw,const Vector2f & field_on_dst,const Coords & coords,float scale,RenderTarget * dst) const546 void Soldier::draw(const EditorGameBase& game,
547 const InfoToDraw& info_to_draw,
548 const Vector2f& field_on_dst,
549 const Coords& coords,
550 float scale,
551 RenderTarget* dst) const {
552 const uint32_t anim = get_current_anim();
553 if (!anim) {
554 return;
555 }
556
557 const Vector2f point_on_dst = calc_drawpos(game, field_on_dst, scale);
558 draw_info_icon(
559 point_on_dst.cast<int>() -
560 Vector2i(0, (g_gr->animations().get_animation(get_current_anim()).height() - 7) * scale),
561 scale, InfoMode::kWalkingAround, info_to_draw, dst);
562 draw_inner(game, point_on_dst, coords, scale, dst);
563 }
564
565 /**
566 * Draw the info icon (level indicators + health bar) for this soldier.
567 * 'draw_mode' determines whether the soldier info is displayed in a building window
568 * or on top of a soldier walking around. 'info_to_draw' checks which info the user wants to see
569 * for soldiers walking around.
570 */
draw_info_icon(Vector2i draw_position,float scale,const InfoMode draw_mode,const InfoToDraw info_to_draw,RenderTarget * dst) const571 void Soldier::draw_info_icon(Vector2i draw_position,
572 float scale,
573 const InfoMode draw_mode,
574 const InfoToDraw info_to_draw,
575 RenderTarget* dst) const {
576 if (!(info_to_draw & InfoToDraw::kSoldierLevels)) {
577 return;
578 }
579
580 // Since the graphics below are all pixel perfect and scaling them as floats
581 // looks weird, we round to the nearest fullest integer. We do allow half size though.
582 scale = std::max(0.5f, std::round(scale));
583
584 #ifndef NDEBUG
585 {
586 // This function assumes stuff about our data files: level icons are all the
587 // same size and this is smaller than the width of the healthbar. This
588 // simplifies the drawing code below a lot. Before it had a lot of if () that
589 // were never tested - since our data files never changed.
590 const Image* healthpic = get_health_level_pic();
591
592 const Image* attackpic = get_attack_level_pic();
593 const Image* defensepic = get_defense_level_pic();
594 const Image* evadepic = get_evade_level_pic();
595
596 const int dimension = attackpic->width();
597 assert(attackpic->height() == dimension);
598 assert(healthpic->width() == dimension);
599 assert(healthpic->height() == dimension);
600 assert(defensepic->width() == dimension);
601 assert(defensepic->height() == dimension);
602 assert(evadepic->width() == dimension);
603 assert(evadepic->height() == dimension);
604 assert(kSoldierHealthBarWidth > dimension);
605 }
606 #endif
607
608 const int icon_size = get_health_level_pic()->height();
609
610 // Draw health info in building windows, or if kSoldierLevels is on.
611 const bool draw_health_bar =
612 draw_mode == InfoMode::kInBuilding || (info_to_draw & InfoToDraw::kSoldierLevels);
613
614 switch (draw_mode) {
615 case InfoMode::kInBuilding:
616 draw_position.x += kSoldierHealthBarWidth * scale;
617 draw_position.y += 2 * icon_size * scale;
618 break;
619 case InfoMode::kWalkingAround:
620 if (draw_health_bar) {
621 draw_position.y -= 5 * scale;
622 }
623 }
624
625 if (draw_health_bar) {
626 // Draw energy bar
627 assert(get_max_health());
628 const RGBColor& color = owner().get_playercolor();
629 const uint16_t color_sum = color.r + color.g + color.b;
630
631 // The frame gets a slight tint of player color
632 const Recti energy_outer(draw_position - Vector2i(kSoldierHealthBarWidth, 0) * scale,
633 kSoldierHealthBarWidth * 2 * scale, 5 * scale);
634 dst->fill_rect(energy_outer, color);
635 dst->brighten_rect(energy_outer, 230 - color_sum / 3);
636
637 // Adjust health to current animation tick
638 uint32_t health_to_show = current_health_;
639 if (battle_) {
640 uint32_t pending_damage = battle_->get_pending_damage(this);
641 if (pending_damage > 0) {
642 int32_t timeshift = owner().egbase().get_gametime() - get_animstart();
643 timeshift = std::min(std::max(0, timeshift), 1000);
644
645 pending_damage *= timeshift;
646 pending_damage /= 1000;
647
648 if (pending_damage > health_to_show) {
649 health_to_show = 0;
650 } else {
651 health_to_show -= pending_damage;
652 }
653 }
654 }
655
656 // Now draw the health bar itself
657 const int health_width = 2 * (kSoldierHealthBarWidth - 1) * health_to_show / get_max_health();
658
659 Recti energy_inner(draw_position + Vector2i(-kSoldierHealthBarWidth + 1, 1) * scale,
660 health_width * scale, 3 * scale);
661 Recti energy_complement(energy_inner.origin() + Vector2i(health_width, 0) * scale,
662 (2 * (kSoldierHealthBarWidth - 1) - health_width) * scale, 3 * scale);
663
664 const RGBColor complement_color =
665 color_sum > 128 * 3 ? RGBColor(32, 32, 32) : RGBColor(224, 224, 224);
666 dst->fill_rect(energy_inner, color);
667 dst->fill_rect(energy_complement, complement_color);
668 }
669
670 // Draw level info in building windows, or if kSoldierLevels is on.
671 if (draw_mode == InfoMode::kInBuilding || (info_to_draw & InfoToDraw::kSoldierLevels)) {
672 const auto draw_level_image = [icon_size, scale, &draw_position, dst](
673 const Vector2i& offset, const Image* image) {
674 dst->blitrect_scale(
675 Rectf(draw_position + offset * icon_size * scale, icon_size * scale, icon_size * scale),
676 image, Recti(0, 0, icon_size, icon_size), 1.f, BlendMode::UseAlpha);
677 };
678
679 draw_level_image(Vector2i(-1, -2), get_attack_level_pic());
680 draw_level_image(Vector2i(0, -2), get_defense_level_pic());
681 draw_level_image(Vector2i(-1, -1), get_health_level_pic());
682 draw_level_image(Vector2i(0, -1), get_evade_level_pic());
683 }
684 }
685
686 /**
687 * Compute the size of the info icon (level indicators + health bar) for soldiers of
688 * the given tribe.
689 */
calc_info_icon_size(const TribeDescr & tribe,int & w,int & h)690 void Soldier::calc_info_icon_size(const TribeDescr& tribe, int& w, int& h) {
691 const SoldierDescr* soldierdesc =
692 static_cast<const SoldierDescr*>(tribe.get_worker_descr(tribe.soldier()));
693 // The function draw_info_icon() already assumes that all icons have the same dimensions,
694 // so we can make the same assumption here too.
695 const int dimension = soldierdesc->get_health_level_pic(0)->height();
696 w = 2 * std::max(dimension, kSoldierHealthBarWidth);
697 h = 5 + 2 * dimension;
698 }
699
pop_task_or_fight(Game & game)700 void Soldier::pop_task_or_fight(Game& game) {
701 if (battle_)
702 start_task_battle(game);
703 else
704 pop_task(game);
705 }
706
707 /**
708 *
709 *
710 */
start_animation(EditorGameBase & egbase,const std::string & animname,uint32_t const time)711 void Soldier::start_animation(EditorGameBase& egbase,
712 const std::string& animname,
713 uint32_t const time) {
714 molog("[soldier] starting animation %s", animname.c_str());
715 Game& game = dynamic_cast<Game&>(egbase);
716 return start_task_idle(game, descr().get_rand_anim(game, animname, this), time);
717 }
718
719 /**
720 * \return \c true if this soldier is considered to be on the battlefield
721 */
is_on_battlefield()722 bool Soldier::is_on_battlefield() {
723 return get_state(taskAttack) || get_state(taskDefense);
724 }
725
726 /**
727 * \return \c true if this soldier is considered to be attacking the player
728 */
is_attacking_player(Game & game,Player & player)729 bool Soldier::is_attacking_player(Game& game, Player& player) {
730 State* state = get_state(taskAttack);
731 if (state) {
732 if (upcast(PlayerImmovable, imm, state->objvar1.get(game))) {
733 return (imm->get_owner() == &player);
734 }
735 }
736 return false;
737 }
738
get_battle() const739 Battle* Soldier::get_battle() const {
740 return battle_;
741 }
742
743 /**
744 * Determine whether this soldier can be challenged by an opponent.
745 *
746 * Such a challenge might override a battle that the soldier is currently
747 * walking towards, to avoid lockups when the combatants cannot reach
748 * each other.
749 */
can_be_challenged()750 bool Soldier::can_be_challenged() {
751 if (current_health_ < 1) { // Soldier is dead!
752 return false;
753 }
754 if (!is_on_battlefield()) {
755 return false;
756 }
757 if (!battle_) {
758 return true;
759 }
760 return !battle_->locked(dynamic_cast<Game&>(get_owner()->egbase()));
761 }
762
763 /**
764 * Assign the soldier to a battle (may be zero).
765 *
766 * \note must only be called by the \ref Battle object
767 */
set_battle(Game & game,Battle * const battle)768 void Soldier::set_battle(Game& game, Battle* const battle) {
769 if (battle_ != battle) {
770 battle_ = battle;
771 send_signal(game, "battle");
772 }
773 }
774
775 /**
776 * Set a fallback task.
777 */
init_auto_task(Game & game)778 void Soldier::init_auto_task(Game& game) {
779 if (get_current_health() < 1) {
780 molog("[soldier] init_auto_task: die\n");
781 return start_task_die(game);
782 }
783
784 return Worker::init_auto_task(game);
785 }
786
787 struct FindNodeOwned {
FindNodeOwnedWidelands::FindNodeOwned788 explicit FindNodeOwned(PlayerNumber owner) : owner_(owner) {
789 }
acceptWidelands::FindNodeOwned790 bool accept(const EditorGameBase&, const FCoords& coords) const {
791 return (coords.field->get_owned_by() == owner_);
792 }
793
794 private:
795 PlayerNumber owner_;
796 };
797
798 /**
799 * Leave our home building and single-mindedly try to attack
800 * and conquer the given building.
801 *
802 * The following variables are used:
803 * \li objvar1 the \ref Building we're attacking.
804 */
805 Bob::Task const Soldier::taskAttack = {"attack", static_cast<Bob::Ptr>(&Soldier::attack_update),
806 nullptr, static_cast<Bob::Ptr>(&Soldier::attack_pop), true};
807
start_task_attack(Game & game,Building & building)808 void Soldier::start_task_attack(Game& game, Building& building) {
809 push_task(game, taskAttack);
810 State& state = top_state();
811 state.objvar1 = &building;
812 state.coords = building.get_position();
813 state.ivar2 = 0; // The return state 1=go home 2=go back in known land
814 state.ivar3 = 0; // Counts how often the soldier is blocked in a row
815
816 state.ivar1 |= CF_RETREAT_WHEN_INJURED;
817 set_retreat_health(kRetreatWhenHealthDropsBelowThisPercentage * get_max_health() / 100);
818
819 // Injured soldiers are not allowed to attack
820 if (get_retreat_health() > get_current_health()) {
821 set_retreat_health(get_current_health());
822 }
823 molog("[attack] starting, retreat health: %d\n", get_retreat_health());
824 }
825
attack_update(Game & game,State & state)826 void Soldier::attack_update(Game& game, State& state) {
827 std::string signal = get_signal();
828 uint32_t defenders = 0;
829
830 if (signal.size()) {
831 if (signal == "battle" || signal == "wakeup" || signal == "sleep") {
832 state.ivar3 = 0;
833 signal_handled();
834 } else if (signal == "blocked") {
835 state.ivar3++;
836 signal_handled();
837 } else if (signal == "fail") {
838 state.ivar3 = 0;
839 signal_handled();
840 if (state.objvar1.get(game)) {
841 molog("[attack] failed to reach enemy\n");
842 state.objvar1 = nullptr;
843 } else {
844 molog("[attack] unexpected fail\n");
845 return pop_task(game);
846 }
847 } else if (signal == "location") {
848 molog("[attack] Location destroyed\n");
849 state.ivar3 = 0;
850 signal_handled();
851 if (state.ivar2 == 0) {
852 state.ivar2 = 1;
853 }
854 } else {
855 molog("[attack] cancelled by unexpected signal '%s'\n", signal.c_str());
856 return pop_task(game);
857 }
858 } else {
859 // no signals means no consecutive block -> we're not stuck anymore
860 state.ivar3 = 0;
861 }
862
863 // We are at enemy building flag, and a defender is coming, sleep until he
864 // "wake up"s me
865 if (signal == "sleep") {
866 return start_task_idle(game, descr().get_animation("idle", this), -1);
867 }
868
869 upcast(Building, location, get_location(game));
870 upcast(Building, enemy, state.objvar1.get(game));
871
872 // Handle returns
873 const Map& map = game.map();
874 if (state.ivar2 > 0) {
875 if (state.ivar2 == 1) {
876 // Return home
877 if (!location || !is_a(MilitarySite, location)) {
878 molog("[attack] No more site to go back to\n");
879 state.ivar2 = 2;
880 return schedule_act(game, 10);
881 }
882 Flag& baseflag = location->base_flag();
883 if (get_position() == baseflag.get_position()) {
884 // At flag, enter building
885 return start_task_move(
886 game, WALK_NW, descr().get_right_walk_anims(does_carry_ware(), this), true);
887 }
888 if (get_position() == location->get_position()) {
889 // At building, check if attack is required
890 if (!enemy) {
891 molog("[attack] returned home\n");
892 return pop_task_or_fight(game);
893 }
894 state.ivar2 = 0;
895 return start_task_leavebuilding(game, false);
896 }
897 // Head to home
898 if (state.ivar3 > kBockCountIsStuck)
899 molog("[attack] soldier is stuck, blocked nodes will be ignored\n");
900
901 if (start_task_movepath(game, baseflag.get_position(),
902 4, // use larger persist when returning home
903 descr().get_right_walk_anims(does_carry_ware(), this), false, -1,
904 state.ivar3 > kBockCountIsStuck))
905 return;
906 else {
907 molog("[attack] failed to return home\n");
908 return pop_task(game);
909 }
910 }
911 if (state.ivar2 == 2) {
912 // No more home, so return to homeland
913 upcast(Flag, flag, map.get_immovable(get_position()));
914 if (flag && flag->get_owner() == get_owner()) {
915 // At a flag
916 molog("[attack] Returned to own flag\n");
917 return pop_task(game);
918 }
919 Coords target;
920 if (get_location(game)) {
921 // We still have a location, head for the flag
922 target = get_location(game)->base_flag().get_position();
923 molog("[attack] Going back to our flag\n");
924 } else {
925 // No location
926 if (get_position().field->get_owned_by() == get_owner()->player_number()) {
927 // We are in our land, become fugitive
928 molog("[attack] Back to our land\n");
929 return pop_task(game);
930 }
931 // Try to find our land
932 std::vector<Coords> coords;
933 uint32_t maxdist = descr().vision_range() * 2;
934 Area<FCoords> area(map.get_fcoords(get_position()), maxdist);
935 if (map.find_reachable_fields(game, area, &coords, CheckStepDefault(descr().movecaps()),
936 FindNodeOwned(get_owner()->player_number()))) {
937 // Found home land
938 target = coords.front();
939 molog("[attack] Going back to our land\n");
940 } else {
941 // Become fugitive
942 molog("[attack] No land in sight\n");
943 return pop_task(game);
944 }
945 }
946 if (start_task_movepath(game, target,
947 4, // use larger persist when returning home
948 descr().get_right_walk_anims(does_carry_ware(), this)))
949 return;
950 else {
951 molog("[attack] failed to return to own land\n");
952 return pop_task(game);
953 }
954 }
955 }
956
957 if (battle_)
958 return start_task_battle(game);
959
960 if (signal == "blocked") {
961 // Wait before we try again. Note that this must come *after*
962 // we check for a battle
963 // Note that we *should* be woken via send_space_signals,
964 // so the timeout is just an additional safety net.
965 return start_task_idle(game, descr().get_animation("idle", this), 5000);
966 }
967
968 // Count remaining defenders
969 if (enemy) {
970 if (enemy->soldier_control() != nullptr) {
971 defenders = enemy->soldier_control()->present_soldiers().size();
972 }
973 if (upcast(Warehouse, wh, enemy)) {
974 Requirements noreq;
975 defenders =
976 wh->count_workers(game, wh->owner().tribe().soldier(), noreq, Warehouse::Match::kExact);
977 }
978 // Any enemy soldier at baseflag count as defender.
979 std::vector<Bob*> soldiers;
980 map.find_bobs(game, Area<FCoords>(map.get_fcoords(enemy->base_flag().get_position()), 0),
981 &soldiers, FindBobEnemySoldier(get_owner()));
982 defenders += soldiers.size();
983 }
984
985 if (!enemy || (get_retreat_health() > get_current_health() && defenders > 0)) {
986 // Injured soldiers will try to return to safe site at home.
987 if (get_retreat_health() > get_current_health()) {
988 assert(state.ivar1 & CF_RETREAT_WHEN_INJURED);
989 if (defenders) {
990 molog(" [attack] badly injured (%d), retreating...\n", get_current_health());
991 state.coords = Coords::null();
992 state.objvar1 = nullptr;
993 }
994 }
995 // The old militarysite gets replaced by a new one, so if "enemy" is not
996 // valid anymore, we either "conquered" the new building, or it was
997 // destroyed.
998 if (state.coords) {
999 BaseImmovable* const newimm = map[state.coords].get_immovable();
1000 upcast(MilitarySite, newsite, newimm);
1001 if (newsite && (&newsite->owner() == &owner())) {
1002 const SoldierControl* soldier_control = newsite->soldier_control();
1003 assert(soldier_control != nullptr); // 'newsite' is a military site
1004 state.objvar1 = nullptr;
1005 // We may also have our location destroyed in between
1006 if (soldier_control->stationed_soldiers().size() <
1007 soldier_control->soldier_capacity() &&
1008 (!location ||
1009 location->base_flag().get_position() != newsite->base_flag().get_position())) {
1010 molog("[attack] enemy belongs to us now, move in\n");
1011 pop_task(game);
1012 set_location(newsite);
1013 newsite->update_soldier_request();
1014 return schedule_act(game, 10);
1015 }
1016 }
1017 }
1018 // Return home
1019 state.ivar2 = 1;
1020 return schedule_act(game, 10);
1021 }
1022
1023 // At this point, we know that the enemy building still stands,
1024 // and that we're outside in the plains.
1025 if (get_position() != enemy->base_flag().get_position()) {
1026 if (start_task_movepath(game, enemy->base_flag().get_position(), 3,
1027 descr().get_right_walk_anims(does_carry_ware(), this)))
1028 return;
1029 else {
1030 molog("[attack] failed to move towards building flag, cancel attack "
1031 "and return home!\n");
1032 state.coords = Coords::null();
1033 state.objvar1 = nullptr;
1034 state.ivar2 = 1;
1035 return schedule_act(game, 10);
1036 }
1037 }
1038
1039 assert(enemy->attack_target() != nullptr);
1040
1041 molog("[attack] attacking target building\n");
1042 // give the enemy soldier some time to act
1043 schedule_act(
1044 game, enemy->attack_target()->attack(this) == AttackTarget::AttackResult::DefenderLaunched ?
1045 1000 :
1046 10);
1047 }
1048
attack_pop(Game & game,State &)1049 void Soldier::attack_pop(Game& game, State&) {
1050 if (battle_)
1051 battle_->cancel(game, *this);
1052 }
1053
1054 /**
1055 * Accept Bob when is a Soldier alive that is attacking the Player.
1056 */
1057 struct FindBobSoldierAttackingPlayer : public FindBob {
FindBobSoldierAttackingPlayerWidelands::FindBobSoldierAttackingPlayer1058 FindBobSoldierAttackingPlayer(Game& g, Player& p) : player(p), game(g) {
1059 }
1060
acceptWidelands::FindBobSoldierAttackingPlayer1061 bool accept(Bob* const bob) const override {
1062 if (upcast(Soldier, soldier, bob)) {
1063 return soldier->get_current_health() && soldier->is_attacking_player(game, player) &&
1064 soldier->owner().is_hostile(player);
1065 }
1066 return false;
1067 }
1068
1069 Player& player;
1070 Game& game;
1071 };
1072
1073 /**
1074 * Soldiers with this task go out of his buildings. They will
1075 * try to find an enemy in his lands and go to hunt them down (signaling
1076 * "battle"). If no enemy was found inside our lands, but an enemy is found
1077 * outside our lands, then wait until the enemy goes inside or dissapear.
1078 * If no enemy is found, then return home.
1079 *
1080 * Variables used:
1081 * \li ivar1 used to store \c CombatFlags
1082 * \li ivar2 when CF_DEFEND_STAYHOME, 1 if it has reached the flag
1083 // when CF_RETREAT_WHEN_INJURED, the lesser health before fleeing
1084 */
1085 Bob::Task const Soldier::taskDefense = {"defense", static_cast<Bob::Ptr>(&Soldier::defense_update),
1086 nullptr, static_cast<Bob::Ptr>(&Soldier::defense_pop),
1087 true};
1088
start_task_defense(Game & game,bool stayhome)1089 void Soldier::start_task_defense(Game& game, bool stayhome) {
1090 molog("[defense] starting\n");
1091 push_task(game, taskDefense);
1092 State& state = top_state();
1093
1094 state.ivar1 = 0;
1095 state.ivar2 = 0;
1096
1097 // Here goes 'configuration'
1098 if (stayhome) {
1099 state.ivar1 |= CF_DEFEND_STAYHOME;
1100 set_retreat_health(0);
1101 } else {
1102 /* Flag defenders are not allowed to flee, to avoid abuses */
1103 state.ivar1 |= CF_RETREAT_WHEN_INJURED;
1104 set_retreat_health(get_max_health() * kRetreatWhenHealthDropsBelowThisPercentage / 100);
1105
1106 // Soldier must defend even if he starts injured
1107 // (current health is below retreat treshold)
1108 if (get_retreat_health() > get_current_health()) {
1109 set_retreat_health(get_current_health());
1110 }
1111 }
1112 molog("[defense] retreat health set: %d\n", get_retreat_health());
1113 }
1114
1115 struct SoldierDistance {
1116 Soldier* s;
1117 int dist;
1118
SoldierDistanceWidelands::SoldierDistance1119 SoldierDistance(Soldier* a, int d) : dist(d) {
1120 s = a;
1121 }
1122
1123 struct Greater {
operator ()Widelands::SoldierDistance::Greater1124 bool operator()(const SoldierDistance& a, const SoldierDistance& b) {
1125 return (a.dist > b.dist);
1126 }
1127 };
1128 };
1129
defense_update(Game & game,State & state)1130 void Soldier::defense_update(Game& game, State& state) {
1131 std::string signal = get_signal();
1132
1133 if (signal.size()) {
1134 if (signal == "blocked" || signal == "battle" || signal == "wakeup") {
1135 signal_handled();
1136 } else {
1137 molog("[defense] cancelled by signal '%s'\n", signal.c_str());
1138 return pop_task(game);
1139 }
1140 }
1141
1142 PlayerImmovable* const location = get_location(game);
1143 BaseImmovable* const position = game.map()[get_position()].get_immovable();
1144
1145 /**
1146 * Attempt to fix a crash when player bulldozes a building being defended
1147 * by soldiers.
1148 */
1149 if (!location)
1150 return pop_task(game);
1151
1152 Flag& baseflag = location->base_flag();
1153
1154 if (battle_)
1155 return start_task_battle(game);
1156
1157 if (signal == "blocked")
1158 // Wait before we try again. Note that this must come *after*
1159 // we check for a battle
1160 // Note that we *should* be woken via send_space_signals,
1161 // so the timeout is just an additional safety net.
1162 return start_task_idle(game, descr().get_animation("idle", this), 5000);
1163
1164 // If we only are defending our home ...
1165 if (state.ivar1 & CF_DEFEND_STAYHOME) {
1166 if (position == location && state.ivar2 == 1) {
1167 molog("[defense] stayhome: returned home\n");
1168 return pop_task_or_fight(game);
1169 }
1170
1171 if (position == &baseflag) {
1172 state.ivar2 = 1;
1173 assert(state.ivar2 == 1);
1174
1175 if (battle_)
1176 return start_task_battle(game);
1177
1178 // Check if any attacker is waiting us to fight
1179 std::vector<Bob*> soldiers;
1180 game.map().find_bobs(
1181 game, Area<FCoords>(get_position(), 0), &soldiers, FindBobEnemySoldier(get_owner()));
1182
1183 for (Bob* temp_bob : soldiers) {
1184 if (upcast(Soldier, temp_soldier, temp_bob)) {
1185 if (temp_soldier->can_be_challenged()) {
1186 assert(temp_soldier != nullptr);
1187 new Battle(game, this, temp_soldier);
1188 return start_task_battle(game);
1189 }
1190 }
1191 }
1192
1193 if (state.ivar2 == 1) {
1194 molog("[defense] stayhome: return home\n");
1195 return start_task_return(game, false);
1196 }
1197 }
1198
1199 molog("[defense] stayhome: leavebuilding\n");
1200 return start_task_leavebuilding(game, false);
1201 }
1202
1203 // We are outside our building, get list of enemy soldiers attacking us
1204 std::vector<Bob*> soldiers;
1205 game.map().find_bobs(game, Area<FCoords>(get_position(), 10), &soldiers,
1206 FindBobSoldierAttackingPlayer(game, *get_owner()));
1207
1208 if (soldiers.empty() || (get_current_health() < get_retreat_health())) {
1209 if (get_retreat_health() > get_current_health()) {
1210 assert(state.ivar1 & CF_RETREAT_WHEN_INJURED);
1211 }
1212
1213 if (get_current_health() < get_retreat_health()) {
1214 molog("[defense] I am heavily injured (%d)!\n", get_current_health());
1215 } else
1216 molog("[defense] no enemy soldiers found, ending task\n");
1217
1218 // If no enemy was found, return home
1219 if (!location) {
1220 molog("[defense] location disappeared during battle\n");
1221 return pop_task(game);
1222 }
1223
1224 // Soldier is inside of building
1225 if (position == location) {
1226 molog("[defense] returned home\n");
1227 return pop_task_or_fight(game);
1228 }
1229
1230 // Soldier is on base flag
1231 if (position == &baseflag) {
1232 return start_task_move(
1233 game, WALK_NW, descr().get_right_walk_anims(does_carry_ware(), this), true);
1234 }
1235 molog("[defense] return home\n");
1236 if (start_task_movepath(game, baseflag.get_position(),
1237 4, // use larger persist when returning home
1238 descr().get_right_walk_anims(does_carry_ware(), this)))
1239 return;
1240
1241 molog("[defense] could not find way home\n");
1242 return pop_task(game);
1243 }
1244
1245 // Go through soldiers
1246 std::vector<SoldierDistance> targets;
1247 for (Bob* temp_bob : soldiers) {
1248
1249 // If enemy is in our land, then go after it!
1250 if (upcast(Soldier, soldier, temp_bob)) {
1251 assert(soldier != this);
1252 Field const f = game.map().operator[](soldier->get_position());
1253
1254 // Check soldier, be sure that we can fight against soldier.
1255 // Soldiers can not go over enemy land when defending.
1256 if ((soldier->can_be_challenged()) && (f.get_owned_by() == get_owner()->player_number())) {
1257 uint32_t thisDist = game.map().calc_distance(get_position(), soldier->get_position());
1258 targets.push_back(SoldierDistance(soldier, thisDist));
1259 }
1260 }
1261 }
1262
1263 std::stable_sort(targets.begin(), targets.end(), SoldierDistance::Greater());
1264
1265 while (!targets.empty()) {
1266 const SoldierDistance& target = targets.back();
1267
1268 if (position == location) {
1269 return start_task_leavebuilding(game, false);
1270 }
1271
1272 if (target.dist <= 1) {
1273 molog("[defense] starting battle with %u!\n", target.s->serial());
1274 assert(target.s != nullptr);
1275 new Battle(game, this, target.s);
1276 return start_task_battle(game);
1277 }
1278
1279 // Move towards soldier
1280 if (start_task_movepath(game, target.s->get_position(), 3,
1281 descr().get_right_walk_anims(does_carry_ware(), this), false, 1)) {
1282 molog("[defense] move towards soldier %u\n", target.s->serial());
1283 return;
1284 } else {
1285 molog("[defense] failed to move towards attacking soldier %u\n", target.s->serial());
1286 targets.pop_back();
1287 }
1288 }
1289 // If the enemy is not in our land, wait
1290 return start_task_idle(game, descr().get_animation("idle", this), 250);
1291 }
1292
defense_pop(Game & game,State &)1293 void Soldier::defense_pop(Game& game, State&) {
1294 if (battle_)
1295 battle_->cancel(game, *this);
1296 }
1297
1298 Bob::Task const Soldier::taskMoveInBattle = {
1299 "moveInBattle", static_cast<Bob::Ptr>(&Soldier::move_in_battle_update), nullptr, nullptr, true};
1300
start_task_move_in_battle(Game & game,CombatWalkingDir dir)1301 void Soldier::start_task_move_in_battle(Game& game, CombatWalkingDir dir) {
1302 int32_t mapdir = IDLE;
1303
1304 switch (dir) {
1305 case CD_WALK_W:
1306 case CD_RETURN_E:
1307 mapdir = WALK_W;
1308 break;
1309 case CD_WALK_E:
1310 case CD_RETURN_W:
1311 mapdir = WALK_E;
1312 break;
1313 case CD_NONE:
1314 case CD_COMBAT_E:
1315 case CD_COMBAT_W:
1316 throw GameDataError("bad direction '%d'", dir);
1317 }
1318
1319 const Map& map = game.map();
1320 int32_t const tdelta = (map.calc_cost(get_position(), mapdir)) / 2;
1321 molog("[move_in_battle] dir: (%d) tdelta: (%d)\n", dir, tdelta);
1322 combat_walking_ = dir;
1323 combat_walkstart_ = game.get_gametime();
1324 combat_walkend_ = combat_walkstart_ + tdelta;
1325
1326 push_task(game, taskMoveInBattle);
1327 State& state = top_state();
1328 state.ivar1 = dir;
1329 set_animation(game, descr().get_animation(mapdir == WALK_E ? "walk_e" : "walk_w", this));
1330 }
1331
move_in_battle_update(Game & game,State &)1332 void Soldier::move_in_battle_update(Game& game, State&) {
1333 if (game.get_gametime() >= combat_walkend_) {
1334 switch (combat_walking_) {
1335 case CD_NONE:
1336 break;
1337 case CD_WALK_W:
1338 combat_walking_ = CD_COMBAT_W;
1339 break;
1340 case CD_WALK_E:
1341 combat_walking_ = CD_COMBAT_E;
1342 break;
1343 case CD_RETURN_W:
1344 case CD_RETURN_E:
1345 case CD_COMBAT_W:
1346 case CD_COMBAT_E:
1347 combat_walking_ = CD_NONE;
1348 break;
1349 }
1350 return pop_task(game);
1351 } else
1352 // Only end the task once we've actually completed the step
1353 // Ignore signals until then
1354 return schedule_act(game, combat_walkend_ - game.get_gametime());
1355 }
1356
1357 /**
1358 * \return \c true if the defending soldier should not stray from
1359 * his home flag.
1360 */
stay_home()1361 bool Soldier::stay_home() {
1362 if (State const* const state = get_state(taskDefense))
1363 return state->ivar1 & CF_DEFEND_STAYHOME;
1364 return false;
1365 }
1366
1367 /**
1368 * We are out in the open and involved in a challenge/battle.
1369 * Meet with the other soldier and fight.
1370 */
1371 Bob::Task const Soldier::taskBattle = {"battle", static_cast<Bob::Ptr>(&Soldier::battle_update),
1372 nullptr, static_cast<Bob::Ptr>(&Soldier::battle_pop), true};
1373
start_task_battle(Game & game)1374 void Soldier::start_task_battle(Game& game) {
1375 assert(battle_);
1376 combat_walking_ = CD_NONE;
1377
1378 push_task(game, taskBattle);
1379 }
1380
battle_update(Game & game,State &)1381 void Soldier::battle_update(Game& game, State&) {
1382 std::string signal = get_signal();
1383 molog("[battle] update for player %u's soldier: signal = \"%s\"\n", owner().player_number(),
1384 signal.c_str());
1385
1386 if (signal.size()) {
1387 if (signal == "blocked") {
1388 signal_handled();
1389 return start_task_idle(game, descr().get_animation("idle", this), 5000);
1390 } else if (signal == "location" || signal == "battle" || signal == "wakeup")
1391 signal_handled();
1392 else {
1393 molog("[battle] interrupted by unexpected signal '%s'\n", signal.c_str());
1394 return pop_task(game);
1395 }
1396 }
1397
1398 // The opponent might have died on us
1399 if (!battle_ || battle_->opponent(*this) == nullptr) {
1400 if (combat_walking_ == CD_COMBAT_W) {
1401 return start_task_move_in_battle(game, CD_RETURN_W);
1402 }
1403 if (combat_walking_ == CD_COMBAT_E) {
1404 return start_task_move_in_battle(game, CD_RETURN_E);
1405 }
1406 assert(combat_walking_ == CD_NONE);
1407 molog("[battle] is over\n");
1408 send_space_signals(game);
1409 return pop_task(game);
1410 }
1411
1412 const Map& map = game.map();
1413 Soldier& opponent = *battle_->opponent(*this);
1414 if (opponent.get_position() != get_position()) {
1415 if (is_a(Building, map[get_position()].get_immovable())) {
1416 // Note that this does not use the "leavebuilding" task,
1417 // because that task is geared towards orderly workers leaving
1418 // their location, whereas this case can also happen when
1419 // a player starts a construction site over a waiting soldier.
1420 molog("[battle] we are in a building, leave it\n");
1421 return start_task_move(
1422 game, WALK_SE, descr().get_right_walk_anims(does_carry_ware(), this), true);
1423 }
1424 }
1425
1426 if (stay_home()) {
1427 if (this == battle_->first()) {
1428 molog("[battle] stay_home, so reverse roles\n");
1429 new Battle(game, battle_->second(), battle_->first());
1430 return skip_act(); // we will get a signal via set_battle()
1431 } else {
1432 if (combat_walking_ != CD_COMBAT_E) {
1433 opponent.send_signal(game, "wakeup");
1434 return start_task_move_in_battle(game, CD_WALK_E);
1435 }
1436 }
1437 } else {
1438 if (opponent.stay_home() && (this == battle_->second())) {
1439 // Wait until correct roles are assigned
1440 new Battle(game, battle_->second(), battle_->first());
1441 return schedule_act(game, 10);
1442 }
1443
1444 if (opponent.get_position() != get_position()) {
1445 Coords dest = opponent.get_position();
1446
1447 if (upcast(Building, building, map[dest].get_immovable()))
1448 dest = building->base_flag().get_position();
1449
1450 uint32_t const dist = map.calc_distance(get_position(), dest);
1451
1452 if (dist >= 2 || this == battle_->first()) {
1453 // Only make small steps at a time, so we can adjust to the
1454 // opponent's change of position.
1455 if (start_task_movepath(game, dest, 0,
1456 descr().get_right_walk_anims(does_carry_ware(), this), false,
1457 (dist + 3) / 4)) {
1458 molog("[battle] player %u's soldier started task_movepath to (%i,%i)\n",
1459 owner().player_number(), dest.x, dest.y);
1460 return;
1461 } else {
1462 BaseImmovable const* const immovable_position =
1463 get_position().field->get_immovable();
1464 BaseImmovable const* const immovable_dest = map[dest].get_immovable();
1465
1466 const std::string messagetext =
1467 (boost::format("The game engine has encountered a logic error. The %s "
1468 "#%u of player %u could not find a way from (%i, %i) "
1469 "(with %s immovable) to the opponent (%s #%u of player "
1470 "%u) at (%i, %i) (with %s immovable). The %s will now "
1471 "desert (but will not be executed). Strange things may "
1472 "happen. No solution for this problem has been "
1473 "implemented yet. (bug #536066) (The game has been "
1474 "paused.)") %
1475 descr().descname().c_str() % serial() %
1476 static_cast<unsigned int>(owner().player_number()) % get_position().x %
1477 get_position().y %
1478 (immovable_position ? immovable_position->descr().descname().c_str() : ("no")) %
1479 opponent.descr().descname().c_str() % opponent.serial() %
1480 static_cast<unsigned int>(opponent.owner().player_number()) % dest.x % dest.y %
1481 (immovable_dest ? immovable_dest->descr().descname().c_str() : ("no")) %
1482 descr().descname().c_str())
1483 .str();
1484 get_owner()->add_message(
1485 game, std::unique_ptr<Message>(
1486 new Message(Message::Type::kGameLogic, game.get_gametime(),
1487 descr().descname(), "images/ui_basic/menu_help.png",
1488 _("Logic error"), messagetext, get_position(), serial_)));
1489 opponent.get_owner()->add_message(
1490 game, std::unique_ptr<Message>(new Message(
1491 Message::Type::kGameLogic, game.get_gametime(), descr().descname(),
1492 "images/ui_basic/menu_help.png", _("Logic error"), messagetext,
1493 opponent.get_position(), serial_)));
1494 game.game_controller()->set_desired_speed(0);
1495 return pop_task(game);
1496 }
1497 }
1498 } else {
1499 assert(opponent.get_position() == get_position());
1500 assert(battle_ == opponent.get_battle());
1501
1502 if (opponent.is_walking()) {
1503 molog("[battle]: Opponent '%d' is walking, sleeping\n", opponent.serial());
1504 // We should be woken up by our opponent, but add a timeout anyway for robustness
1505 return start_task_idle(game, descr().get_animation("idle", this), 5000);
1506 }
1507
1508 if (battle_->first()->serial() == serial()) {
1509 if (combat_walking_ != CD_COMBAT_W) {
1510 molog("[battle]: Moving west\n");
1511 opponent.send_signal(game, "wakeup");
1512 return start_task_move_in_battle(game, CD_WALK_W);
1513 }
1514 } else {
1515 if (combat_walking_ != CD_COMBAT_E) {
1516 molog("[battle]: Moving east\n");
1517 opponent.send_signal(game, "wakeup");
1518 return start_task_move_in_battle(game, CD_WALK_E);
1519 }
1520 }
1521 }
1522 }
1523
1524 battle_->get_battle_work(game, *this);
1525 }
1526
battle_pop(Game & game,State &)1527 void Soldier::battle_pop(Game& game, State&) {
1528 if (battle_)
1529 battle_->cancel(game, *this);
1530 }
1531
1532 Bob::Task const Soldier::taskDie = {"die", static_cast<Bob::Ptr>(&Soldier::die_update), nullptr,
1533 static_cast<Bob::Ptr>(&Soldier::die_pop), true};
1534
start_task_die(Game & game)1535 void Soldier::start_task_die(Game& game) {
1536 push_task(game, taskDie);
1537 top_state().ivar1 = game.get_gametime() + 1000;
1538
1539 // Dead soldier is not owned by a location
1540 set_location(nullptr);
1541
1542 const uint32_t anim =
1543 descr().get_rand_anim(game, combat_walking_ == CD_COMBAT_W ? "die_w" : "die_e", this);
1544 start_task_idle(game, anim, 1000);
1545 }
1546
die_update(Game & game,State & state)1547 void Soldier::die_update(Game& game, State& state) {
1548 std::string signal = get_signal();
1549 molog("[die] update for player %u's soldier: signal = \"%s\"\n", owner().player_number(),
1550 signal.c_str());
1551
1552 if (signal.size()) {
1553 signal_handled();
1554 }
1555
1556 if ((state.ivar1 >= 0) && (static_cast<uint32_t>(state.ivar1) > game.get_gametime()))
1557 return schedule_act(game, state.ivar1 - game.get_gametime());
1558
1559 // When task updated, dead is near!
1560 return pop_task(game);
1561 }
1562
die_pop(Game & game,State &)1563 void Soldier::die_pop(Game& game, State&) {
1564 // Destroy the soldier!
1565 molog("[die] soldier %u has died\n", serial());
1566 schedule_destroy(game);
1567 }
1568
1569 /**
1570 * Accept a Bob if it is a Soldier, is not dead and is running taskAttack or
1571 * taskDefense.
1572 */
1573 struct FindBobSoldierOnBattlefield : public FindBob {
acceptWidelands::FindBobSoldierOnBattlefield1574 bool accept(Bob* const bob) const override {
1575 if (upcast(Soldier, soldier, bob))
1576 return soldier->is_on_battlefield() && soldier->get_current_health();
1577 return false;
1578 }
1579 };
1580
1581 /**
1582 * Override \ref Bob::check_node_blocked.
1583 *
1584 * As long as we're on the battlefield, check for other soldiers.
1585 */
check_node_blocked(Game & game,const FCoords & field,bool const commit)1586 bool Soldier::check_node_blocked(Game& game, const FCoords& field, bool const commit) {
1587 State* attackdefense = get_state(taskAttack);
1588
1589 if (!attackdefense)
1590 attackdefense = get_state(taskDefense);
1591
1592 if (!attackdefense || ((attackdefense->ivar1 & CF_RETREAT_WHEN_INJURED) &&
1593 get_retreat_health() > get_current_health())) {
1594 // Retreating or non-combatant soldiers act like normal bobs
1595 return Bob::check_node_blocked(game, field, commit);
1596 }
1597
1598 if (field.field->get_immovable() && field.field->get_immovable() == get_location(game)) {
1599 if (commit)
1600 send_space_signals(game);
1601 return false; // we can always walk home
1602 }
1603
1604 Soldier* foundsoldier = nullptr;
1605 bool foundbattle = false;
1606 bool foundopponent = false;
1607 bool multiplesoldiers = false;
1608
1609 for (Bob* bob = field.field->get_first_bob(); bob; bob = bob->get_next_on_field()) {
1610 if (upcast(Soldier, soldier, bob)) {
1611 if (!soldier->is_on_battlefield() || !soldier->get_current_health())
1612 continue;
1613
1614 if (!foundsoldier) {
1615 foundsoldier = soldier;
1616 } else {
1617 multiplesoldiers = true;
1618 }
1619
1620 if (soldier->get_battle()) {
1621 foundbattle = true;
1622
1623 if (battle_ && battle_->opponent(*this) == soldier)
1624 foundopponent = true;
1625 }
1626 }
1627 }
1628
1629 if (!foundopponent && (foundbattle || foundsoldier)) {
1630 if (commit && !foundbattle && !multiplesoldiers) {
1631 if (foundsoldier->owner().is_hostile(*get_owner()) && foundsoldier->can_be_challenged()) {
1632 molog("[check_node_blocked] attacking a soldier (%u)\n", foundsoldier->serial());
1633 new Battle(game, this, foundsoldier);
1634 }
1635 }
1636
1637 return true;
1638 } else {
1639 if (commit)
1640 send_space_signals(game);
1641 return false;
1642 }
1643 }
1644
1645 /**
1646 * Send a "wakeup" signal to all surrounding soldiers that are out in the open,
1647 * so that they may repeat pathfinding.
1648 */
send_space_signals(Game & game)1649 void Soldier::send_space_signals(Game& game) {
1650 std::vector<Bob*> soldiers;
1651
1652 game.map().find_bobs(
1653 game, Area<FCoords>(get_position(), 1), &soldiers, FindBobSoldierOnBattlefield());
1654
1655 for (Bob* temp_soldier : soldiers) {
1656 if (upcast(Soldier, soldier, temp_soldier)) {
1657 if (soldier != this) {
1658 soldier->send_signal(game, "wakeup");
1659 }
1660 }
1661 }
1662
1663 PlayerNumber const land_owner = get_position().field->get_owned_by();
1664 // First check if the soldier is standing on someone else's territory
1665 if (land_owner != owner().player_number()) {
1666 // Let's collect all reachable attack_target sites in vicinity (militarysites mainly)
1667 std::vector<BaseImmovable*> attack_targets;
1668 game.map().find_reachable_immovables_unique(
1669 game, Area<FCoords>(get_position(), kMaxProtectionRadius), attack_targets,
1670 CheckStepWalkOn(descr().movecaps(), false), FindImmovableAttackTarget());
1671
1672 for (BaseImmovable* temp_attack_target : attack_targets) {
1673 Building* building = dynamic_cast<Building*>(temp_attack_target);
1674 assert(building != nullptr && building->attack_target() != nullptr);
1675 const Player& attack_target_player = building->owner();
1676 // Let's inform the site that this (=enemy) soldier is nearby and within the site's owner's
1677 // territory
1678 if (attack_target_player.player_number() == land_owner &&
1679 attack_target_player.is_hostile(*get_owner())) {
1680 building->attack_target()->enemy_soldier_approaches(*this);
1681 }
1682 }
1683 }
1684 }
1685
log_general_info(const EditorGameBase & egbase) const1686 void Soldier::log_general_info(const EditorGameBase& egbase) const {
1687 Worker::log_general_info(egbase);
1688 molog("[Soldier]\n");
1689 molog("Levels: %d/%d/%d/%d\n", health_level_, attack_level_, defense_level_, evade_level_);
1690 molog("Health: %d/%d\n", current_health_, get_max_health());
1691 molog("Retreat: %d\n", retreat_health_);
1692 molog("Attack: %d-%d\n", get_min_attack(), get_max_attack());
1693 molog("Defense: %d%%\n", get_defense());
1694 molog("Evade: %d%%\n", get_evade());
1695 molog("CombatWalkingDir: %i\n", combat_walking_);
1696 molog("CombatWalkingStart: %i\n", combat_walkstart_);
1697 molog("CombatWalkEnd: %i\n", combat_walkend_);
1698 molog("HasBattle: %s\n", battle_ ? "yes" : "no");
1699 if (battle_) {
1700 molog("BattleSerial: %u\n", battle_->serial());
1701 molog("Opponent: %u\n", battle_->opponent(*this)->serial());
1702 }
1703 }
1704
1705 /*
1706 ==============================
1707
1708 Load/save support
1709
1710 ==============================
1711 */
1712
1713 constexpr uint8_t kCurrentPacketVersion = 3;
1714 // TODO(TiborB): This is only for map compatibility in regression tests, we should get rid of this
1715 // ASAP
1716 constexpr uint8_t kOldPacketVersion = 2;
1717
Loader()1718 Soldier::Loader::Loader() : battle_(0) {
1719 }
1720
load(FileRead & fr)1721 void Soldier::Loader::load(FileRead& fr) {
1722 Worker::Loader::load(fr);
1723
1724 try {
1725 uint8_t packet_version = fr.unsigned_8();
1726 if (packet_version == kCurrentPacketVersion || packet_version == kOldPacketVersion) {
1727
1728 Soldier& soldier = get<Soldier>();
1729 soldier.current_health_ = fr.unsigned_32();
1730 if (packet_version == kCurrentPacketVersion) {
1731 soldier.retreat_health_ = fr.unsigned_32();
1732 } else {
1733 // not ideal but will be used only for regression tests
1734 soldier.retreat_health_ = 0;
1735 }
1736
1737 soldier.health_level_ = std::min(fr.unsigned_32(), soldier.descr().get_max_health_level());
1738 soldier.attack_level_ = std::min(fr.unsigned_32(), soldier.descr().get_max_attack_level());
1739 soldier.defense_level_ =
1740 std::min(fr.unsigned_32(), soldier.descr().get_max_defense_level());
1741 soldier.evade_level_ = std::min(fr.unsigned_32(), soldier.descr().get_max_evade_level());
1742
1743 if (soldier.current_health_ > soldier.get_max_health())
1744 soldier.current_health_ = soldier.get_max_health();
1745
1746 if (soldier.retreat_health_ > soldier.get_max_health())
1747 soldier.retreat_health_ = soldier.get_max_health();
1748
1749 soldier.combat_walking_ = static_cast<CombatWalkingDir>(fr.unsigned_8());
1750 if (soldier.combat_walking_ != CD_NONE) {
1751 soldier.combat_walkstart_ = fr.unsigned_32();
1752 soldier.combat_walkend_ = fr.unsigned_32();
1753 }
1754
1755 battle_ = fr.unsigned_32();
1756 } else {
1757 throw UnhandledVersionError("Soldier", packet_version, kCurrentPacketVersion);
1758 }
1759 } catch (const std::exception& e) {
1760 throw wexception("loading soldier: %s", e.what());
1761 }
1762 }
1763
load_pointers()1764 void Soldier::Loader::load_pointers() {
1765 Worker::Loader::load_pointers();
1766
1767 Soldier& soldier = get<Soldier>();
1768
1769 if (battle_)
1770 soldier.battle_ = &mol().get<Battle>(battle_);
1771 }
1772
get_task(const std::string & name)1773 const Bob::Task* Soldier::Loader::get_task(const std::string& name) {
1774 if (name == "attack")
1775 return &taskAttack;
1776 if (name == "defense")
1777 return &taskDefense;
1778 if (name == "battle")
1779 return &taskBattle;
1780 if (name == "moveInBattle")
1781 return &taskMoveInBattle;
1782 if (name == "die")
1783 return &taskDie;
1784 return Worker::Loader::get_task(name);
1785 }
1786
create_loader()1787 Soldier::Loader* Soldier::create_loader() {
1788 return new Loader;
1789 }
1790
do_save(EditorGameBase & egbase,MapObjectSaver & mos,FileWrite & fw)1791 void Soldier::do_save(EditorGameBase& egbase, MapObjectSaver& mos, FileWrite& fw) {
1792 Worker::do_save(egbase, mos, fw);
1793
1794 fw.unsigned_8(kCurrentPacketVersion);
1795 fw.unsigned_32(current_health_);
1796 fw.unsigned_32(retreat_health_);
1797 fw.unsigned_32(health_level_);
1798 fw.unsigned_32(attack_level_);
1799 fw.unsigned_32(defense_level_);
1800 fw.unsigned_32(evade_level_);
1801
1802 fw.unsigned_8(combat_walking_);
1803 if (combat_walking_ != CD_NONE) {
1804 fw.unsigned_32(combat_walkstart_);
1805 fw.unsigned_32(combat_walkend_);
1806 }
1807
1808 fw.unsigned_32(mos.get_object_file_index_or_zero(battle_));
1809 }
1810 } // namespace Widelands
1811