1 /*
2 Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
3 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY.
11
12 See the COPYING file for more details.
13 */
14
15 /**
16 * @file
17 * Routines to manage units.
18 */
19
20 #include "units/unit.hpp"
21
22 #include "color.hpp"
23 #include "deprecation.hpp"
24 #include "display_context.hpp"
25 #include "formatter.hpp"
26 #include "formula/string_utils.hpp" // for VGETTEXT
27 #include "game_board.hpp" // for game_board
28 #include "game_config.hpp" // for add_color_info, etc
29 #include "game_data.hpp"
30 #include "game_errors.hpp" // for game_error
31 #include "game_events/manager.hpp" // for add_events
32 #include "preferences/game.hpp" // for encountered_units
33 #include "gettext.hpp" // for N_
34 #include "lexical_cast.hpp"
35 #include "log.hpp" // for LOG_STREAM, logger, etc
36 #include "map/map.hpp" // for gamemap
37 #include "random.hpp" // for generator, rng
38 #include "resources.hpp" // for units, gameboard, teams, etc
39 #include "scripting/game_lua_kernel.hpp" // for game_lua_kernel
40 #include "side_filter.hpp" // for side_filter
41 #include "synced_context.hpp"
42 #include "team.hpp" // for team, get_teams, etc
43 #include "terrain/filter.hpp" // for terrain_filter
44 #include "units/abilities.hpp" // for effect, filter_base_matches
45 #include "units/animation.hpp" // for unit_animation
46 #include "units/animation_component.hpp" // for unit_animation_component
47 #include "units/filter.hpp"
48 #include "units/formula_manager.hpp" // for unit_formula_manager
49 #include "units/id.hpp"
50 #include "units/map.hpp" // for unit_map, etc
51 #include "variable.hpp" // for vconfig, etc
52 #include "game_version.hpp"
53
54 #include "utils/functional.hpp"
55 #include <boost/dynamic_bitset.hpp>
56 #include <boost/function_output_iterator.hpp>
57
58 #ifdef _MSC_VER
59 #pragma warning (push)
60 #pragma warning (disable: 4510 4610)
61 #endif
62 #include <boost/range/algorithm.hpp>
63 #ifdef _MSC_VER
64 #pragma warning (pop)
65 #endif
66
67 #include <cassert> // for assert
68 #include <cstdlib> // for rand
69 #include <exception> // for exception
70 #include <iterator> // for back_insert_iterator, etc
71 #include <new> // for operator new
72 #include <ostream> // for operator<<, basic_ostream, etc
73
74 namespace t_translation { struct terrain_code; }
75
76 static lg::log_domain log_unit("unit");
77 #define DBG_UT LOG_STREAM(debug, log_unit)
78 #define LOG_UT LOG_STREAM(info, log_unit)
79 #define WRN_UT LOG_STREAM(warn, log_unit)
80 #define ERR_UT LOG_STREAM(err, log_unit)
81
82 namespace
83 {
84 // "advance" only kept around for backwards compatibility; only "advancement" should be used
85 const std::array<std::string, 4> ModificationTypes {{ "advancement", "advance", "trait", "object" }};
86
87 /**
88 * Pointers to units which have data in their internal caches. The
89 * destructor of an unit removes itself from the cache, so the pointers are
90 * always valid.
91 */
92 static std::vector<const unit*> units_with_cache;
93
94 static const std::string leader_crown_path = "misc/leader-crown.png";
95 static std::string internalized_attrs[] {
96 "type",
97 "id",
98 "name",
99 "gender",
100 "random_gender",
101 "variation",
102 "role",
103 "ai_special",
104 "side",
105 "underlying_id",
106 "overlays",
107 "facing",
108 "race",
109 "level",
110 "recall_cost",
111 "undead_variation",
112 "max_attacks",
113 "attacks_left",
114 "alpha",
115 "zoc",
116 "flying",
117 "cost",
118 "max_hitpoints",
119 "max_moves",
120 "vision",
121 "jamming",
122 "max_experience",
123 "advances_to",
124 "hitpoints",
125 "goto_x",
126 "goto_y",
127 "moves",
128 "experience",
129 "resting",
130 "unrenamable",
131 "alignment",
132 "canrecruit",
133 "extra_recruit",
134 "x",
135 "y",
136 "placement",
137 "parent_type",
138 "description",
139 "usage",
140 "halo",
141 "ellipse",
142 "upkeep",
143 "random_traits",
144 "generate_name",
145 "profile",
146 "small_profile",
147 "fire_event",
148 "passable",
149 "overwrite",
150 "location_id",
151 "hidden",
152 // Useless attributes created when saving units to WML:
153 "flag_rgb",
154 "language_name",
155 "image",
156 "image_icon"
157 };
158
159 struct internalized_attrs_sorter
160 {
internalized_attrs_sorter__anonf047bb650111::internalized_attrs_sorter161 internalized_attrs_sorter()
162 {
163 std::sort(std::begin(internalized_attrs), std::end(internalized_attrs));
164 }
165 };
166
167 // Sort the array to make set_difference below work.
168 internalized_attrs_sorter sorter;
169
warn_unknown_attribute(const config::const_attr_itors & cfg)170 void warn_unknown_attribute(const config::const_attr_itors& cfg)
171 {
172 config::const_attribute_iterator cur = cfg.begin();
173 config::const_attribute_iterator end = cfg.end();
174
175 const std::string* cur_known = std::begin(internalized_attrs);
176 const std::string* end_known = std::end(internalized_attrs);
177
178 while(cur_known != end_known) {
179 if(cur == end) {
180 return;
181 }
182 int comp = cur->first.compare(*cur_known);
183 if(comp < 0) {
184 WRN_UT << "Unknown attribute '" << cur->first << "' discarded." << std::endl;
185 ++cur;
186 }
187 else if(comp == 0) {
188 ++cur;
189 ++cur_known;
190 }
191 else {
192 ++cur_known;
193 }
194 }
195
196 while(cur != end) {
197 WRN_UT << "Unknown attribute '" << cur->first << "' discarded." << std::endl;
198 ++cur;
199 }
200 }
201
202 template<typename T>
copy_or_null(const std::unique_ptr<T> & ptr)203 T* copy_or_null(const std::unique_ptr<T>& ptr)
204 {
205 return ptr ? new T(*ptr) : nullptr;
206 }
207 } // end anon namespace
208
209 /**
210 * Intrusive Pointer interface
211 *
212 **/
213
intrusive_ptr_add_ref(const unit * u)214 void intrusive_ptr_add_ref(const unit* u)
215 {
216 assert(u->ref_count_ >= 0);
217 // the next code line is to notice possible wrongly initialized units.
218 // The 100000 is picked rather randomly. If you are in the situation
219 // that you can actually have more then 100000 intrusive_ptr to one unit
220 // or if you are sure that the refcounting system works
221 // then feel free to remove the next line
222 assert(u->ref_count_ < 100000);
223 if(u->ref_count_ == 0) {
224 LOG_UT << "Freshly constructed" << std::endl;
225 }
226 ++(u->ref_count_);
227 }
228
intrusive_ptr_release(const unit * u)229 void intrusive_ptr_release(const unit* u)
230 {
231 assert(u->ref_count_ >= 1);
232 assert(u->ref_count_ < 100000); //See comment in intrusive_ptr_add_ref
233 if(--(u->ref_count_) == 0)
234 {
235 DBG_UT << "Deleting a unit: id = " << u->id() << ", uid = " << u->underlying_id() << std::endl;
236 delete u;
237 }
238 }
239
240 /**
241 * Converts a string ID to a unit_type.
242 * Throws a game_error exception if the string does not correspond to a type.
243 */
get_unit_type(const std::string & type_id)244 static const unit_type& get_unit_type(const std::string& type_id)
245 {
246 if(type_id.empty()) {
247 throw unit_type::error("creating unit with an empty type field");
248 }
249 std::string new_id = type_id;
250 unit_type::check_id(new_id);
251 const unit_type* i = unit_types.find(new_id);
252 if(!i) throw unit_type::error("unknown unit type: " + type_id);
253 return *i;
254 }
255
generate_gender(const unit_type & type,bool random_gender)256 static unit_race::GENDER generate_gender(const unit_type& type, bool random_gender)
257 {
258 const std::vector<unit_race::GENDER>& genders = type.genders();
259 assert(genders.size() > 0);
260
261 if(random_gender == false || genders.size() == 1) {
262 return genders.front();
263 } else {
264 return genders[randomness::generator->get_random_int(0,genders.size()-1)];
265 // Note: genders is guaranteed to be non-empty, so this is not a
266 // potential division by zero.
267 // Note: Whoever wrote this code, you should have used an assertion, to save others hours of work...
268 // If the assertion size>0 is failing for you, one possible cause is that you are constructing a unit
269 // from a unit type which has not been ``built'' using the unit_type_data methods.
270 }
271 }
272
generate_gender(const unit_type & u_type,const config & cfg)273 static unit_race::GENDER generate_gender(const unit_type& u_type, const config& cfg)
274 {
275 const std::string& gender = cfg["gender"];
276 if(!gender.empty()) {
277 return string_gender(gender);
278 }
279
280 return generate_gender(u_type, cfg["random_gender"].to_bool());
281 }
282
283 struct ptr_vector_pushback
284 {
ptr_vector_pushbackptr_vector_pushback285 ptr_vector_pushback(boost::ptr_vector<config>& vec) : vec_(&vec) {}
286
operator ()ptr_vector_pushback287 void operator()(const config& cfg)
288 {
289 vec_->push_back(new config(cfg));
290 }
291
292 //Don't use reference to be copyable.
293 boost::ptr_vector<config>* vec_;
294 };
295
296 // Copy constructor
unit(const unit & o)297 unit::unit(const unit& o)
298 : ref_count_(0)
299 , loc_(o.loc_)
300 , advances_to_(o.advances_to_)
301 , type_(o.type_)
302 , type_name_(o.type_name_)
303 , race_(o.race_)
304 , id_(o.id_)
305 , name_(o.name_)
306 , underlying_id_(o.underlying_id_)
307 , undead_variation_(o.undead_variation_)
308 , variation_(o.variation_)
309 , hit_points_(o.hit_points_)
310 , max_hit_points_(o.max_hit_points_)
311 , experience_(o.experience_)
312 , max_experience_(o.max_experience_)
313 , level_(o.level_)
314 , recall_cost_(o.recall_cost_)
315 , canrecruit_(o.canrecruit_)
316 , recruit_list_(o.recruit_list_)
317 , alignment_(o.alignment_)
318 , flag_rgb_(o.flag_rgb_)
319 , image_mods_(o.image_mods_)
320 , unrenamable_(o.unrenamable_)
321 , side_(o.side_)
322 , gender_(o.gender_)
323 , formula_man_(new unit_formula_manager(o.formula_manager()))
324 , movement_(o.movement_)
325 , max_movement_(o.max_movement_)
326 , vision_(o.vision_)
327 , jamming_(o.jamming_)
328 , movement_type_(o.movement_type_)
329 , hold_position_(o.hold_position_)
330 , end_turn_(o.end_turn_)
331 , resting_(o.resting_)
332 , attacks_left_(o.attacks_left_)
333 , max_attacks_(o.max_attacks_)
334 , states_(o.states_)
335 , known_boolean_states_(o.known_boolean_states_)
336 , variables_(o.variables_)
337 , events_(o.events_)
338 , filter_recall_(o.filter_recall_)
339 , emit_zoc_(o.emit_zoc_)
340 , overlays_(o.overlays_)
341 , role_(o.role_)
342 , attacks_(o.attacks_)
343 , facing_(o.facing_)
344 , trait_names_(o.trait_names_)
345 , trait_descriptions_(o.trait_descriptions_)
346 , unit_value_(o.unit_value_)
347 , goto_(o.goto_)
348 , interrupted_move_(o.interrupted_move_)
349 , is_fearless_(o.is_fearless_)
350 , is_healthy_(o.is_healthy_)
351 , modification_descriptions_(o.modification_descriptions_)
352 , anim_comp_(new unit_animation_component(*this, *o.anim_comp_))
353 , hidden_(o.hidden_)
354 , hp_bar_scaling_(o.hp_bar_scaling_)
355 , xp_bar_scaling_(o.xp_bar_scaling_)
356 , modifications_(o.modifications_)
357 , abilities_(o.abilities_)
358 , advancements_(o.advancements_)
359 , description_(o.description_)
360 , usage_(copy_or_null(o.usage_))
361 , halo_(copy_or_null(o.halo_))
362 , ellipse_(copy_or_null(o.ellipse_))
363 , random_traits_(o.random_traits_)
364 , generate_name_(o.generate_name_)
365 , upkeep_(o.upkeep_)
366 , profile_(o.profile_)
367 , small_profile_(o.small_profile_)
368 , invisibility_cache_()
369 {
370 // Copy the attacks rather than just copying references
371 for(auto& a : attacks_) {
372 a.reset(new attack_type(*a));
373 }
374 }
375
unit()376 unit::unit()
377 : ref_count_(0)
378 , loc_()
379 , advances_to_()
380 , type_(nullptr)
381 , type_name_()
382 , race_(&unit_race::null_race)
383 , id_()
384 , name_()
385 , underlying_id_(0)
386 , undead_variation_()
387 , variation_()
388 , hit_points_(1)
389 , max_hit_points_(1)
390 , experience_(0)
391 , max_experience_(1)
392 , level_(0)
393 , recall_cost_(-1)
394 , canrecruit_(false)
395 , recruit_list_()
396 , alignment_()
397 , flag_rgb_()
398 , image_mods_()
399 , unrenamable_(false)
400 , side_(0)
401 , gender_(unit_race::NUM_GENDERS)
402 , formula_man_(new unit_formula_manager())
403 , movement_(0)
404 , max_movement_(0)
405 , vision_(-1)
406 , jamming_(0)
407 , movement_type_()
408 , hold_position_(false)
409 , end_turn_(false)
410 , resting_(false)
411 , attacks_left_(0)
412 , max_attacks_(0)
413 , states_()
414 , known_boolean_states_()
415 , variables_()
416 , events_()
417 , filter_recall_()
418 , emit_zoc_(0)
419 , overlays_()
420 , role_()
421 , attacks_()
422 , facing_(map_location::NDIRECTIONS)
423 , trait_names_()
424 , trait_descriptions_()
425 , unit_value_()
426 , goto_()
427 , interrupted_move_()
428 , is_fearless_(false)
429 , is_healthy_(false)
430 , modification_descriptions_()
431 , anim_comp_(new unit_animation_component(*this))
432 , hidden_(false)
433 , hp_bar_scaling_(0)
434 , xp_bar_scaling_(0)
435 , modifications_()
436 , abilities_()
437 , advancements_()
438 , description_()
439 , usage_()
440 , halo_()
441 , ellipse_()
442 , random_traits_(true)
443 , generate_name_(true)
444 , upkeep_(upkeep_full())
445 , invisibility_cache_()
446 {
447
448 }
449
init(const config & cfg,bool use_traits,const vconfig * vcfg)450 void unit::init(const config& cfg, bool use_traits, const vconfig* vcfg)
451 {
452 loc_ = map_location(cfg["x"], cfg["y"], wml_loc());
453 type_ = &get_unit_type(cfg["parent_type"].blank() ? cfg["type"].str() : cfg["parent_type"].str());
454 race_ = &unit_race::null_race;
455 id_ = cfg["id"].str();
456 name_ = cfg["name"].t_str();
457 variation_ = cfg["variation"].empty() ? type_->default_variation() : cfg["variation"].str();
458 canrecruit_ = cfg["canrecruit"].to_bool();
459 gender_ = generate_gender(*type_, cfg);
460 role_ = cfg["role"].str();
461 //, facing_(map_location::NDIRECTIONS)
462 //, anim_comp_(new unit_animation_component(*this))
463 hidden_ = cfg["hidden"].to_bool(false);
464 hp_bar_scaling_ = cfg["hp_bar_scaling"].blank() ? type_->hp_bar_scaling() : cfg["hp_bar_scaling"];
465 xp_bar_scaling_ = cfg["xp_bar_scaling"].blank() ? type_->xp_bar_scaling() : cfg["xp_bar_scaling"];
466 random_traits_ = true;
467 generate_name_ = true;
468 side_ = cfg["side"].to_int();
469
470 if(side_ <= 0) {
471 side_ = 1;
472 }
473
474 validate_side(side_);
475 underlying_id_ = n_unit::unit_id(cfg["underlying_id"].to_size_t());
476 set_underlying_id(resources::gameboard ? resources::gameboard->unit_id_manager() : n_unit::id_manager::global_instance());
477
478 overlays_ = utils::parenthetical_split(cfg["overlays"], ',');
479 if(overlays_.size() == 1 && overlays_.front().empty()) {
480 overlays_.clear();
481 }
482
483 if(vcfg) {
484 const vconfig& filter_recall = vcfg->child("filter_recall");
485 if(!filter_recall.null())
486 filter_recall_ = filter_recall.get_config();
487
488 const vconfig::child_list& events = vcfg->get_children("event");
489 for(const vconfig& e : events) {
490 events_.add_child("event", e.get_config());
491 }
492 } else {
493 filter_recall_ = cfg.child_or_empty("filter_recall");
494
495 for(const config& unit_event : cfg.child_range("event")) {
496 events_.add_child("event", unit_event);
497 }
498 }
499
500 if(resources::game_events) {
501 resources::game_events->add_events(events_.child_range("event"));
502 }
503
504 random_traits_ = cfg["random_traits"].to_bool(true);
505 facing_ = map_location::parse_direction(cfg["facing"]);
506 if(facing_ == map_location::NDIRECTIONS) facing_ = static_cast<map_location::DIRECTION>(randomness::rng::default_instance().get_random_int(0, map_location::NDIRECTIONS-1));
507
508 if(const config& mods = cfg.child("modifications")) {
509 modifications_ = mods;
510 }
511
512 generate_name_ = cfg["generate_name"].to_bool(true);
513
514 // Apply the unit type's data to this unit.
515 advance_to(*type_, use_traits);
516
517 if(const config& variables = cfg.child("variables")) {
518 variables_ = variables;
519 }
520
521 if(const config::attribute_value* v = cfg.get("race")) {
522 if(const unit_race *r = unit_types.find_race(*v)) {
523 race_ = r;
524 } else {
525 race_ = &unit_race::null_race;
526 }
527 }
528
529 level_ = cfg["level"].to_int(level_);
530
531 if(const config::attribute_value* v = cfg.get("undead_variation")) {
532 undead_variation_ = v->str();
533 }
534
535 if(const config::attribute_value* v = cfg.get("max_attacks")) {
536 max_attacks_ = std::max(0, v->to_int(1));
537 }
538
539 attacks_left_ = std::max(0, cfg["attacks_left"].to_int(max_attacks_));
540
541 if(const config::attribute_value* v = cfg.get("zoc")) {
542 emit_zoc_ = v->to_bool(level_ > 0);
543 }
544
545 if(const config::attribute_value* v = cfg.get("description")) {
546 description_ = *v;
547 }
548
549 if(const config::attribute_value* v = cfg.get("cost")) {
550 unit_value_ = *v;
551 }
552
553 if(const config::attribute_value* v = cfg.get("ellipse")) {
554 set_image_ellipse(*v);
555 }
556
557 if(const config::attribute_value* v = cfg.get("halo")) {
558 set_image_halo(*v);
559 }
560
561 if(const config::attribute_value* v = cfg.get("usage")) {
562 set_usage(*v);
563 }
564
565 if(const config::attribute_value* v = cfg.get("profile")) {
566 std::string profile = (*v).str();
567 adjust_profile(profile);
568 profile_ = profile;
569 }
570
571 if(const config::attribute_value* v = cfg.get("small_profile")) {
572 small_profile_ = (*v).str();
573 }
574
575 max_hit_points_ = std::max(1, cfg["max_hitpoints"].to_int(max_hit_points_));
576 max_movement_ = std::max(0, cfg["max_moves"].to_int(max_movement_));
577 max_experience_ = std::max(1, cfg["max_experience"].to_int(max_experience_));
578
579 vision_ = cfg["vision"].to_int(vision_);
580
581 std::vector<std::string> temp_advances = utils::split(cfg["advances_to"]);
582 if(temp_advances.size() == 1 && temp_advances.front() == "null") {
583 advances_to_.clear();
584 } else if(temp_advances.size() >= 1 && !temp_advances.front().empty()) {
585 advances_to_ = temp_advances;
586 }
587
588 if(const config& ai = cfg.child("ai")) {
589 formula_man_->read(ai);
590 }
591
592 // Don't use the unit_type's attacks if this config has its own defined
593 if(config::const_child_itors cfg_range = cfg.child_range("attack")) {
594 attacks_.clear();
595 for(const config& c : cfg_range) {
596 attacks_.emplace_back(new attack_type(c));
597 }
598 }
599
600 // If cfg specifies [advancement]s, replace this [advancement]s with them.
601 if(cfg.has_child("advancement")) {
602 this->advancements_.clear();
603 boost::copy(cfg.child_range("advancement"), boost::make_function_output_iterator(ptr_vector_pushback(advancements_)));
604 }
605
606 // Don't use the unit_type's abilities if this config has its own defined
607 // Why do we allow multiple [abilities] tags?
608 if(config::const_child_itors cfg_range = cfg.child_range("abilities")) {
609 abilities_.clear();
610 for(const config& abilities : cfg_range) {
611 this->abilities_.append(abilities);
612 }
613 }
614
615 // Adjust the unit's defense, movement, vision, jamming, resistances, and
616 // flying status if this config has its own defined.
617 movement_type_.merge(cfg);
618
619 if(const config& status_flags = cfg.child("status")) {
620 for(const config::attribute &st : status_flags.attribute_range()) {
621 if(st.second.to_bool()) {
622 set_state(st.first, true);
623 }
624 }
625 }
626
627 if(cfg["ai_special"] == "guardian") {
628 set_state(STATE_GUARDIAN, true);
629 }
630
631 if(const config::attribute_value* v = cfg.get("hitpoints")) {
632 hit_points_ = *v;
633 } else {
634 hit_points_ = max_hit_points_;
635 }
636
637 if(const config::attribute_value* v = cfg.get("invulnerable")) {
638 set_state("invulnerable", v->to_bool());
639 }
640
641 goto_.set_wml_x(cfg["goto_x"].to_int());
642 goto_.set_wml_y(cfg["goto_y"].to_int());
643
644 if(const config::attribute_value* v = cfg.get("moves")) {
645 movement_ = *v;
646 if(movement_ < 0) {
647 attacks_left_ = 0;
648 movement_ = 0;
649 }
650 } else {
651 movement_ = max_movement_;
652 }
653
654 experience_ = cfg["experience"];
655 resting_ = cfg["resting"].to_bool();
656 unrenamable_ = cfg["unrenamable"].to_bool();
657
658 // We need to check to make sure that the cfg is not blank and if it
659 // isn't pull that value otherwise it goes with the default of -1.
660 if(!cfg["recall_cost"].blank()) {
661 recall_cost_ = cfg["recall_cost"].to_int(recall_cost_);
662 }
663
664 alignment_.parse(cfg["alignment"].str());
665
666 generate_name();
667
668 parse_upkeep(cfg["upkeep"]);
669
670 set_recruits(utils::split(cfg["extra_recruit"]));
671
672 game_config::add_color_info(cfg);
673 warn_unknown_attribute(cfg.attribute_range());
674
675 #if 0
676 // Debug unit animations for units as they appear in game
677 for(const auto& anim : anim_comp_->animations_) {
678 std::cout << anim.debug() << std::endl;
679 }
680 #endif
681 }
682
clear_status_caches()683 void unit::clear_status_caches()
684 {
685 for(auto& u : units_with_cache) {
686 u->clear_visibility_cache();
687 }
688
689 units_with_cache.clear();
690 }
691
init(const unit_type & u_type,int side,bool real_unit,unit_race::GENDER gender)692 void unit::init(const unit_type& u_type, int side, bool real_unit, unit_race::GENDER gender)
693 {
694 type_ = &u_type;
695 race_ = &unit_race::null_race;
696 variation_ = type_->default_variation();
697 side_ = side;
698 gender_ = gender != unit_race::NUM_GENDERS ? gender : generate_gender(u_type, real_unit);
699 facing_ = static_cast<map_location::DIRECTION>(randomness::rng::default_instance().get_random_int(0, map_location::NDIRECTIONS-1));
700 upkeep_ = upkeep_full();
701
702 // Apply the unit type's data to this unit.
703 advance_to(u_type, real_unit);
704
705 if(real_unit) {
706 generate_name();
707 }
708
709 set_underlying_id(resources::gameboard ? resources::gameboard->unit_id_manager() : n_unit::id_manager::global_instance());
710
711 // Set these after traits and modifications have set the maximums.
712 movement_ = max_movement_;
713 hit_points_ = max_hit_points_;
714 attacks_left_ = max_attacks_;
715 }
716
~unit()717 unit::~unit()
718 {
719 try {
720 anim_comp_->clear_haloes();
721
722 // Remove us from the status cache
723 auto itor = std::find(units_with_cache.begin(), units_with_cache.end(), this);
724
725 if(itor != units_with_cache.end()) {
726 units_with_cache.erase(itor);
727 }
728 } catch(const std::exception & e) {
729 ERR_UT << "Caught exception when destroying unit: " << e.what() << std::endl;
730 } catch(...) {}
731 }
732
733 /**
734 * Swap, for copy and swap idiom
735 */
swap(unit & o)736 void unit::swap(unit & o)
737 {
738 using std::swap;
739
740 // Don't swap reference count, or it will be incorrect...
741 swap(loc_, o.loc_);
742 swap(advances_to_, o.advances_to_);
743 swap(type_, o.type_);
744 swap(type_name_, o.type_name_);
745 swap(race_, o.race_);
746 swap(id_, o.id_);
747 swap(name_, o.name_);
748 swap(underlying_id_, o.underlying_id_);
749 swap(undead_variation_, o.undead_variation_);
750 swap(variation_, o.variation_);
751 swap(hit_points_, o.hit_points_);
752 swap(max_hit_points_, o.max_hit_points_);
753 swap(experience_, o.experience_);
754 swap(max_experience_, o.max_experience_);
755 swap(level_, o.level_);
756 swap(recall_cost_, o.recall_cost_);
757 swap(canrecruit_, o.canrecruit_);
758 swap(recruit_list_, o.recruit_list_);
759 swap(alignment_, o.alignment_);
760 swap(flag_rgb_, o.flag_rgb_);
761 swap(image_mods_, o.image_mods_);
762 swap(unrenamable_, o.unrenamable_);
763 swap(side_, o.side_);
764 swap(gender_, o.gender_);
765 swap(formula_man_, o.formula_man_);
766 swap(movement_, o.movement_);
767 swap(max_movement_, o.max_movement_);
768 swap(vision_, o.vision_);
769 swap(jamming_, o.jamming_);
770 swap(movement_type_, o.movement_type_);
771 swap(hold_position_, o.hold_position_);
772 swap(end_turn_, o.end_turn_);
773 swap(resting_, o.resting_);
774 swap(attacks_left_, o.attacks_left_);
775 swap(max_attacks_, o.max_attacks_);
776 swap(states_, o.states_);
777 swap(known_boolean_states_, o.known_boolean_states_);
778 swap(variables_, o.variables_);
779 swap(events_, o.events_);
780 swap(filter_recall_, o.filter_recall_);
781 swap(emit_zoc_, o.emit_zoc_);
782 swap(overlays_, o.overlays_);
783 swap(role_, o.role_);
784 swap(attacks_, o.attacks_);
785 swap(facing_, o.facing_);
786 swap(trait_names_, o.trait_names_);
787 swap(trait_descriptions_, o.trait_descriptions_);
788 swap(unit_value_, o.unit_value_);
789 swap(goto_, o.goto_);
790 swap(interrupted_move_, o.interrupted_move_);
791 swap(is_fearless_, o.is_fearless_);
792 swap(is_healthy_, o.is_healthy_);
793 swap(modification_descriptions_, o.modification_descriptions_);
794 swap(anim_comp_, o.anim_comp_);
795 swap(hidden_, o.hidden_);
796 swap(modifications_, o.modifications_);
797 swap(invisibility_cache_, o.invisibility_cache_);
798 }
799
generate_name()800 void unit::generate_name()
801 {
802 if(!name_.empty() || !generate_name_) {
803 return;
804 }
805 name_ = race_->generate_name(gender_);
806 generate_name_ = false;
807 }
808
generate_traits(bool must_have_only)809 void unit::generate_traits(bool must_have_only)
810 {
811 LOG_UT << "Generating a trait for unit type " << type().log_id() << " with must_have_only " << must_have_only << std::endl;
812 const unit_type& u_type = type();
813
814 // Calculate the unit's traits
815 config::const_child_itors current_traits = modifications_.child_range("trait");
816 std::vector<const config*> candidate_traits;
817
818 for(const config& t : u_type.possible_traits()) {
819 // Skip the trait if the unit already has it.
820 const std::string& tid = t["id"];
821 bool already = false;
822 for(const config& mod : current_traits) {
823 if(mod["id"] == tid) {
824 already = true;
825 break;
826 }
827 }
828
829 if(already) {
830 continue;
831 }
832
833 // Add the trait if it is mandatory.
834 const std::string& avl = t["availability"];
835 if(avl == "musthave") {
836 modifications_.add_child("trait", t);
837 current_traits = modifications_.child_range("trait");
838 continue;
839 }
840
841 // The trait is still available, mark it as a candidate for randomizing.
842 // For leaders, only traits with availability "any" are considered.
843 if(!must_have_only && (!can_recruit() || avl == "any")) {
844 candidate_traits.push_back(&t);
845 }
846 }
847
848 if(must_have_only) return;
849
850 // Now randomly fill out to the number of traits required or until
851 // there aren't any more traits.
852 int nb_traits = current_traits.size();
853 int max_traits = u_type.num_traits();
854 for(; nb_traits < max_traits && !candidate_traits.empty(); ++nb_traits)
855 {
856 int num = randomness::generator->get_random_int(0,candidate_traits.size()-1);
857 modifications_.add_child("trait", *candidate_traits[num]);
858 candidate_traits.erase(candidate_traits.begin() + num);
859 }
860
861 // Once random traits are added, don't do it again.
862 // Such as when restoring a saved character.
863 random_traits_ = false;
864 }
865
get_traits_list() const866 std::vector<std::string> unit::get_traits_list() const
867 {
868 std::vector<std::string> res;
869
870 for(const config& mod : modifications_.child_range("trait"))
871 {
872 // Make sure to return empty id trait strings as otherwise
873 // names will not match in length (Bug #21967)
874 res.push_back(mod["id"]);
875 }
876 return res;
877 }
878
879
880 /**
881 * Advances this unit to the specified type.
882 * Experience is left unchanged.
883 * Current hit point total is left unchanged unless it would violate max HP.
884 * Assumes gender_ and variation_ are set to their correct values.
885 */
advance_to(const unit_type & u_type,bool use_traits)886 void unit::advance_to(const unit_type& u_type, bool use_traits)
887 {
888 appearance_changed_ = true;
889 // For reference, the type before this advancement.
890 const unit_type& old_type = type();
891 // Adjust the new type for gender and variation.
892 const unit_type& new_type = u_type.get_gender_unit_type(gender_).get_variation(variation_);
893
894 // Reset the scalar values first
895 trait_names_.clear();
896 trait_descriptions_.clear(),
897 is_fearless_ = false;
898 is_healthy_ = false;
899
900 // Clear modification-related caches
901 modification_descriptions_.clear();
902
903 // build unit type ready to create units. Not sure if needed.
904 new_type.get_cfg_for_units();
905
906 if(!new_type.usage().empty()) {
907 set_usage(new_type.usage());
908 }
909
910 set_image_halo(new_type.halo());
911 if(!new_type.ellipse().empty()) {
912 set_image_ellipse(new_type.ellipse());
913 }
914
915 generate_name_ &= new_type.generate_name();
916 abilities_ = new_type.abilities_cfg();
917 advancements_.clear();
918
919 for(const config& advancement : new_type.advancements()) {
920 advancements_.push_back(new config(advancement));
921 }
922
923 // If unit has specific profile, remember it and keep it after advancing
924 if(small_profile_.empty() || small_profile_ == old_type.small_profile()) {
925 small_profile_ = new_type.small_profile();
926 }
927
928 if(profile_.empty() || profile_ == old_type.big_profile()) {
929 profile_ = new_type.big_profile();
930 }
931 // NOTE: There should be no need to access old_cfg (or new_cfg) after this
932 // line. Particularly since the swap might have affected old_cfg.
933
934 advances_to_ = new_type.advances_to();
935
936 race_ = new_type.race();
937 type_ = &new_type;
938 type_name_ = new_type.type_name();
939 description_ = new_type.unit_description();
940 undead_variation_ = new_type.undead_variation();
941 max_experience_ = new_type.experience_needed(false);
942 level_ = new_type.level();
943 recall_cost_ = new_type.recall_cost();
944 alignment_ = new_type.alignment();
945 max_hit_points_ = new_type.hitpoints();
946 hp_bar_scaling_ = new_type.hp_bar_scaling();
947 xp_bar_scaling_ = new_type.xp_bar_scaling();
948 max_movement_ = new_type.movement();
949 vision_ = new_type.vision(true);
950 jamming_ = new_type.jamming();
951 movement_type_ = new_type.movement_type();
952 emit_zoc_ = new_type.has_zoc();
953 attacks_.clear();
954 std::transform(new_type.attacks().begin(), new_type.attacks().end(), std::back_inserter(attacks_), [](const attack_type& atk) {
955 return std::make_shared<attack_type>(atk);
956 });
957 unit_value_ = new_type.cost();
958
959 max_attacks_ = new_type.max_attacks();
960
961 flag_rgb_ = new_type.flag_rgb();
962
963 upkeep_ = upkeep_full();
964 parse_upkeep(new_type.get_cfg()["upkeep"]);
965
966 anim_comp_->reset_after_advance(&new_type);
967
968 if(random_traits_) {
969 generate_traits(!use_traits);
970 } else {
971 // This will add any "musthave" traits to the new unit that it doesn't already have.
972 // This covers the Dark Sorcerer advancing to Lich and gaining the "undead" trait,
973 // but random and/or optional traits are not added,
974 // and neither are inappropriate traits removed.
975 generate_traits(true);
976 }
977
978 // Apply modifications etc, refresh the unit.
979 // This needs to be after type and gender are fixed,
980 // since there can be filters on the modifications
981 // that may result in different effects after the advancement.
982 apply_modifications();
983
984 // Now that modifications are done modifying traits, check if poison should
985 // be cleared.
986 if(get_state("unpoisonable")) {
987 set_state(STATE_POISONED, false);
988 }
989
990 // Now that modifications are done modifying the maximum hit points,
991 // enforce this maximum.
992 if(hit_points_ > max_hit_points_) {
993 hit_points_ = max_hit_points_;
994 }
995
996 // In case the unit carries EventWML, apply it now
997 if(resources::game_events) {
998 resources::game_events->add_events(new_type.events(), new_type.id());
999 }
1000 }
1001
big_profile() const1002 std::string unit::big_profile() const
1003 {
1004 if(!profile_.empty() && profile_ != "unit_image") {
1005 return profile_;
1006 }
1007
1008 return absolute_image();
1009 }
1010
small_profile() const1011 std::string unit::small_profile() const
1012 {
1013 if(!small_profile_.empty() && small_profile_ != "unit_image") {
1014 return small_profile_;
1015 }
1016
1017 if(!profile_.empty() && small_profile_ != "unit_image" && profile_ != "unit_image") {
1018 return profile_;
1019 }
1020
1021 return absolute_image();
1022 }
1023
leader_crown()1024 const std::string& unit::leader_crown()
1025 {
1026 return leader_crown_path;
1027 }
1028
flag_rgb() const1029 const std::string& unit::flag_rgb() const
1030 {
1031 return flag_rgb_.empty() ? game_config::unit_rgb : flag_rgb_;
1032 }
1033
hp_color_impl(int hitpoints,int max_hitpoints)1034 static color_t hp_color_impl(int hitpoints, int max_hitpoints)
1035 {
1036 double unit_energy = 0.0;
1037 color_t energy_color {0,0,0,255};
1038
1039 if(max_hitpoints > 0) {
1040 unit_energy = static_cast<double>(hitpoints)/static_cast<double>(max_hitpoints);
1041 }
1042
1043 if(1.0 == unit_energy) {
1044 energy_color.r = 33;
1045 energy_color.g = 225;
1046 energy_color.b = 0;
1047 } else if(unit_energy > 1.0) {
1048 energy_color.r = 100;
1049 energy_color.g = 255;
1050 energy_color.b = 100;
1051 } else if(unit_energy >= 0.75) {
1052 energy_color.r = 170;
1053 energy_color.g = 255;
1054 energy_color.b = 0;
1055 } else if(unit_energy >= 0.5) {
1056 energy_color.r = 255;
1057 energy_color.g = 175;
1058 energy_color.b = 0;
1059 } else if(unit_energy >= 0.25) {
1060 energy_color.r = 255;
1061 energy_color.g = 155;
1062 energy_color.b = 0;
1063 } else {
1064 energy_color.r = 255;
1065 energy_color.g = 0;
1066 energy_color.b = 0;
1067 }
1068
1069 return energy_color;
1070 }
1071
hp_color() const1072 color_t unit::hp_color() const
1073 {
1074 return hp_color_impl(hitpoints(), max_hitpoints());
1075 }
1076
hp_color(int new_hitpoints) const1077 color_t unit::hp_color(int new_hitpoints) const
1078 {
1079 return hp_color_impl(new_hitpoints, hitpoints());
1080 }
1081
xp_color() const1082 color_t unit::xp_color() const
1083 {
1084 const color_t near_advance_color {255,255,255,255};
1085 const color_t mid_advance_color {150,255,255,255};
1086 const color_t far_advance_color {0,205,205,255};
1087 const color_t normal_color {0,160,225,255};
1088 const color_t near_amla_color {225,0,255,255};
1089 const color_t mid_amla_color {169,30,255,255};
1090 const color_t far_amla_color {139,0,237,255};
1091 const color_t amla_color {170,0,255,255};
1092
1093 const bool near_advance = static_cast<int>(experience_to_advance()) <= game_config::kill_experience;
1094 const bool mid_advance = static_cast<int>(experience_to_advance()) <= game_config::kill_experience*2;
1095 const bool far_advance = static_cast<int>(experience_to_advance()) <= game_config::kill_experience*3;
1096
1097 color_t color = normal_color;
1098 bool major_amla = false;
1099 for(const config& adv:get_modification_advances()){
1100 major_amla |= adv["major_amla"].to_bool();
1101 }
1102 if(advances_to().size() ||major_amla){
1103 if(near_advance){
1104 color=near_advance_color;
1105 } else if(mid_advance){
1106 color=mid_advance_color;
1107 } else if(far_advance){
1108 color=far_advance_color;
1109 }
1110 } else if(get_modification_advances().size()){
1111 if(near_advance){
1112 color=near_amla_color;
1113 } else if(mid_advance){
1114 color=mid_amla_color;
1115 } else if(far_advance){
1116 color=far_amla_color;
1117 } else {
1118 color=amla_color;
1119 }
1120 }
1121
1122 return(color);
1123 }
1124
set_recruits(const std::vector<std::string> & recruits)1125 void unit::set_recruits(const std::vector<std::string>& recruits)
1126 {
1127 unit_types.check_types(recruits);
1128 recruit_list_ = recruits;
1129 //TODO crab
1130 //info_.minimum_recruit_price = 0;
1131 //ai::manager::get_singleton().raise_recruit_list_changed();
1132 }
1133
advances_to_translated() const1134 const std::vector<std::string> unit::advances_to_translated() const
1135 {
1136 std::vector<std::string> result;
1137 for(const std::string& adv_type_id : advances_to_) {
1138 if(const unit_type* adv_type = unit_types.find(adv_type_id)) {
1139 result.push_back(adv_type->type_name());
1140 } else {
1141 WRN_UT << "unknown unit in advances_to list of type "
1142 << type().log_id() << ": " << adv_type_id << std::endl;
1143 }
1144 }
1145
1146 return result;
1147 }
1148
set_advances_to(const std::vector<std::string> & advances_to)1149 void unit::set_advances_to(const std::vector<std::string>& advances_to)
1150 {
1151 unit_types.check_types(advances_to);
1152 advances_to_ = advances_to;
1153 }
1154
set_movement(int moves,bool unit_action)1155 void unit::set_movement(int moves, bool unit_action)
1156 {
1157 // If this was because the unit acted, clear its "not acting" flags.
1158 if(unit_action) {
1159 end_turn_ = hold_position_ = false;
1160 }
1161
1162 movement_ = std::max<int>(0, moves);
1163 }
1164
1165 /**
1166 * Determines if @a mod_dur "matches" @a goal_dur.
1167 * If goal_dur is not empty, they match if they are equal.
1168 * If goal_dur is empty, they match if mod_dur is neither empty nor "forever".
1169 * Helper function for expire_modifications().
1170 */
mod_duration_match(const std::string & mod_dur,const std::string & goal_dur)1171 inline bool mod_duration_match(const std::string& mod_dur, const std::string& goal_dur)
1172 {
1173 if(goal_dur.empty()) {
1174 // Default is all temporary modifications.
1175 return !mod_dur.empty() && mod_dur != "forever";
1176 }
1177
1178 return mod_dur == goal_dur;
1179 }
1180
expire_modifications(const std::string & duration)1181 void unit::expire_modifications(const std::string& duration)
1182 {
1183 // If any modifications expire, then we will need to rebuild the unit.
1184 const unit_type* rebuild_from = nullptr;
1185 int hp = hit_points_;
1186 // Loop through all types of modifications.
1187 for(const auto& mod_name : ModificationTypes) {
1188 // Loop through all modifications of this type.
1189 // Looping in reverse since we may delete the current modification.
1190 for(int j = modifications_.child_count(mod_name)-1; j >= 0; --j)
1191 {
1192 const config& mod = modifications_.child(mod_name, j);
1193
1194 if(mod_duration_match(mod["duration"], duration)) {
1195 // If removing this mod means reverting the unit's type:
1196 if(const config::attribute_value* v = mod.get("prev_type")) {
1197 rebuild_from = &get_unit_type(v->str());
1198 }
1199 // Else, if we have not already specified a type to build from:
1200 else if(rebuild_from == nullptr) {
1201 rebuild_from = &type();
1202 }
1203
1204 modifications_.remove_child(mod_name, j);
1205 }
1206 }
1207 }
1208
1209 if(rebuild_from != nullptr) {
1210 anim_comp_->clear_haloes();
1211 advance_to(*rebuild_from);
1212 hit_points_ = hp;
1213 }
1214 }
1215
new_turn()1216 void unit::new_turn()
1217 {
1218 expire_modifications("turn");
1219
1220 end_turn_ = hold_position_;
1221 movement_ = total_movement();
1222 attacks_left_ = max_attacks_;
1223 set_state(STATE_UNCOVERED, false);
1224 }
1225
end_turn()1226 void unit::end_turn()
1227 {
1228 expire_modifications("turn end");
1229
1230 set_state(STATE_SLOWED,false);
1231 if((movement_ != total_movement()) && !(get_state(STATE_NOT_MOVED))) {
1232 resting_ = false;
1233 }
1234
1235 set_state(STATE_NOT_MOVED,false);
1236 // Clear interrupted move
1237 set_interrupted_move(map_location());
1238 }
1239
new_scenario()1240 void unit::new_scenario()
1241 {
1242 // Set the goto-command to be going to no-where
1243 goto_ = map_location();
1244
1245 // Expire all temporary modifications.
1246 expire_modifications("");
1247
1248 heal_fully();
1249 set_state(STATE_SLOWED, false);
1250 set_state(STATE_POISONED, false);
1251 set_state(STATE_PETRIFIED, false);
1252 set_state(STATE_GUARDIAN, false);
1253 }
1254
heal(int amount)1255 void unit::heal(int amount)
1256 {
1257 int max_hp = max_hitpoints();
1258 if(hit_points_ < max_hp) {
1259 hit_points_ += amount;
1260
1261 if(hit_points_ > max_hp) {
1262 hit_points_ = max_hp;
1263 }
1264 }
1265
1266 if(hit_points_<1) {
1267 hit_points_ = 1;
1268 }
1269 }
1270
get_states() const1271 const std::set<std::string> unit::get_states() const
1272 {
1273 std::set<std::string> all_states = states_;
1274 for(const auto& state : known_boolean_state_names_) {
1275 if(get_state(state.second)) {
1276 all_states.insert(state.first);
1277 }
1278 }
1279
1280 // Backwards compatibility for not_living. Don't remove before 1.12
1281 if(all_states.count("undrainable") && all_states.count("unpoisonable") && all_states.count("unplagueable")) {
1282 all_states.insert("not_living");
1283 }
1284
1285 return all_states;
1286 }
1287
get_state(const std::string & state) const1288 bool unit::get_state(const std::string& state) const
1289 {
1290 state_t known_boolean_state_id = get_known_boolean_state_id(state);
1291 if(known_boolean_state_id!=STATE_UNKNOWN){
1292 return get_state(known_boolean_state_id);
1293 }
1294
1295 // Backwards compatibility for not_living. Don't remove before 1.12
1296 if(state == "not_living") {
1297 return
1298 get_state("undrainable") &&
1299 get_state("unpoisonable") &&
1300 get_state("unplagueable");
1301 }
1302
1303 return states_.find(state) != states_.end();
1304 }
1305
set_state(state_t state,bool value)1306 void unit::set_state(state_t state, bool value)
1307 {
1308 known_boolean_states_[state] = value;
1309 }
1310
get_state(state_t state) const1311 bool unit::get_state(state_t state) const
1312 {
1313 return known_boolean_states_[state];
1314 }
1315
get_known_boolean_state_id(const std::string & state)1316 unit::state_t unit::get_known_boolean_state_id(const std::string& state)
1317 {
1318 auto i = known_boolean_state_names_.find(state);
1319 if(i != known_boolean_state_names_.end()) {
1320 return i->second;
1321 }
1322
1323 return STATE_UNKNOWN;
1324 }
1325
1326 std::map<std::string, unit::state_t> unit::known_boolean_state_names_ {
1327 {"slowed", STATE_SLOWED},
1328 {"poisoned", STATE_POISONED},
1329 {"petrified", STATE_PETRIFIED},
1330 {"uncovered", STATE_UNCOVERED},
1331 {"not_moved", STATE_NOT_MOVED},
1332 {"unhealable", STATE_UNHEALABLE},
1333 {"guardian", STATE_GUARDIAN},
1334 };
1335
set_state(const std::string & state,bool value)1336 void unit::set_state(const std::string& state, bool value)
1337 {
1338 appearance_changed_ = true;
1339 state_t known_boolean_state_id = get_known_boolean_state_id(state);
1340 if(known_boolean_state_id != STATE_UNKNOWN) {
1341 set_state(known_boolean_state_id, value);
1342 return;
1343 }
1344
1345 // Backwards compatibility for not_living. Don't remove before 1.12
1346 if(state == "not_living") {
1347 set_state("undrainable", value);
1348 set_state("unpoisonable", value);
1349 set_state("unplagueable", value);
1350 }
1351
1352 if(value) {
1353 states_.insert(state);
1354 } else {
1355 states_.erase(state);
1356 }
1357 }
1358
has_ability_by_id(const std::string & ability) const1359 bool unit::has_ability_by_id(const std::string& ability) const
1360 {
1361 for(const config::any_child &ab : this->abilities_.all_children_range()) {
1362 if(ab.cfg["id"] == ability) {
1363 return true;
1364 }
1365 }
1366
1367 return false;
1368 }
1369
remove_ability_by_id(const std::string & ability)1370 void unit::remove_ability_by_id(const std::string& ability)
1371 {
1372 config::all_children_iterator i = this->abilities_.ordered_begin();
1373 while (i != this->abilities_.ordered_end()) {
1374 if(i->cfg["id"] == ability) {
1375 i = this->abilities_.erase(i);
1376 } else {
1377 ++i;
1378 }
1379 }
1380 }
1381
write(config & cfg) const1382 void unit::write(config& cfg) const
1383 {
1384 config back;
1385 auto write_subtag = [&](const std::string& key, const config& child)
1386 {
1387 cfg.clear_children(key);
1388
1389 if(!child.empty()) {
1390 cfg.add_child(key, child);
1391 } else {
1392 back.add_child(key, child);
1393 }
1394 };
1395
1396 movement_type_.write(cfg);
1397 cfg["small_profile"] = small_profile_;
1398 cfg["profile"] = profile_;
1399
1400 if(description_ != type().unit_description()) {
1401 cfg["description"] = description_;
1402 }
1403
1404 if(halo_.get()) {
1405 cfg["halo"] = *halo_;
1406 }
1407
1408 if(ellipse_.get()) {
1409 cfg["ellipse"] = *ellipse_;
1410 }
1411
1412 if(usage_.get()) {
1413 cfg["usage"] = *usage_;
1414 }
1415
1416 write_upkeep(cfg["upkeep"]);
1417
1418 cfg["hitpoints"] = hit_points_;
1419 cfg["max_hitpoints"] = max_hit_points_;
1420
1421 cfg["image_icon"] = type().icon();
1422 cfg["image"] = type().image();
1423 cfg["random_traits"] = random_traits_;
1424 cfg["generate_name"] = generate_name_;
1425 cfg["experience"] = experience_;
1426 cfg["max_experience"] = max_experience_;
1427 cfg["recall_cost"] = recall_cost_;
1428
1429 cfg["side"] = side_;
1430
1431 cfg["type"] = type_id();
1432
1433 if(type_id() != type().base_id()) {
1434 cfg["parent_type"] = type().base_id();
1435 }
1436
1437 // Support for unit formulas in [ai] and unit-specific variables in [ai] [vars]
1438 formula_man_->write(cfg);
1439
1440 cfg["gender"] = gender_string(gender_);
1441 cfg["variation"] = variation_;
1442 cfg["role"] = role_;
1443
1444 config status_flags;
1445 for(const std::string& state : get_states()) {
1446 status_flags[state] = true;
1447 }
1448
1449 write_subtag("variables", variables_);
1450 write_subtag("filter_recall", filter_recall_);
1451 write_subtag("status", status_flags);
1452
1453 cfg.clear_children("events");
1454 cfg.append(events_);
1455
1456 cfg["overlays"] = utils::join(overlays_);
1457
1458 cfg["name"] = name_;
1459 cfg["id"] = id_;
1460 cfg["underlying_id"] = underlying_id_.value;
1461
1462 if(can_recruit()) {
1463 cfg["canrecruit"] = true;
1464 }
1465
1466 cfg["extra_recruit"] = utils::join(recruit_list_);
1467
1468 cfg["facing"] = map_location::write_direction(facing_);
1469
1470 cfg["goto_x"] = goto_.wml_x();
1471 cfg["goto_y"] = goto_.wml_y();
1472
1473 cfg["moves"] = movement_;
1474 cfg["max_moves"] = max_movement_;
1475 cfg["vision"] = vision_;
1476 cfg["jamming"] = jamming_;
1477
1478 cfg["resting"] = resting_;
1479
1480 cfg["advances_to"] = utils::join(advances_to_);
1481
1482 cfg["race"] = race_->id();
1483 cfg["language_name"] = type_name_;
1484 cfg["undead_variation"] = undead_variation_;
1485 cfg["level"] = level_;
1486 cfg["alignment"] = alignment_.to_string();
1487 cfg["flag_rgb"] = flag_rgb_;
1488 cfg["unrenamable"] = unrenamable_;
1489
1490 cfg["attacks_left"] = attacks_left_;
1491 cfg["max_attacks"] = max_attacks_;
1492 cfg["zoc"] = emit_zoc_;
1493
1494 cfg["hidden"] = hidden_;
1495
1496 cfg.clear_children("attack");
1497 for(attack_ptr i : attacks_) {
1498 i->write(cfg.add_child("attack"));
1499 }
1500
1501 cfg["cost"] = unit_value_;
1502
1503 write_subtag("modifications", modifications_);
1504 write_subtag("abilities", abilities_);
1505
1506 cfg.clear_children("advancement");
1507 for(const config& advancement : this->advancements_) {
1508 if(!advancement.empty()) {
1509 cfg.add_child("advancement", advancement);
1510 }
1511 }
1512 cfg.append(back);
1513 }
1514
set_facing(map_location::DIRECTION dir) const1515 void unit::set_facing(map_location::DIRECTION dir) const
1516 {
1517 if(dir != map_location::NDIRECTIONS && dir != facing_) {
1518 appearance_changed_ = true;
1519 facing_ = dir;
1520 }
1521 // Else look at yourself (not available so continue to face the same direction)
1522 }
1523
upkeep() const1524 int unit::upkeep() const
1525 {
1526 // Leaders do not incur upkeep.
1527 if(can_recruit()) {
1528 return 0;
1529 }
1530
1531 return boost::apply_visitor(upkeep_value_visitor(*this), upkeep_);
1532 }
1533
loyal() const1534 bool unit::loyal() const
1535 {
1536 return boost::get<upkeep_loyal>(&upkeep_) != nullptr;
1537 }
1538
defense_modifier(const t_translation::terrain_code & terrain) const1539 int unit::defense_modifier(const t_translation::terrain_code & terrain) const
1540 {
1541 int def = movement_type_.defense_modifier(terrain);
1542 #if 0
1543 // A [defense] ability is too costly and doesn't take into account target locations.
1544 // Left as a comment in case someone ever wonders why it isn't a good idea.
1545 unit_ability_list defense_abilities = get_abilities("defense");
1546 if(!defense_abilities.empty()) {
1547 unit_abilities::effect defense_effect(defense_abilities, def, false);
1548 def = defense_effect.get_composite_value();
1549 }
1550 #endif
1551 return def;
1552 }
1553
resistance_filter_matches(const config & cfg,bool attacker,const std::string & damage_name,int res) const1554 bool unit::resistance_filter_matches(const config& cfg, bool attacker, const std::string& damage_name, int res) const
1555 {
1556 if(!(cfg["active_on"].empty() || (attacker && cfg["active_on"] == "offense") || (!attacker && cfg["active_on"] == "defense"))) {
1557 return false;
1558 }
1559
1560 const std::string& apply_to = cfg["apply_to"];
1561 if(!apply_to.empty()) {
1562 if(damage_name != apply_to) {
1563 if(apply_to.find(',') != std::string::npos &&
1564 apply_to.find(damage_name) != std::string::npos) {
1565 const std::vector<std::string>& vals = utils::split(apply_to);
1566 if(std::find(vals.begin(),vals.end(),damage_name) == vals.end()) {
1567 return false;
1568 }
1569 } else {
1570 return false;
1571 }
1572 }
1573 }
1574
1575 if(!unit_abilities::filter_base_matches(cfg, res)) {
1576 return false;
1577 }
1578
1579 return true;
1580 }
1581
resistance_against(const std::string & damage_name,bool attacker,const map_location & loc) const1582 int unit::resistance_against(const std::string& damage_name,bool attacker,const map_location& loc) const
1583 {
1584 int res = movement_type_.resistance_against(damage_name);
1585
1586 unit_ability_list resistance_abilities = get_abilities("resistance",loc);
1587 for(unit_ability_list::iterator i = resistance_abilities.begin(); i != resistance_abilities.end();) {
1588 if(!resistance_filter_matches(*i->first, attacker, damage_name, 100-res)) {
1589 i = resistance_abilities.erase(i);
1590 } else {
1591 ++i;
1592 }
1593 }
1594
1595 if(!resistance_abilities.empty()) {
1596 unit_abilities::effect resist_effect(resistance_abilities, 100-res, false);
1597
1598 res = 100 - std::min<int>(
1599 resist_effect.get_composite_value(),
1600 resistance_abilities.highest("max_value").first
1601 );
1602 }
1603
1604 return res;
1605 }
1606
advancement_icons() const1607 std::map<std::string, std::string> unit::advancement_icons() const
1608 {
1609 std::map<std::string,std::string> temp;
1610 if(!can_advance()) {
1611 return temp;
1612 }
1613
1614 if(!advances_to_.empty()) {
1615 std::ostringstream tooltip;
1616 const std::string& image = game_config::images::level;
1617
1618 for(const std::string& s : advances_to()) {
1619 if(!s.empty()) {
1620 tooltip << s << std::endl;
1621 }
1622 }
1623
1624 temp[image] = tooltip.str();
1625 }
1626
1627 for(const config& adv : get_modification_advances()) {
1628 const std::string& image = adv["image"];
1629 if(image.empty()) {
1630 continue;
1631 }
1632
1633 std::ostringstream tooltip;
1634 tooltip << temp[image];
1635
1636 const std::string& tt = adv["description"];
1637 if(!tt.empty()) {
1638 tooltip << tt << std::endl;
1639 }
1640
1641 temp[image] = tooltip.str();
1642 }
1643
1644 return(temp);
1645 }
1646
amla_icons() const1647 std::vector<std::pair<std::string, std::string>> unit::amla_icons() const
1648 {
1649 std::vector<std::pair<std::string, std::string>> temp;
1650 std::pair<std::string, std::string> icon; // <image,tooltip>
1651
1652 for(const config& adv : get_modification_advances()) {
1653 icon.first = adv["icon"].str();
1654 icon.second = adv["description"].str();
1655
1656 for(unsigned j = 0, j_count = modification_count("advancement", adv["id"]); j < j_count; ++j) {
1657 temp.push_back(icon);
1658 }
1659 }
1660
1661 return(temp);
1662 }
1663
get_modification_advances() const1664 std::vector<config> unit::get_modification_advances() const
1665 {
1666 std::vector<config> res;
1667 for(const config& adv : modification_advancements()) {
1668 if(adv["strict_amla"].to_bool() && !advances_to_.empty()) {
1669 continue;
1670 }
1671
1672 if(modification_count("advancement", adv["id"]) >= static_cast<unsigned>(adv["max_times"].to_int(1))) {
1673 continue;
1674 }
1675
1676 std::vector<std::string> temp_require = utils::split(adv["require_amla"]);
1677 std::vector<std::string> temp_exclude = utils::split(adv["exclude_amla"]);
1678
1679 if(temp_require.empty() && temp_exclude.empty()) {
1680 res.push_back(adv);
1681 continue;
1682 }
1683
1684 std::sort(temp_require.begin(), temp_require.end());
1685 std::sort(temp_exclude.begin(), temp_exclude.end());
1686
1687 std::vector<std::string> uniq_require, uniq_exclude;
1688
1689 std::unique_copy(temp_require.begin(), temp_require.end(), std::back_inserter(uniq_require));
1690 std::unique_copy(temp_exclude.begin(), temp_exclude.end(), std::back_inserter(uniq_exclude));
1691
1692 bool exclusion_found = false;
1693 for(const std::string& s : uniq_exclude) {
1694 int max_num = std::count(temp_exclude.begin(), temp_exclude.end(), s);
1695 int mod_num = modification_count("advancement", s);
1696 if(mod_num >= max_num) {
1697 exclusion_found = true;
1698 break;
1699 }
1700 }
1701
1702 if(exclusion_found) {
1703 continue;
1704 }
1705
1706 bool requirements_done = true;
1707 for(const std::string& s : uniq_require) {
1708 int required_num = std::count(temp_require.begin(), temp_require.end(), s);
1709 int mod_num = modification_count("advancement", s);
1710 if(required_num > mod_num) {
1711 requirements_done = false;
1712 break;
1713 }
1714 }
1715
1716 if(requirements_done) {
1717 res.push_back(adv);
1718 }
1719 }
1720
1721 return res;
1722 }
1723
set_advancements(std::vector<config> advancements)1724 void unit::set_advancements(std::vector<config> advancements)
1725 {
1726 this->advancements_.clear();
1727 for(config& advancement : advancements) {
1728 this->advancements_.push_back(new config());
1729 this->advancements_.back().swap(advancement);
1730 }
1731 }
1732
modification_count(const std::string & mod_type,const std::string & id) const1733 size_t unit::modification_count(const std::string& mod_type, const std::string& id) const
1734 {
1735 size_t res = 0;
1736 for(const config& item : modifications_.child_range(mod_type)) {
1737 if(item["id"] == id) {
1738 ++res;
1739 }
1740 }
1741
1742 // For backwards compatibility, if asked for "advancement", also count "advance"
1743 if(mod_type == "advancement") {
1744 res += modification_count("advance", id);
1745 }
1746
1747 return res;
1748 }
1749
1750 const std::set<std::string> unit::builtin_effects {
1751 "alignment", "attack", "defense", "ellipse", "experience", "fearless",
1752 "halo", "healthy", "hitpoints", "image_mod", "jamming", "jamming_costs",
1753 "loyal", "max_attacks", "max_experience", "movement", "movement_costs",
1754 "new_ability", "new_advancement", "new_animation", "new_attack", "overlay", "profile",
1755 "recall_cost", "remove_ability", "remove_advancement", "remove_attacks", "resistance",
1756 "status", "type", "variation", "vision", "vision_costs", "zoc"
1757 };
1758
describe_builtin_effect(std::string apply_to,const config & effect)1759 std::string unit::describe_builtin_effect(std::string apply_to, const config& effect)
1760 {
1761 if(apply_to == "attack") {
1762 std::vector<t_string> attack_names;
1763
1764 std::string desc;
1765 for(attack_ptr a : attacks_) {
1766 bool affected = a->describe_modification(effect, &desc);
1767 if(affected && !desc.empty()) {
1768 attack_names.emplace_back(a->name(), "wesnoth-units");
1769 }
1770 }
1771 if(!attack_names.empty()) {
1772 utils::string_map symbols;
1773 symbols["attack_list"] = utils::format_conjunct_list("", attack_names);
1774 symbols["effect_description"] = desc;
1775 return VGETTEXT("$attack_list|: $effect_description", symbols);
1776 }
1777 } else if(apply_to == "hitpoints") {
1778 const std::string& increase_total = effect["increase_total"];
1779 if(!increase_total.empty()) {
1780 return VGETTEXT(
1781 "<span color=\"$color\">$number_or_percent</span> HP",
1782 {{"number_or_percent", utils::print_modifier(increase_total)}, {"color", increase_total[0] == '-' ? "red" : "green"}});
1783 }
1784 } else {
1785 const std::string& increase = effect["increase"];
1786 if(increase.empty()) {
1787 return "";
1788 }
1789 if(apply_to == "movement") {
1790 return VNGETTEXT(
1791 "<span color=\"$color\">$number_or_percent</span> move",
1792 "<span color=\"$color\">$number_or_percent</span> moves",
1793 std::stoi(increase),
1794 {{"number_or_percent", utils::print_modifier(increase)}, {"color", increase[0] == '-' ? "red" : "green"}});
1795 } else if(apply_to == "vision") {
1796 return VGETTEXT(
1797 "<span color=\"$color\">$number_or_percent</span> vision",
1798 {{"number_or_percent", utils::print_modifier(increase)}, {"color", increase[0] == '-' ? "red" : "green"}});
1799 } else if(apply_to == "jamming") {
1800 return VGETTEXT(
1801 "<span color=\"$color\">$number_or_percent</span> jamming",
1802 {{"number_or_percent", utils::print_modifier(increase)}, {"color", increase[0] == '-' ? "red" : "green"}});
1803 } else if(apply_to == "max_experience") {
1804 // Unlike others, decreasing experience is a *GOOD* thing
1805 return VGETTEXT(
1806 "<span color=\"$color\">$number_or_percent</span> XP to advance",
1807 {{"number_or_percent", utils::print_modifier(increase)}, {"color", increase[0] == '-' ? "green" : "red"}});
1808 } else if(apply_to == "max_attacks") {
1809 return VNGETTEXT(
1810 "<span color=\"$color\">$number_or_percent</span> attack per turn",
1811 "<span color=\"$color\">$number_or_percent</span> attacks per turn",
1812 std::stoi(increase),
1813 {{"number_or_percent", utils::print_modifier(increase)}, {"color", increase[0] == '-' ? "red" : "green"}});
1814 } else if(apply_to == "recall_cost") {
1815 // Unlike others, decreasing recall cost is a *GOOD* thing
1816 return VGETTEXT(
1817 "<span color=\"$color\">$number_or_percent</span> cost to recall",
1818 {{"number_or_percent", utils::print_modifier(increase)}, {"color", increase[0] == '-' ? "green" : "red"}});
1819 }
1820 }
1821 return "";
1822 }
1823
apply_builtin_effect(std::string apply_to,const config & effect)1824 void unit::apply_builtin_effect(std::string apply_to, const config& effect)
1825 {
1826 appearance_changed_ = true;
1827 if(apply_to == "fearless") {
1828 is_fearless_ = effect["set"].to_bool(true);
1829 } else if(apply_to == "healthy") {
1830 is_healthy_ = effect["set"].to_bool(true);
1831 } else if(apply_to == "profile") {
1832 if(const config::attribute_value* v = effect.get("portrait")) {
1833 std::string portrait = (*v).str();
1834 adjust_profile(portrait);
1835 profile_ = portrait;
1836 }
1837
1838 if(const config::attribute_value* v = effect.get("small_portrait")) {
1839 small_profile_ = (*v).str();
1840 }
1841
1842 if(const config::attribute_value* v = effect.get("description")) {
1843 description_ = *v;
1844 }
1845 } else if(apply_to == "new_attack") {
1846 attacks_.emplace_back(new attack_type(effect));
1847 } else if(apply_to == "remove_attacks") {
1848 auto iter = std::remove_if(attacks_.begin(), attacks_.end(), [&effect](attack_ptr a) {
1849 return a->matches_filter(effect);
1850 });
1851
1852 attacks_.erase(iter, attacks_.end());
1853 } else if(apply_to == "attack") {
1854 for(attack_ptr a : attacks_) {
1855 a->apply_modification(effect);
1856 }
1857 } else if(apply_to == "hitpoints") {
1858 LOG_UT << "applying hitpoint mod..." << hit_points_ << "/" << max_hit_points_ << std::endl;
1859 const std::string& increase_hp = effect["increase"];
1860 const std::string& increase_total = effect["increase_total"];
1861 const std::string& set_hp = effect["set"];
1862 const std::string& set_total = effect["set_total"];
1863
1864 // If the hitpoints are allowed to end up greater than max hitpoints
1865 const bool violate_max = effect["violate_maximum"].to_bool();
1866
1867 if(!set_hp.empty()) {
1868 if(set_hp.back() == '%') {
1869 hit_points_ = lexical_cast_default<int>(set_hp)*max_hit_points_/100;
1870 } else {
1871 hit_points_ = lexical_cast_default<int>(set_hp);
1872 }
1873 }
1874
1875 if(!set_total.empty()) {
1876 if(set_total.back() == '%') {
1877 max_hit_points_ = lexical_cast_default<int>(set_total)*max_hit_points_/100;
1878 } else {
1879 max_hit_points_ = lexical_cast_default<int>(set_total);
1880 }
1881 }
1882
1883 if(!increase_total.empty()) {
1884 // A percentage on the end means increase by that many percent
1885 max_hit_points_ = utils::apply_modifier(max_hit_points_, increase_total);
1886 }
1887
1888 if(max_hit_points_ < 1)
1889 max_hit_points_ = 1;
1890
1891 if(effect["heal_full"].to_bool()) {
1892 heal_fully();
1893 }
1894
1895 if(!increase_hp.empty()) {
1896 hit_points_ = utils::apply_modifier(hit_points_, increase_hp);
1897 }
1898
1899 LOG_UT << "modded to " << hit_points_ << "/" << max_hit_points_ << std::endl;
1900 if(hit_points_ > max_hit_points_ && !violate_max) {
1901 LOG_UT << "resetting hp to max" << std::endl;
1902 hit_points_ = max_hit_points_;
1903 }
1904
1905 if(hit_points_ < 1) {
1906 hit_points_ = 1;
1907 }
1908 } else if(apply_to == "movement") {
1909 const std::string& increase = effect["increase"];
1910
1911 if(!increase.empty()) {
1912 max_movement_ = utils::apply_modifier(max_movement_, increase, 1);
1913 }
1914
1915 max_movement_ = effect["set"].to_int(max_movement_);
1916
1917 if(movement_ > max_movement_) {
1918 movement_ = max_movement_;
1919 }
1920 } else if(apply_to == "vision") {
1921 const std::string& increase = effect["increase"];
1922
1923 if(!increase.empty()) {
1924 const int current_vision = vision_ < 0 ? max_movement_ : vision_;
1925 vision_ = utils::apply_modifier(current_vision, increase, 1);
1926 }
1927
1928 vision_ = effect["set"].to_int(vision_);
1929 } else if(apply_to == "jamming") {
1930 const std::string& increase = effect["increase"];
1931
1932 if(!increase.empty()) {
1933 jamming_ = utils::apply_modifier(jamming_, increase, 1);
1934 }
1935
1936 jamming_ = effect["set"].to_int(jamming_);
1937 } else if(apply_to == "experience") {
1938 const std::string& increase = effect["increase"];
1939 const std::string& set = effect["set"];
1940
1941 if(!set.empty()) {
1942 if(set.back() == '%') {
1943 experience_ = lexical_cast_default<int>(set)*max_experience_/100;
1944 } else {
1945 experience_ = lexical_cast_default<int>(set);
1946 }
1947 }
1948
1949 if(increase.empty() == false) {
1950 experience_ = utils::apply_modifier(experience_, increase, 1);
1951 }
1952 } else if(apply_to == "max_experience") {
1953 const std::string& increase = effect["increase"];
1954 const std::string& set = effect["set"];
1955
1956 if(set.empty() == false) {
1957 if(set.back() == '%') {
1958 max_experience_ = lexical_cast_default<int>(set)*max_experience_/100;
1959 } else {
1960 max_experience_ = lexical_cast_default<int>(set);
1961 }
1962 }
1963
1964 if(increase.empty() == false) {
1965 max_experience_ = utils::apply_modifier(max_experience_, increase, 1);
1966 }
1967 } else if(apply_to == upkeep_loyal::type()) {
1968 upkeep_ = upkeep_loyal();
1969 } else if(apply_to == "status") {
1970 const std::string& add = effect["add"];
1971 const std::string& remove = effect["remove"];
1972
1973 for(const std::string& to_add : utils::split(add))
1974 {
1975 set_state(to_add, true);
1976 }
1977
1978 for(const std::string& to_remove : utils::split(remove))
1979 {
1980 set_state(to_remove, false);
1981 }
1982 } else if(std::find(movetype::effects.cbegin(), movetype::effects.cend(), apply_to) != movetype::effects.cend()) {
1983 // "movement_costs", "vision_costs", "jamming_costs", "defense", "resistance"
1984 if(const config& ap = effect.child(apply_to)) {
1985 movement_type_.merge(ap, apply_to, effect["replace"].to_bool());
1986 }
1987 } else if(apply_to == "zoc") {
1988 if(const config::attribute_value* v = effect.get("value")) {
1989 emit_zoc_ = v->to_bool();
1990 }
1991 } else if(apply_to == "new_ability") {
1992 if(const config& ab_effect = effect.child("abilities")) {
1993 config to_append;
1994 for(const config::any_child &ab : ab_effect.all_children_range()) {
1995 if(!has_ability_by_id(ab.cfg["id"])) {
1996 to_append.add_child(ab.key, ab.cfg);
1997 }
1998 }
1999 this->abilities_.append(to_append);
2000 }
2001 } else if(apply_to == "remove_ability") {
2002 if(const config& ab_effect = effect.child("abilities")) {
2003 for(const config::any_child &ab : ab_effect.all_children_range()) {
2004 remove_ability_by_id(ab.cfg["id"]);
2005 }
2006 }
2007 } else if(apply_to == "image_mod") {
2008 LOG_UT << "applying image_mod" << std::endl;
2009 std::string mod = effect["replace"];
2010 if(!mod.empty()){
2011 image_mods_ = mod;
2012 }
2013 LOG_UT << "applying image_mod" << std::endl;
2014 mod = effect["add"].str();
2015 if(!mod.empty()){
2016 if(!image_mods_.empty()) {
2017 image_mods_ += '~';
2018 }
2019
2020 image_mods_ += mod;
2021 }
2022
2023 game_config::add_color_info(effect);
2024 LOG_UT << "applying image_mod" << std::endl;
2025 } else if(apply_to == "new_animation") {
2026 anim_comp_->apply_new_animation_effect(effect);
2027 } else if(apply_to == "ellipse") {
2028 set_image_ellipse(effect["ellipse"]);
2029 } else if(apply_to == "halo") {
2030 set_image_halo(effect["halo"]);
2031 } else if(apply_to == "overlay") {
2032 const std::string& add = effect["add"];
2033 const std::string& replace = effect["replace"];
2034
2035 if(!add.empty()) {
2036 std::vector<std::string> temp_overlays = utils::parenthetical_split(add, ',');
2037 std::vector<std::string>::iterator it;
2038 for(it=temp_overlays.begin();it<temp_overlays.end();++it) {
2039 overlays_.push_back(*it);
2040 }
2041 }
2042 else if(!replace.empty()) {
2043 overlays_ = utils::parenthetical_split(replace, ',');
2044 }
2045 } else if(apply_to == "new_advancement") {
2046 const std::string& types = effect["types"];
2047 const bool replace = effect["replace"].to_bool(false);
2048
2049 if(!types.empty()) {
2050 if(replace) {
2051 advances_to_ = utils::parenthetical_split(types, ',');
2052 } else {
2053 std::vector<std::string> temp_advances = utils::parenthetical_split(types, ',');
2054 std::copy(temp_advances.begin(), temp_advances.end(), std::back_inserter(advances_to_));
2055 }
2056 }
2057
2058 if(effect.has_child("advancement")) {
2059 if(replace) {
2060 advancements_.clear();
2061 }
2062
2063 config temp = effect;
2064 boost::copy(effect.child_range("advancement"), boost::make_function_output_iterator(ptr_vector_pushback(advancements_)));
2065 }
2066 } else if(apply_to == "remove_advancement") {
2067 const std::string& types = effect["types"];
2068 const std::string& amlas = effect["amlas"];
2069
2070 std::vector<std::string> temp_advances = utils::parenthetical_split(types, ',');
2071 std::vector<std::string>::iterator iter;
2072 for(const std::string& unit : temp_advances) {
2073 iter = std::find(advances_to_.begin(), advances_to_.end(), unit);
2074 if(iter != advances_to_.end()) {
2075 advances_to_.erase(iter);
2076 }
2077 }
2078
2079 temp_advances = utils::parenthetical_split(amlas, ',');
2080
2081 for(int i = advancements_.size() - 1; i >= 0; i--) {
2082 if(std::find(temp_advances.begin(), temp_advances.end(), advancements_[i]["id"]) != temp_advances.end()) {
2083 advancements_.erase(advancements_.begin() + i);
2084 }
2085 }
2086 } else if(apply_to == "alignment") {
2087 unit_type::ALIGNMENT new_align;
2088 if(new_align.parse(effect["set"])) {
2089 alignment_ = new_align;
2090 }
2091 } else if(apply_to == "max_attacks") {
2092 const std::string& increase = effect["increase"];
2093
2094 if(!increase.empty()) {
2095 max_attacks_ = utils::apply_modifier(max_attacks_, increase, 1);
2096 }
2097 } else if(apply_to == "recall_cost") {
2098 const std::string& increase = effect["increase"];
2099 const std::string& set = effect["set"];
2100 const int recall_cost = recall_cost_ < 0 ? resources::gameboard->teams().at(side_).recall_cost() : recall_cost_;
2101
2102 if(!set.empty()) {
2103 if(set.back() == '%') {
2104 recall_cost_ = lexical_cast_default<int>(set)*recall_cost/100;
2105 } else {
2106 recall_cost_ = lexical_cast_default<int>(set);
2107 }
2108 }
2109
2110 if(!increase.empty()) {
2111 recall_cost_ = utils::apply_modifier(recall_cost, increase, 1);
2112 }
2113 } else if(effect["apply_to"] == "variation") {
2114 variation_ = effect["name"].str();
2115 const unit_type* base_type = unit_types.find(type().base_id());
2116 assert(base_type != nullptr);
2117 advance_to(*base_type);
2118 } else if(effect["apply_to"] == "type") {
2119 std::string prev_type = effect["prev_type"];
2120 if(prev_type.empty()) {
2121 prev_type = type().base_id();
2122 }
2123 const std::string& new_type_id = effect["name"];
2124 const unit_type* new_type = unit_types.find(new_type_id);
2125 if(new_type) {
2126 const bool heal_full = effect["heal_full"].to_bool(false);
2127 advance_to(*new_type);
2128 preferences::encountered_units().insert(new_type_id);
2129 if(heal_full) {
2130 heal_fully();
2131 }
2132 } else {
2133 WRN_UT << "unknown type= in [effect]apply_to=type, ignoring" << std::endl;
2134 }
2135 }
2136 }
2137
add_modification(const std::string & mod_type,const config & mod,bool no_add)2138 void unit::add_modification(const std::string& mod_type, const config& mod, bool no_add)
2139 {
2140 bool generate_description = mod["generate_description"].to_bool(true);
2141
2142 if(no_add == false) {
2143 modifications_.add_child(mod_type, mod);
2144 }
2145
2146 bool set_poisoned = false; // Tracks if the poisoned state was set after the type or variation was changed.
2147 config type_effect, variation_effect;
2148 std::vector<t_string> effects_description;
2149 for(const config& effect : mod.child_range("effect")) {
2150 // Apply SUF.
2151 if(const config& afilter = effect.child("filter")) {
2152 // @FIXME: during gamestate construction resources::filter_con is not available
2153 if(resources::filter_con && !unit_filter(vconfig(afilter)).matches(*this, loc_)) {
2154 continue;
2155 }
2156 }
2157 const std::string& apply_to = effect["apply_to"];
2158 int times = effect["times"].to_int(1);
2159 t_string description;
2160
2161 if(effect["times"] == "per level") {
2162 times = level_;
2163 }
2164
2165 if(times) {
2166 while (times > 0) {
2167 times --;
2168
2169 bool was_poisoned = get_state(STATE_POISONED);
2170 // Apply unit type/variation changes last to avoid double applying effects on advance.
2171 if(apply_to == "type") {
2172 set_poisoned = false;
2173 type_effect = effect;
2174 continue;
2175 }
2176 if(apply_to == "variation") {
2177 set_poisoned = false;
2178 variation_effect = effect;
2179 continue;
2180 }
2181
2182 std::string description_component;
2183 if(resources::lua_kernel) {
2184 description_component = resources::lua_kernel->apply_effect(apply_to, *this, effect, true);
2185 } else if(builtin_effects.count(apply_to)) {
2186 // Normally, the built-in effects are dispatched through Lua so that a user
2187 // can override them if desired. However, since they're built-in, we can still
2188 // apply them if the lua kernel is unavailable.
2189 apply_builtin_effect(apply_to, effect);
2190 description_component = describe_builtin_effect(apply_to, effect);
2191 }
2192 if(!times) {
2193 description += description_component;
2194 }
2195 if(!was_poisoned && get_state(STATE_POISONED)) {
2196 set_poisoned = true;
2197 } else if(was_poisoned && !get_state(STATE_POISONED)) {
2198 set_poisoned = false;
2199 }
2200 } // end while
2201 } else { // for times = per level & level = 0 we still need to rebuild the descriptions
2202 if(resources::lua_kernel) {
2203 description += resources::lua_kernel->apply_effect(apply_to, *this, effect, false);
2204 } else if(builtin_effects.count(apply_to)) {
2205 description += describe_builtin_effect(apply_to, effect);
2206 }
2207 }
2208
2209 if(effect["times"] == "per level" && !times) {
2210 description = VGETTEXT("$effect_description per level", {{"effect_description", description}});
2211 }
2212
2213 if(!description.empty()) {
2214 effects_description.push_back(description);
2215 }
2216 }
2217 // Apply variations -- only apply if we are adding this for the first time.
2218 if((!type_effect.empty() || !variation_effect.empty()) && no_add == false) {
2219 if(!type_effect.empty()) {
2220 std::string description;
2221 if(resources::lua_kernel) {
2222 description = resources::lua_kernel->apply_effect(type_effect["apply_to"], *this, type_effect, true);
2223 } else if(builtin_effects.count(type_effect["apply_to"])) {
2224 apply_builtin_effect(type_effect["apply_to"], type_effect);
2225 description = describe_builtin_effect(type_effect["apply_to"], type_effect);
2226 }
2227 effects_description.push_back(description);
2228 }
2229 if(!variation_effect.empty()) {
2230 std::string description;
2231 if(resources::lua_kernel) {
2232 description = resources::lua_kernel->apply_effect(variation_effect["apply_to"], *this, variation_effect, true);
2233 } else if(builtin_effects.count(variation_effect["apply_to"])) {
2234 apply_builtin_effect(variation_effect["apply_to"], variation_effect);
2235 description = describe_builtin_effect(variation_effect["apply_to"], variation_effect);
2236 }
2237 effects_description.push_back(description);
2238 }
2239 if(set_poisoned)
2240 // An effect explicitly set the poisoned state, and this
2241 // should override the unit being immune to poison.
2242 set_state(STATE_POISONED, true);
2243 }
2244
2245 t_string description;
2246
2247 const t_string& mod_description = mod["description"];
2248 if(!mod_description.empty()) {
2249 description = mod_description;
2250 }
2251
2252 // Punctuation should be translatable: not all languages use Latin punctuation.
2253 // (However, there maybe is a better way to do it)
2254 if(generate_description && !effects_description.empty()) {
2255 if(!mod_description.empty()) {
2256 description += "\n";
2257 }
2258
2259 for(const auto& desc_line : effects_description) {
2260 description += desc_line + "\n";
2261 }
2262 }
2263
2264 // store trait info
2265 if(mod_type == "trait") {
2266 add_trait_description(mod, description);
2267 }
2268
2269 //NOTE: if not a trait, description is currently not used
2270 }
2271
add_trait_description(const config & trait,const t_string & description)2272 void unit::add_trait_description(const config& trait, const t_string& description)
2273 {
2274 const std::string& gender_string = gender_ == unit_race::FEMALE ? "female_name" : "male_name";
2275 const auto& gender_specific_name = trait[gender_string];
2276
2277 const t_string name = gender_specific_name.empty() ? trait["name"] : gender_specific_name;
2278
2279 if(!name.empty()) {
2280 trait_names_.push_back(name);
2281 trait_descriptions_.push_back(description);
2282 }
2283 }
2284
absolute_image() const2285 std::string unit::absolute_image() const
2286 {
2287 return type().icon().empty() ? type().image() : type().icon();
2288 }
2289
default_anim_image() const2290 std::string unit::default_anim_image() const
2291 {
2292 return type().image().empty() ? type().icon() : type().image();
2293 }
2294
apply_modifications()2295 void unit::apply_modifications()
2296 {
2297 log_scope("apply mods");
2298
2299 variables_.clear_children("mods");
2300
2301 for(const auto& mod : ModificationTypes) {
2302 if(mod == "advance" && modifications_.has_child(mod)) {
2303 deprecated_message("[advance]", DEP_LEVEL::PREEMPTIVE, {1, 15, 0}, "Use [advancement] instead.");
2304 }
2305
2306 for(const config& m : modifications_.child_range(mod)) {
2307 lg::scope_logger inner_scope_logging_object__(lg::general(), "add mod");
2308 add_modification(mod, m, true);
2309 }
2310 }
2311
2312 // Apply the experience acceleration last
2313 int exp_accel = unit_experience_accelerator::get_acceleration();
2314 max_experience_ = std::max<int>(1, (max_experience_ * exp_accel + 50)/100);
2315 }
2316
invisible(const map_location & loc,const display_context & dc,bool see_all) const2317 bool unit::invisible(const map_location& loc, const display_context& dc, bool see_all) const
2318 {
2319 if(loc != get_location()) {
2320 DBG_UT << "unit::invisible called: id = " << id() << " loc = " << loc << " get_loc = " << get_location() << std::endl;
2321 }
2322
2323 // This is a quick condition to check, and it does not depend on the
2324 // location (so might as well bypass the location-based cache).
2325 if(get_state(STATE_UNCOVERED)) {
2326 return false;
2327 }
2328
2329 // Fetch from cache
2330 /**
2331 * @todo FIXME: We use the cache only when using the default see_all=true
2332 * Maybe add a second cache if the see_all=false become more frequent.
2333 */
2334 if(see_all) {
2335 const auto itor = invisibility_cache_.find(loc);
2336 if(itor != invisibility_cache_.end()) {
2337 return itor->second;
2338 }
2339 }
2340
2341 // Test hidden status
2342 static const std::string hides("hides");
2343 bool is_inv = get_ability_bool(hides, loc, dc);
2344 if(is_inv){
2345 is_inv = (resources::gameboard ? !resources::gameboard->would_be_discovered(loc, side_,see_all) : true);
2346 }
2347
2348 if(see_all) {
2349 // Add to caches
2350 if(invisibility_cache_.empty()) {
2351 units_with_cache.push_back(this);
2352 }
2353
2354 invisibility_cache_[loc] = is_inv;
2355 }
2356
2357 return is_inv;
2358 }
2359
is_visible_to_team(const team & team,const display_context & dc,bool const see_all) const2360 bool unit::is_visible_to_team(const team& team,const display_context& dc, bool const see_all) const
2361 {
2362 const map_location& loc = get_location();
2363 return is_visible_to_team(loc, team, dc, see_all);
2364 }
2365
is_visible_to_team(const map_location & loc,const team & team,const display_context & dc,bool const see_all) const2366 bool unit::is_visible_to_team(const map_location& loc, const team& team, const display_context& dc, bool const see_all) const
2367 {
2368 if(!dc.map().on_board(loc)) {
2369 return false;
2370 }
2371
2372 if(see_all) {
2373 return true;
2374 }
2375
2376 if(team.is_enemy(side()) && invisible(loc, dc)) {
2377 return false;
2378 }
2379
2380 // allied planned moves are also visible under fog. (we assume that fake units on the map are always whiteboard markers)
2381 if(!team.is_enemy(side()) && underlying_id_.is_fake()) {
2382 return true;
2383 }
2384
2385 // when the whiteboard planned unit map is applied, it uses moved the _real_ unit so
2386 // underlying_id_.is_fake() will be false and the check above will not apply.
2387 // TODO: improve this check so that is also works for allied planned units but without
2388 // breaking sp campaigns with allies under fog. We probably need an explicit flag
2389 // is_planned_ in unit that is set by the whiteboard.
2390 if(team.side() == side()) {
2391 return true;
2392 }
2393
2394 if(team.fogged(loc)) {
2395 return false;
2396 }
2397
2398 return true;
2399 }
2400
set_underlying_id(n_unit::id_manager & id_manager)2401 void unit::set_underlying_id(n_unit::id_manager& id_manager)
2402 {
2403 if(underlying_id_.value == 0) {
2404 if(synced_context::is_synced() || !resources::gamedata || resources::gamedata->phase() == game_data::INITIAL) {
2405 underlying_id_ = id_manager.next_id();
2406 } else {
2407 underlying_id_ = id_manager.next_fake_id();
2408 }
2409 }
2410
2411 if(id_.empty() /*&& !underlying_id_.is_fake()*/) {
2412 std::stringstream ss;
2413 ss << (type_id().empty() ? "Unit" : type_id()) << "-" << underlying_id_.value;
2414 id_ = ss.str();
2415 }
2416 }
2417
mark_clone(bool is_temporary)2418 unit& unit::mark_clone(bool is_temporary)
2419 {
2420 n_unit::id_manager& ids = resources::gameboard ? resources::gameboard->unit_id_manager() : n_unit::id_manager::global_instance();
2421 if(is_temporary) {
2422 underlying_id_ = ids.next_fake_id();
2423 } else {
2424 if(synced_context::is_synced() || !resources::gamedata || resources::gamedata->phase() == game_data::INITIAL) {
2425 underlying_id_ = ids.next_id();
2426 }
2427 else {
2428 underlying_id_ = ids.next_fake_id();
2429 }
2430 std::string::size_type pos = id_.find_last_of('-');
2431 if(pos != std::string::npos && pos+1 < id_.size()
2432 && id_.find_first_not_of("0123456789", pos+1) == std::string::npos) {
2433 // this appears to be a duplicate of a generic unit, so give it a new id
2434 WRN_UT << "assigning new id to clone of generic unit " << id_ << std::endl;
2435 id_.clear();
2436 set_underlying_id(ids);
2437 }
2438 }
2439 return *this;
2440 }
2441
2442
unit_movement_resetter(const unit & u,bool operate)2443 unit_movement_resetter::unit_movement_resetter(const unit &u, bool operate)
2444 : u_(const_cast<unit&>(u))
2445 , moves_(u.movement_left(true))
2446 {
2447 if(operate) {
2448 u_.set_movement(u_.total_movement());
2449 }
2450 }
2451
~unit_movement_resetter()2452 unit_movement_resetter::~unit_movement_resetter()
2453 {
2454 assert(resources::gameboard);
2455 try {
2456 if(!resources::gameboard->units().has_unit(&u_)) {
2457 /*
2458 * It might be valid that the unit is not in the unit map.
2459 * It might also mean a no longer valid unit will be assigned to.
2460 */
2461 DBG_UT << "The unit to be removed is not in the unit map." << std::endl;
2462 }
2463
2464 u_.set_movement(moves_);
2465 } catch(...) {}
2466 }
2467
TC_image_mods() const2468 std::string unit::TC_image_mods() const
2469 {
2470 return formatter() << "~RC(" << flag_rgb() << ">" << team::get_side_color_id(side()) << ")";
2471 }
2472
image_mods() const2473 std::string unit::image_mods() const
2474 {
2475 if(!image_mods_.empty()) {
2476 return formatter() << "~" << image_mods_ << TC_image_mods();
2477 }
2478
2479 return TC_image_mods();
2480 }
2481
2482 // Called by the Lua API after resetting an attack pointer.
remove_attack(attack_ptr atk)2483 bool unit::remove_attack(attack_ptr atk)
2484 {
2485 auto iter = std::find(attacks_.begin(), attacks_.end(), atk);
2486 if(iter == attacks_.end()) {
2487 return false;
2488 }
2489 attacks_.erase(iter);
2490 return true;
2491 }
2492
remove_attacks_ai()2493 void unit::remove_attacks_ai()
2494 {
2495 if(attacks_left_ == max_attacks_) {
2496 //TODO: add state_not_attacked
2497 }
2498
2499 set_attacks(0);
2500 }
2501
remove_movement_ai()2502 void unit::remove_movement_ai()
2503 {
2504 if(movement_left() == total_movement()) {
2505 set_state(STATE_NOT_MOVED,true);
2506 }
2507
2508 set_movement(0, true);
2509 }
2510
set_hidden(bool state) const2511 void unit::set_hidden(bool state) const
2512 {
2513 // appearance_changed_ = true;
2514 hidden_ = state;
2515 if(!state) {
2516 return;
2517 }
2518
2519 // We need to get rid of haloes immediately to avoid display glitches
2520 anim_comp_->clear_haloes();
2521 }
2522
set_image_halo(const std::string & halo)2523 void unit::set_image_halo(const std::string& halo)
2524 {
2525 appearance_changed_ = true;
2526 anim_comp_->clear_haloes();
2527 halo_.reset(new std::string(halo));
2528 }
2529
parse_upkeep(const config::attribute_value & upkeep)2530 void unit::parse_upkeep(const config::attribute_value& upkeep)
2531 {
2532 if(upkeep.empty()) {
2533 return;
2534 }
2535
2536 // TODO: create abetter way to check whether it is actually an int.
2537 int upkeep_int = upkeep.to_int(-99);
2538 if(upkeep_int != -99) {
2539 upkeep_ = upkeep_int;
2540 } else if(upkeep == upkeep_loyal::type() || upkeep == "free") {
2541 upkeep_ = upkeep_loyal();
2542 } else if(upkeep == upkeep_full::type()) {
2543 upkeep_ = upkeep_full();
2544 } else {
2545 WRN_UT << "Found invalid upkeep=\"" << upkeep << "\" in a unit" << std::endl;
2546 upkeep_ = upkeep_full();
2547 }
2548 }
2549
write_upkeep(config::attribute_value & upkeep) const2550 void unit::write_upkeep(config::attribute_value& upkeep) const
2551 {
2552 upkeep = boost::apply_visitor(upkeep_type_visitor(), upkeep_);
2553 }
2554
2555 // Filters unimportant stats from the unit config and returns a checksum of
2556 // the remaining config.
get_checksum(const unit & u)2557 std::string get_checksum(const unit& u)
2558 {
2559 config unit_config;
2560 config wcfg;
2561 u.write(unit_config);
2562
2563 const std::string main_keys[] {
2564 "advances_to",
2565 "alignment",
2566 "cost",
2567 "experience",
2568 "gender",
2569 "hitpoints",
2570 "ignore_race_traits",
2571 "ignore_global_traits",
2572 "level",
2573 "recall_cost",
2574 "max_attacks",
2575 "max_experience",
2576 "max_hitpoints",
2577 "max_moves",
2578 "movement",
2579 "movement_type",
2580 "race",
2581 "random_traits",
2582 "resting",
2583 "undead_variation",
2584 "upkeep",
2585 "zoc",
2586 ""
2587 };
2588
2589 for(int i = 0; !main_keys[i].empty(); ++i) {
2590 wcfg[main_keys[i]] = unit_config[main_keys[i]];
2591 }
2592
2593 const std::string attack_keys[] {
2594 "name",
2595 "type",
2596 "range",
2597 "damage",
2598 "number",
2599 ""
2600 };
2601
2602 for(const config& att : unit_config.child_range("attack")) {
2603 config& child = wcfg.add_child("attack");
2604
2605 for(int i = 0; !attack_keys[i].empty(); ++i) {
2606 child[attack_keys[i]] = att[attack_keys[i]];
2607 }
2608
2609 for(const config& spec : att.child_range("specials")) {
2610 config& child_spec = child.add_child("specials", spec);
2611
2612 child_spec.recursive_clear_value("description");
2613 }
2614 }
2615
2616 for(const config& abi : unit_config.child_range("abilities")) {
2617 config& child = wcfg.add_child("abilities", abi);
2618
2619 child.recursive_clear_value("description");
2620 child.recursive_clear_value("description_inactive");
2621 child.recursive_clear_value("name");
2622 child.recursive_clear_value("name_inactive");
2623 }
2624
2625 for(const config& trait : unit_config.child_range("trait")) {
2626 config& child = wcfg.add_child("trait", trait);
2627
2628 child.recursive_clear_value("description");
2629 child.recursive_clear_value("male_name");
2630 child.recursive_clear_value("female_name");
2631 child.recursive_clear_value("name");
2632 }
2633
2634 const std::string child_keys[] {
2635 "advance_from",
2636 "defense",
2637 "movement_costs",
2638 "vision_costs",
2639 "jamming_costs",
2640 "resistance",
2641 ""
2642 };
2643
2644 for(int i = 0; !child_keys[i].empty(); ++i) {
2645 for(const config& c : unit_config.child_range(child_keys[i])) {
2646 wcfg.add_child(child_keys[i], c);
2647 }
2648 }
2649
2650 DBG_UT << wcfg;
2651
2652 return wcfg.hash();
2653 }
2654
swap(unit & lhs,unit & rhs)2655 void swap(unit& lhs, unit& rhs)
2656 {
2657 lhs.swap(rhs);
2658 }
2659