The Battle for Wesnoth  1.19.18+dev
attack_type.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2025
3  by David White <dave@whitevine.net>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 /**
17  * @file
18  * Handle unit-type specific attributes, animations, advancement.
19  */
20 
21 #include "units/attack_type.hpp"
22 #include "units/types.hpp"
23 #include "units/unit.hpp"
25 #include "formula/formula.hpp"
26 #include "formula/string_utils.hpp"
28 #include "deprecation.hpp"
29 #include "game_version.hpp"
30 
31 #include "log.hpp"
33 #include "serialization/markup.hpp"
34 #include "gettext.hpp"
35 #include "utils/math.hpp"
36 
37 
38 static lg::log_domain log_config("config");
39 #define ERR_CF LOG_STREAM(err, log_config)
40 #define WRN_CF LOG_STREAM(warn, log_config)
41 #define LOG_CONFIG LOG_STREAM(info, log_config)
42 #define DBG_CF LOG_STREAM(debug, log_config)
43 
44 static lg::log_domain log_unit("unit");
45 #define DBG_UT LOG_STREAM(debug, log_unit)
46 #define ERR_UT LOG_STREAM(err, log_unit)
47 
48 static lg::log_domain log_wml("wml");
49 #define ERR_WML LOG_STREAM(err, log_wml)
50 
51 static lg::log_domain log_engine("engine");
52 #define ERR_NG LOG_STREAM(err, log_engine)
53 
54 
56  : context_()
57  , description_(cfg["description"].t_str())
58  , id_(cfg["name"])
59  , type_(cfg["type"])
60  , icon_(cfg["icon"])
61  , range_(cfg["range"])
62  , min_range_(cfg["min_range"].to_int(1))
63  , max_range_(cfg["max_range"].to_int(1))
64  , alignment_(unit_alignments::get_enum(cfg["alignment"].str()))
65  , damage_(cfg["damage"].to_int())
66  , num_attacks_(cfg["number"].to_int())
67  , attack_weight_(cfg["attack_weight"].to_double(1.0))
68  , defense_weight_(cfg["defense_weight"].to_double(1.0))
69  , accuracy_(cfg["accuracy"].to_int())
70  , movement_used_(cfg["movement_used"].to_int(100000))
71  , attacks_used_(cfg["attacks_used"].to_int(1))
72  , parry_(cfg["parry"].to_int())
73  , specials_()
74  , changed_(true)
75 {
78 
79  if (description_.empty())
81 
82  if(icon_.empty()){
83  if (!id_.empty())
84  icon_ = "attacks/" + id_ + ".png";
85  else
86  icon_ = "attacks/blank-attack.png";
87  }
88 }
89 
91 {
92  if(accuracy_ == 0 && parry_ == 0) {
93  return "";
94  }
95 
96  std::ostringstream s;
98 
99  if(parry_ != 0) {
100  s << "/" << utils::signed_percent(parry_);
101  }
102 
103  return s.str();
104 }
105 
107 {
108  if(accuracy_ == 0 && parry_ == 0) {
109  return "";
110  }
111 
112  std::stringstream tooltip;
113  if (accuracy_) {
114  tooltip << _("Accuracy:") << " " << markup::bold(utils::signed_percent(accuracy_)) << "\n";
115  }
116  if (parry_) {
117  tooltip << _("Parry:") << " " << markup::bold(utils::signed_percent(parry_));
118  }
119 
120  return tooltip.str();
121 }
122 
123 namespace
124 {
125  bool special_checking(const std::string& special_id, const std::string& tag_name, const std::set<std::string>& filter_special, const std::set<std::string>& filter_special_id, const std::set<std::string>& filter_special_type)
126  {
127  if (!filter_special.empty() && filter_special.count(special_id) == 0 && filter_special.count(tag_name) == 0)
128  return false;
129 
130  if (!filter_special_id.empty() && filter_special_id.count(special_id) == 0)
131  return false;
132 
133  if (!filter_special_type.empty() && filter_special_type.count(tag_name) == 0)
134  return false;
135 
136  return true;
137  }
138 }
139 
140 
142 {
143  if (range().empty()) {
144  return false;
145  }
146  const std::set<std::string> filter_special = utils::split_set(filter["special"].str());
147  const std::set<std::string> filter_special_id = utils::split_set(filter["special_id"].str());
148  const std::set<std::string> filter_special_type = utils::split_set(filter["special_type"].str());
149  for (const auto& p_ab : specials()) {
150  if (special_checking(p_ab->id(), p_ab->tag(), filter_special, filter_special_id, filter_special_type)) {
151  return true;
152  }
153  }
154  return false;
155 }
156 
157 /**
158  * Returns whether or not *this matches the given @a filter, ignoring the
159  * complexities introduced by [and], [or], and [not].
160  */
161 static bool matches_simple_filter(const attack_type & attack, const config & filter, const std::string& check_if_recursion)
162 {
163  const std::set<std::string> filter_range = utils::split_set(filter["range"].str());
164  const std::string& filter_min_range = filter["min_range"];
165  const std::string& filter_max_range = filter["max_range"];
166  const std::string& filter_damage = filter["damage"];
167  const std::string& filter_attacks = filter["number"];
168  const std::string& filter_accuracy = filter["accuracy"];
169  const std::string& filter_parry = filter["parry"];
170  const std::string& filter_movement = filter["movement_used"];
171  const std::string& filter_attacks_used = filter["attacks_used"];
172  const std::set<std::string> filter_alignment = utils::split_set(filter["alignment"].str());
173  const std::set<std::string> filter_name = utils::split_set(filter["name"].str());
174  const std::set<std::string> filter_type = utils::split_set(filter["type"].str());
175  const std::set<std::string> filter_base_type = utils::split_set(filter["base_type"].str());
176  const std::vector<std::string> filter_special_active = utils::split(filter["special_active"]);
177  const std::vector<std::string> filter_special_id_active = utils::split(filter["special_id_active"]);
178  const std::vector<std::string> filter_special_type_active = utils::split(filter["special_type_active"]);
179  const std::string filter_formula = filter["formula"];
180 
181  if (!filter_min_range.empty() && !in_ranges(attack.min_range(), utils::parse_ranges_int(filter_min_range)))
182  return false;
183 
184  if (!filter_max_range.empty() && !in_ranges(attack.max_range(), utils::parse_ranges_int(filter_max_range)))
185  return false;
186 
187  if ( !filter_range.empty() && filter_range.count(attack.range()) == 0 )
188  return false;
189 
190  if ( !filter_damage.empty() && !in_ranges(attack.damage(), utils::parse_ranges_unsigned(filter_damage)) )
191  return false;
192 
193  if (!filter_attacks.empty() && !in_ranges(attack.num_attacks(), utils::parse_ranges_unsigned(filter_attacks)))
194  return false;
195 
196  if (!filter_accuracy.empty() && !in_ranges(attack.accuracy(), utils::parse_ranges_int(filter_accuracy)))
197  return false;
198 
199  if (!filter_parry.empty() && !in_ranges(attack.parry(), utils::parse_ranges_int(filter_parry)))
200  return false;
201 
202  if (!filter_movement.empty() && !in_ranges(attack.movement_used(), utils::parse_ranges_unsigned(filter_movement)))
203  return false;
204 
205  if (!filter_attacks_used.empty() && !in_ranges(attack.attacks_used(), utils::parse_ranges_unsigned(filter_attacks_used)))
206  return false;
207 
208  if(!filter_alignment.empty() && filter_alignment.count(attack.alignment_str()) == 0)
209  return false;
210 
211  if ( !filter_name.empty() && filter_name.count(attack.id()) == 0)
212  return false;
213 
214  if (!filter_type.empty()){
215  // Although there's a general guard against infinite recursion, the "damage_type" special
216  // should always use the base type of the weapon. Otherwise it will flip-flop between the
217  // special being active or inactive based on whether ATTACK_RECURSION_LIMIT is even or odd;
218  // without this it will also behave differently when calculating resistance_against.
219  if(check_if_recursion == "damage_type"){
220  if (filter_type.count(attack.type()) == 0){
221  return false;
222  }
223  } else {
224  //if the type is different from "damage_type" then damage_type() can be called for safe checking.
225  if (filter_type.count(attack.effective_damage_type().first) == 0){
226  return false;
227  }
228  }
229  }
230 
231  if ( !filter_base_type.empty() && filter_base_type.count(attack.type()) == 0 )
232  return false;
233 
234  if(filter.has_attribute("special")) {
235  deprecated_message("special=", DEP_LEVEL::PREEMPTIVE, {1, 17, 0}, "Please use special_id or special_type instead");
236  }
237 
238  if(filter.has_attribute("special") || filter.has_attribute("special_id") || filter.has_attribute("special_type")) {
240  return false;
241  }
242  }
243 
244  if(filter.has_attribute("special_active")) {
245  deprecated_message("special_active=", DEP_LEVEL::PREEMPTIVE, {1, 17, 0}, "Please use special_id_active or special_type_active instead");
246  }
247 
248  if (!filter_special_type_active.empty()) {
249  if (!utils::find_if(filter_special_type_active, [&](const std::string& special_tag) { return attack.has_special_or_ability(special_tag); })) {
250  return false;
251  }
252  }
253 
254  if (!filter_special_id_active.empty()) {
255  if (!utils::find_if(filter_special_id_active, [&](const std::string& special_id) { return attack.has_active_special_or_ability_id(special_id); })) {
256  return false;
257  }
258  }
259 
260  if (!filter_special_active.empty()) {
261  auto pred = [&](const std::string& special) {
262  //This is not he fastest implementation but this is just a compatibiltiy path anyways.
263  return attack.has_active_special_or_ability_id(special) || attack.has_special_or_ability(special);
264  };
265  if (!utils::find_if(filter_special_active, pred)) {
266  return false;
267  }
268  }
269 
270  //children filter_special are checked later,
271  //but only when the function doesn't return earlier
272  if(auto sub_filter_special = filter.optional_child("filter_special")) {
273  if(!attack.has_special_or_ability_with_filter(*sub_filter_special)) {
274  return false;
275  }
276  }
277 
278  if (!filter_formula.empty()) {
279  try {
280  const wfl::attack_type_callable callable(attack);
282  const wfl::formula form(filter_formula, &symbols);
283  if(!form.evaluate(callable).as_bool()) {
284  return false;
285  }
286  } catch(const wfl::formula_error& e) {
287  lg::log_to_chat() << "Formula error in weapon filter: " << e.type << " at " << e.filename << ':' << e.line << ")\n";
288  ERR_WML << "Formula error in weapon filter: " << e.type << " at " << e.filename << ':' << e.line << ")";
289  // Formulae with syntax errors match nothing
290  return false;
291  }
292  }
293 
294  // Passed all tests.
295  return true;
296 }
297 
298 /**
299  * Returns whether or not *this matches the given @a filter.
300  */
301 bool attack_type::matches_filter(const config& filter, const std::string& check_if_recursion) const
302 {
303  // Handle the basic filter.
304  bool matches = matches_simple_filter(*this, filter, check_if_recursion);
305 
306  // Handle [and], [or], and [not] with in-order precedence
307  for(const auto [key, condition_cfg] : filter.all_children_view() )
308  {
309  // Handle [and]
310  if ( key == "and" )
311  matches = matches && matches_filter(condition_cfg, check_if_recursion);
312 
313  // Handle [or]
314  else if ( key == "or" )
315  matches = matches || matches_filter(condition_cfg, check_if_recursion);
316 
317  // Handle [not]
318  else if ( key == "not" )
319  matches = matches && !matches_filter(condition_cfg, check_if_recursion);
320  }
321 
322  return matches;
323 }
324 
326 {
327  auto i = specials_.begin();
328  while (i != specials_.end()) {
329  if((**i).matches_filter(filter)) {
330  i = specials_.erase(i);
331  } else {
332  ++i;
333  }
334  }
335 }
336 
338 {
339  set_changed(true);
340  const config::attribute_value& set_name = cfg["set_name"];
341  const t_string& set_desc = cfg["set_description"].t_str();
342  const config::attribute_value& set_type = cfg["set_type"];
343  const config::attribute_value& set_range = cfg["set_range"];
344  const config::attribute_value& set_attack_alignment = cfg["set_alignment"];
345  const config::attribute_value& set_icon = cfg["set_icon"];
346  const config::attribute_value& del_specials = cfg["remove_specials"];
347  auto set_specials = cfg.optional_child("set_specials");
348  const config::attribute_value& increase_min_range = cfg["increase_min_range"];
349  const config::attribute_value& set_min_range = cfg["set_min_range"];
350  const config::attribute_value& increase_max_range = cfg["increase_max_range"];
351  const config::attribute_value& set_max_range = cfg["set_max_range"];
352  auto remove_specials = cfg.optional_child("remove_specials");
353  const config::attribute_value& increase_damage = cfg["increase_damage"];
354  const config::attribute_value& set_damage = cfg["set_damage"];
355  const config::attribute_value& increase_attacks = cfg["increase_attacks"];
356  const config::attribute_value& set_attacks = cfg["set_attacks"];
357  const config::attribute_value& set_attack_weight = cfg["attack_weight"];
358  const config::attribute_value& set_defense_weight = cfg["defense_weight"];
359  const config::attribute_value& increase_accuracy = cfg["increase_accuracy"];
360  const config::attribute_value& set_accuracy = cfg["set_accuracy"];
361  const config::attribute_value& increase_parry = cfg["increase_parry"];
362  const config::attribute_value& set_parry = cfg["set_parry"];
363  const config::attribute_value& increase_movement = cfg["increase_movement_used"];
364  const config::attribute_value& set_movement = cfg["set_movement_used"];
365  const config::attribute_value& increase_attacks_used = cfg["increase_attacks_used"];
366  const config::attribute_value& set_attacks_used = cfg["set_attacks_used"];
367  // NB: If you add something here that requires a description,
368  // it needs to be added to describe_effect as well.
369 
370  if(set_name.empty() == false) {
371  id_ = set_name;
372  }
373 
374  if(set_desc.empty() == false) {
375  description_ = set_desc;
376  }
377 
378  if(set_type.empty() == false) {
379  type_ = set_type;
380  }
381 
382  if(set_range.empty() == false) {
383  range_ = set_range;
384  }
385 
386  if(set_attack_alignment.empty() == false) {
388  }
389 
390  if(set_icon.empty() == false) {
391  icon_ = set_icon;
392  }
393 
394  if(del_specials.empty() == false) {
395  const std::vector<std::string>& dsl = utils::split(del_specials);
396  ability_vector new_specials;
397  for(ability_ptr& p_ab : specials_) {
398  if(!utils::contains(dsl, p_ab->id())) {
399  new_specials.emplace_back(std::move(p_ab));
400  }
401  }
402  specials_ = new_specials;
403  }
404 
405  if(set_specials) {
406  const std::string &mode = set_specials["mode"];
407  if(mode.empty()){
408  deprecated_message("[set_specials]mode=<unset>", DEP_LEVEL::INDEFINITE, "",
409  "The mode defaults to 'replace', but should often be 'append' instead. The default may change in a future version, or the attribute may become mandatory.");
410  // fall through to mode != "append"
411  }
412  if(mode != "append") {
413  specials_.clear();
414  }
415  // expand and add registry weapon specials
416  config registry_specials = unit_type_data::add_registry_entries(
417  config{"specials_list", set_specials["specials_list"]}, "specials", unit_types.specials());
418  for(const auto [key, cfg] : registry_specials.all_children_view()) {
419  specials_.push_back(unit_ability_t::create(key, cfg, true));
420  }
421 
422  for(const auto [key, cfg] : set_specials->all_children_view()) {
423  specials_.push_back(unit_ability_t::create(key, cfg, true));
424  }
425  }
426 
427  if(set_min_range.empty() == false) {
428  min_range_ = set_min_range.to_int();
429  }
430 
431  if(increase_min_range.empty() == false) {
432  min_range_ = utils::apply_modifier(min_range_, increase_min_range);
433  }
434 
435  if(set_max_range.empty() == false) {
436  max_range_ = set_max_range.to_int();
437  }
438 
439  if(increase_max_range.empty() == false) {
440  max_range_ = utils::apply_modifier(max_range_, increase_max_range);
441  }
442 
443  if(remove_specials) {
444  remove_special_by_filter(*remove_specials);
445  }
446 
447  if(set_damage.empty() == false) {
448  damage_ = set_damage.to_int();
449  if (damage_ < 0) {
450  damage_ = 0;
451  }
452  }
453 
454  if(increase_damage.empty() == false) {
455  damage_ = utils::apply_modifier(damage_, increase_damage);
456  if(damage_ < 0) {
457  damage_ = 0;
458  }
459  }
460 
461  if(set_attacks.empty() == false) {
462  num_attacks_ = set_attacks.to_int();
463  if (num_attacks_ < 0) {
464  num_attacks_ = 0;
465  }
466 
467  }
468 
469  if(increase_attacks.empty() == false) {
470  num_attacks_ = utils::apply_modifier(num_attacks_, increase_attacks, 1);
471  }
472 
473  if(set_accuracy.empty() == false) {
474  accuracy_ = set_accuracy.to_int();
475  }
476 
477  if(increase_accuracy.empty() == false) {
478  accuracy_ = utils::apply_modifier(accuracy_, increase_accuracy);
479  }
480 
481  if(set_parry.empty() == false) {
482  parry_ = set_parry.to_int();
483  }
484 
485  if(increase_parry.empty() == false) {
486  parry_ = utils::apply_modifier(parry_, increase_parry);
487  }
488 
489  if(set_movement.empty() == false) {
490  movement_used_ = set_movement.to_int();
491  }
492 
493  if(increase_movement.empty() == false) {
494  movement_used_ = utils::apply_modifier(movement_used_, increase_movement, 1);
495  }
496 
497  if(set_attacks_used.empty() == false) {
498  attacks_used_ = set_attacks_used.to_int();
499  }
500 
501  if(increase_attacks_used.empty() == false) {
502  attacks_used_ = utils::apply_modifier(attacks_used_, increase_attacks_used, 1);
503  }
504 
505  if(set_attack_weight.empty() == false) {
506  attack_weight_ = set_attack_weight.to_double(1.0);
507  }
508 
509  if(set_defense_weight.empty() == false) {
510  defense_weight_ = set_defense_weight.to_double(1.0);
511  }
512 }
513 
515 {
516  const config::attribute_value& increase_min_range = cfg["increase_min_range"];
517  const config::attribute_value& set_min_range = cfg["set_min_range"];
518  const config::attribute_value& increase_max_range = cfg["increase_max_range"];
519  const config::attribute_value& set_max_range = cfg["set_max_range"];
520  const config::attribute_value& increase_damage = cfg["increase_damage"];
521  const config::attribute_value& set_damage = cfg["set_damage"];
522  const config::attribute_value& increase_attacks = cfg["increase_attacks"];
523  const config::attribute_value& set_attacks = cfg["set_attacks"];
524  const config::attribute_value& increase_accuracy = cfg["increase_accuracy"];
525  const config::attribute_value& set_accuracy = cfg["set_accuracy"];
526  const config::attribute_value& increase_parry = cfg["increase_parry"];
527  const config::attribute_value& set_parry = cfg["set_parry"];
528  const config::attribute_value& increase_movement = cfg["increase_movement_used"];
529  const config::attribute_value& set_movement = cfg["set_movement_used"];
530  const config::attribute_value& increase_attacks_used = cfg["increase_attacks_used"];
531  const config::attribute_value& set_attacks_used = cfg["set_attacks_used"];
532 
533  const auto format_modifier = [](const config::attribute_value& attr) -> utils::string_map {
534  return {{"number_or_percent", utils::print_modifier(attr)}, {"color", attr.to_int() < 0 ? "#f00" : "#0f0"}};
535  };
536 
537  std::vector<t_string> desc;
538 
539  if(!set_min_range.empty()) {
540  desc.emplace_back(VGETTEXT(
541  // TRANSLATORS: Current value for WML code set_min_range, documented in https://wiki.wesnoth.org/EffectWML
542  "$number min range",
543  {{"number", set_min_range.str()}}));
544  }
545 
546  if(!increase_min_range.empty()) {
547  desc.emplace_back(VGETTEXT(
548  // TRANSLATORS: Current value for WML code increase_min_range, documented in https://wiki.wesnoth.org/EffectWML
549  "<span color=\"$color\">$number_or_percent</span> min range",
550  format_modifier(increase_min_range)));
551  }
552 
553  if(!set_max_range.empty()) {
554  desc.emplace_back(VGETTEXT(
555  // TRANSLATORS: Current value for WML code set_max_range, documented in https://wiki.wesnoth.org/EffectWML
556  "$number max range",
557  {{"number", set_max_range.str()}}));
558  }
559 
560  if(!increase_max_range.empty()) {
561  desc.emplace_back(VGETTEXT(
562  // TRANSLATORS: Current value for WML code increase_max_range, documented in https://wiki.wesnoth.org/EffectWML
563  "<span color=\"$color\">$number_or_percent</span> max range",
564  format_modifier(increase_max_range)));
565  }
566 
567  if(!increase_damage.empty()) {
568  desc.emplace_back(VNGETTEXT(
569  // TRANSLATORS: Current value for WML code increase_damage, documented in https://wiki.wesnoth.org/EffectWML
570  "<span color=\"$color\">$number_or_percent</span> damage",
571  "<span color=\"$color\">$number_or_percent</span> damage",
572  increase_damage.to_int(),
573  format_modifier(increase_damage)));
574  }
575 
576  if(!set_damage.empty()) {
577  // TRANSLATORS: Current value for WML code set_damage, documented in https://wiki.wesnoth.org/EffectWML
578  desc.emplace_back(VNGETTEXT(
579  "$number damage",
580  "$number damage",
581  set_damage.to_int(),
582  {{"number", set_damage.str()}}));
583  }
584 
585  if(!increase_attacks.empty()) {
586  desc.emplace_back(VNGETTEXT(
587  // TRANSLATORS: Current value for WML code increase_attacks, documented in https://wiki.wesnoth.org/EffectWML
588  "<span color=\"$color\">$number_or_percent</span> strike",
589  "<span color=\"$color\">$number_or_percent</span> strikes",
590  increase_attacks.to_int(),
591  format_modifier(increase_attacks)));
592  }
593 
594  if(!set_attacks.empty()) {
595  desc.emplace_back(VNGETTEXT(
596  // TRANSLATORS: Current value for WML code set_attacks, documented in https://wiki.wesnoth.org/EffectWML
597  "$number strike",
598  "$number strikes",
599  set_attacks.to_int(),
600  {{"number", set_attacks.str()}}));
601  }
602 
603  if(!set_accuracy.empty()) {
604  desc.emplace_back(VGETTEXT(
605  // TRANSLATORS: Current value for WML code set_accuracy, documented in https://wiki.wesnoth.org/EffectWML
606  "$number| accuracy",
607  {{"number", set_accuracy.str()}}));
608  }
609 
610  if(!increase_accuracy.empty()) {
611  desc.emplace_back(VGETTEXT(
612  // TRANSLATORS: Current value for WML code increase_accuracy, documented in https://wiki.wesnoth.org/EffectWML
613  "<span color=\"$color\">$number_or_percent|%</span> accuracy",
614  format_modifier(increase_accuracy)));
615  }
616 
617  if(!set_parry.empty()) {
618  desc.emplace_back(VGETTEXT(
619  // TRANSLATORS: Current value for WML code set_parry, documented in https://wiki.wesnoth.org/EffectWML
620  "$number parry",
621  {{"number", set_parry.str()}}));
622  }
623 
624  if(!increase_parry.empty()) {
625  desc.emplace_back(VGETTEXT(
626  // TRANSLATORS: Current value for WML code increase_parry, documented in https://wiki.wesnoth.org/EffectWML
627  "<span color=\"$color\">$number_or_percent</span> parry",
628  format_modifier(increase_parry)));
629  }
630 
631  if(!set_movement.empty()) {
632  desc.emplace_back(VNGETTEXT(
633  // TRANSLATORS: Current value for WML code set_movement_used, documented in https://wiki.wesnoth.org/EffectWML
634  "$number movement point",
635  "$number movement points",
636  set_movement.to_int(),
637  {{"number", set_movement.str()}}));
638  }
639 
640  if(!increase_movement.empty()) {
641  desc.emplace_back(VNGETTEXT(
642  // TRANSLATORS: Current value for WML code increase_movement_used, documented in https://wiki.wesnoth.org/EffectWML
643  "<span color=\"$color\">$number_or_percent</span> movement point",
644  "<span color=\"$color\">$number_or_percent</span> movement points",
645  increase_movement.to_int(),
646  format_modifier(increase_movement)));
647  }
648 
649  if(!set_attacks_used.empty()) {
650  desc.emplace_back(VNGETTEXT(
651  // TRANSLATORS: Current value for WML code set_attacks_used, documented in https://wiki.wesnoth.org/EffectWML
652  "$number attack used",
653  "$number attacks used",
654  set_attacks_used.to_int(),
655  {{"number", set_attacks_used.str()}}));
656  }
657 
658  if(!increase_attacks_used.empty()) {
659  desc.emplace_back(VNGETTEXT(
660  // TRANSLATORS: Current value for WML code increase_attacks_used, documented in https://wiki.wesnoth.org/EffectWML
661  "<span color=\"$color\">$number_or_percent</span> attack used",
662  "<span color=\"$color\">$number_or_percent</span> attacks used",
663  increase_attacks_used.to_int(),
664  format_modifier(increase_attacks_used)));
665  }
666 
667  return utils::format_conjunct_list("", desc);
668 }
669 
671 {
672  cfg["description"] = description_;
673  cfg["name"] = id_;
674  cfg["type"] = type_;
675  cfg["icon"] = icon_;
676  cfg["range"] = range_;
677  cfg["min_range"] = min_range_;
678  cfg["max_range"] = max_range_;
679  cfg["alignment"] = alignment_str();
680  cfg["damage"] = damage_;
681  cfg["number"] = num_attacks_;
682  cfg["attack_weight"] = attack_weight_;
683  cfg["defense_weight"] = defense_weight_;
684  cfg["accuracy"] = accuracy_;
685  cfg["movement_used"] = movement_used_;
686  cfg["attacks_used"] = attacks_used_;
687  cfg["parry"] = parry_;
688  cfg.add_child("specials", specials_cfg());
689 }
690 
691 
692 
693 int attack_type::composite_value(const active_ability_list& abil_list, int base_value) const
694 {
695  return unit_abilities::effect(abil_list, base_value, shared_from_this()).get_composite_value();
696 }
697 
698 
699 /**
700  * Calculates the number of attacks this weapon has, considering specials.
701  * This returns two numbers because of the swarm special. The actual number of
702  * attacks depends on the unit's health and should be:
703  * min_attacks + (max_attacks - min_attacks) * (current hp) / (max hp)
704  * c.f. swarm_blows()
705  */
706 void attack_type::modified_attacks(unsigned& min_attacks,
707  unsigned& max_attacks) const
708 {
709  // Apply [attacks].
710  int attacks_value = composite_value(get_specials_and_abilities("attacks"), num_attacks());
711 
712  if (attacks_value < 0) {
713  attacks_value = 0;
714  ERR_NG << "negative number of strikes after applying weapon specials";
715  }
716 
717  // Apply [swarm].
718  active_ability_list swarm_specials = get_specials_and_abilities("swarm");
719  if (!swarm_specials.empty()) {
720  min_attacks = std::max<int>(0, swarm_specials.highest("swarm_attacks_min").first);
721  max_attacks = std::max<int>(0, swarm_specials.highest("swarm_attacks_max", attacks_value).first);
722  }
723  else {
724  min_attacks = max_attacks = attacks_value;
725  }
726 }
727 
728 std::string attack_type::select_replacement_type(const active_ability_list& damage_type_list) const
729 {
730  std::map<std::string, unsigned int> type_count;
731  unsigned int max = 0;
732  for (auto& i : damage_type_list) {
733  const config& c = i.ability_cfg();
734  if (c.has_attribute("replacement_type")) {
735  std::string type = c["replacement_type"].str();
736  unsigned int count = ++type_count[type];
737  if ((count > max)) {
738  max = count;
739  }
740  }
741  }
742 
743  if (type_count.empty()) return type();
744 
745  std::vector<std::string> type_list;
746  for (auto& i : type_count) {
747  if (i.second == max) {
748  type_list.push_back(i.first);
749  }
750  }
751 
752  if (type_list.empty()) return type();
753 
754  return type_list.front();
755 }
756 
757 std::pair<std::string, int> attack_type::select_alternative_type(const active_ability_list& damage_type_list, const active_ability_list& resistance_list) const
758 {
759  auto ctx = fallback_context();
760  auto [self, other] = context_->self_and_other(*this);
761  std::map<std::string, int> type_res;
762  int max_res = INT_MIN;
763  if (other.un) {
764  for (auto& i : damage_type_list) {
765  const config& c = i.ability_cfg();
766  if (c.has_attribute("alternative_type")) {
767  std::string type = c["alternative_type"].str();
768  if (type_res.count(type) == 0) {
769  type_res[type] = other.un->resistance_value(resistance_list, type);
770  max_res = std::max(max_res, type_res[type]);
771  }
772  }
773  }
774  }
775 
776  if (type_res.empty()) return { "", INT_MIN };
777 
778  std::vector<std::string> type_list;
779  for (auto& i : type_res) {
780  if (i.second == max_res) {
781  type_list.push_back(i.first);
782  }
783  }
784  if (type_list.empty()) return { "", INT_MIN };
785 
786  return { type_list.front(), max_res };
787 }
788 
789 /**
790  * The type of attack used and the resistance value that does the most damage.
791  */
792 std::pair<std::string, int> attack_type::effective_damage_type() const
793 {
794  auto ctx = fallback_context();
795  auto [self, other] = context_->self_and_other(*this);
796  bool is_attacker = &context_->attacker == &self;
797  if (attack_empty()) {
798  return { "", 100 };
799  }
800  active_ability_list resistance_list;
801  if (other.un) {
802  resistance_list = context_->get_abilities_weapons("resistance", *other.un);
803  utils::erase_if(resistance_list, [&](const active_ability& i) {
804  return !i.ability().active_on_matches(!is_attacker);
805  });
806  }
807  active_ability_list damage_type_list = get_specials_and_abilities("damage_type");
808  int res = other.un ? other.un->resistance_value(resistance_list, type()) : 100;
809  if (damage_type_list.empty()) {
810  return { type(), res };
811  }
812  std::string replacement_type = select_replacement_type(damage_type_list);
813  std::pair<std::string, int> alternative_type = select_alternative_type(damage_type_list, resistance_list);
814 
815  if (other.un) {
816  res = replacement_type != type() ? other.un->resistance_value(resistance_list, replacement_type) : res;
817  replacement_type = alternative_type.second > res ? alternative_type.first : replacement_type;
818  res = std::max(res, alternative_type.second);
819  }
820  return { replacement_type, res };
821 }
822 
823 /**
824  * Return a type()/replacement_type and a list of alternative_types that should be displayed in the selected unit's report.
825  */
826 std::pair<std::string, std::set<std::string>> attack_type::damage_types() const
827 {
828  active_ability_list damage_type_list = get_specials_and_abilities("damage_type");
829  std::set<std::string> alternative_damage_types;
830  if (damage_type_list.empty()) {
831  return { type(), alternative_damage_types };
832  }
833  std::string replacement_type = select_replacement_type(damage_type_list);
834  for (auto& i : damage_type_list) {
835  const config& c = i.ability_cfg();
836  if (c.has_attribute("alternative_type")) {
837  alternative_damage_types.insert(c["alternative_type"].str());
838  }
839  }
840 
841  return { replacement_type, alternative_damage_types };
842 }
843 
844 /**
845  * Returns the damage per attack of this weapon, considering specials.
846  */
848 {
849  return unit_abilities::effect(get_specials_and_abilities("damage"), damage(), shared_from_this()).get_composite_double_value();
850 }
851 
853 {
854  auto ctx = fallback_context();
855  auto [self, other] = context_->self_and_other(*this);
856  int parry = other.at ? other.at->parry() : 0;
857  cth = std::clamp(cth + accuracy_ - parry, 0, 100);
858  return composite_value(get_specials_and_abilities("chance_to_hit"), cth);
859 }
860 
861 std::unique_ptr<specials_context_t> attack_type::fallback_context(const unit_ptr& self) const
862 {
863  if (context_) {
864  return nullptr;
865  } else {
866  map_location loc = self ? self->get_location() : map_location();
867  bool attacking = true;
868  return std::unique_ptr<specials_context_t>(new specials_context_t(specials_context_t::make({ self , loc, shared_from_this() }, {}, attacking)));
869  }
870 }
871 
873 {
874  auto ctx = fallback_context();
875  auto [self, other] = context_->self_and_other(*this);
876  return context_->is_special_active(self, ab, whom);
877 }
878 
879 
881 {
882  auto ctx = fallback_context();
883  auto abil_list = context_->get_active_specials(*this, special);
884 
885  // get a list of specials/"specials as abilities" that may potentially overwrite others
886  active_ability_list overwriters = overwrite_special_overwriter(abil_list);
887  if (!abil_list.empty() && !overwriters.empty()) {
888  // remove all abilities that would be overwritten
889  utils::erase_if(abil_list, [&](const active_ability& j) {
890  return (overwrite_special_checking(overwriters, j.ability()));
891  });
892  }
893  return abil_list;
894 }
895 /**
896  * Returns whether or not @a *this has a special ability with a tag or id equal to
897  * @a special. the Check is for a special ability
898  * active in the current context (see set_specials_context), including
899  * specials obtained from the opponent's attack.
900  */
901 bool attack_type::has_special_or_ability(const std::string& special) const
902 {
903  auto ctx = fallback_context();
904  return context_->has_active_special(*this, special);
905 }
906 
907 bool attack_type::has_active_special_or_ability_id(const std::string& special) const
908 {
909  auto ctx = fallback_context();
910  return context_->has_active_special_id(*this, special);
911 }
912 
914 {
915  if (!filter["active"].to_bool()) {
916  return utils::find_if(specials(), [&](const ability_ptr& p_ab) { return p_ab->matches_filter(filter); });
917  }
918 
919  auto ctx = fallback_context();
921 }
922 
923 
924 /**
925  * Returns a vector of names and descriptions for the specials of *this.
926  */
927 std::vector<unit_ability_t::tooltip_info> attack_type::special_tooltips() const
928 {
929  std::vector<unit_ability_t::tooltip_info> res;
930  for (const auto& p_ab : specials()) {
931  auto name = p_ab->get_name();
932  auto desc = p_ab->get_description();
933 
934  if (!name.empty()) {
935  res.AGGREGATE_EMPLACE(
936  name,
937  desc,
938  p_ab->get_help_topic_id()
939  );
940  }
941  }
942  return res;
943 }
std::vector< ability_ptr > ability_vector
Definition: abilities.hpp:33
map_location loc
Definition: move.cpp:172
static lg::log_domain log_unit("unit")
static lg::log_domain log_engine("engine")
#define ERR_NG
Definition: attack_type.cpp:52
#define ERR_WML
Definition: attack_type.cpp:49
static lg::log_domain log_wml("wml")
static bool matches_simple_filter(const attack_type &attack, const config &filter, const std::string &check_if_recursion)
Returns whether or not *this matches the given filter, ignoring the complexities introduced by [and],...
static lg::log_domain log_config("config")
std::pair< int, map_location > highest(const std::string &key, int def=0) const
Definition: abilities.hpp:195
bool empty() const
Definition: abilities.hpp:217
std::string alignment_str() const
Returns alignment specified by alignment() for filtering when exist.
Definition: attack_type.hpp:92
void set_min_range(int value)
Definition: attack_type.hpp:72
active_ability_list overwrite_special_overwriter(active_ability_list overwriters) const
Filter a list of abilities or weapon specials, removing any entries that don't own the overwrite_spec...
Definition: abilities.cpp:1403
int min_range() const
Definition: attack_type.hpp:51
bool has_active_special_or_ability_id(const std::string &special) const
const std::string & range() const
Definition: attack_type.hpp:50
void set_attacks_used(int value)
int movement_used() const
bool has_special_or_ability_with_filter(const config &filter) const
check if special matche
void set_accuracy(int value)
Definition: attack_type.hpp:75
std::vector< unit_ability_t::tooltip_info > special_tooltips() const
Returns a vector of names and descriptions for the specials of *this.
const std::string & type() const
Definition: attack_type.hpp:48
std::pair< std::string, int > select_alternative_type(const active_ability_list &damage_type_list, const active_ability_list &resistance_list) const
Select best damage type based on highest damage for alternative_type.
bool has_special_or_ability(const std::string &special) const
used for abilities used like weapon and true specials
int parry() const
Definition: attack_type.hpp:56
std::string accuracy_parry_tooltip() const
std::string accuracy_parry_description() const
Definition: attack_type.cpp:90
std::unique_ptr< specials_context_t > fallback_context(const unit_ptr &self=nullptr) const
double modified_damage() const
Returns the damage per attack of this weapon, considering specials.
void apply_effect(const config &cfg)
Applies effect modifications described by cfg.
bool matches_filter(const config &filter, const std::string &check_if_recursion="") const
Returns whether or not *this matches the given filter.
std::string select_replacement_type(const active_ability_list &damage_type_list) const
Select best damage type based on frequency count for replacement_type.
config specials_cfg() const
Definition: attack_type.hpp:63
friend class specials_context_t
void set_defense_weight(double value)
Definition: attack_type.hpp:80
int num_attacks() const
Definition: attack_type.hpp:58
void set_changed(bool value)
active_ability_list get_specials_and_abilities(const std::string &special) const
std::pair< std::string, std::set< std::string > > damage_types() const
Return a type()/replacement_type and a list of alternative_types that should be displayed in the sele...
std::string type_
std::string icon_
void set_parry(int value)
Definition: attack_type.hpp:76
void set_attack_weight(double value)
Definition: attack_type.hpp:79
void set_damage(int value)
Definition: attack_type.hpp:77
ability_vector specials_
const t_string & name() const
Definition: attack_type.hpp:46
int attacks_used() const
const std::string & id() const
Definition: attack_type.hpp:47
void set_icon(const std::string &value)
Definition: attack_type.hpp:70
bool has_filter_special_or_ability(const config &filter) const
check if special matche handles the special_(id/type) attributes in weapon filters.
int modified_chance_to_hit(int cth) const
Return the defense value, considering specials.
bool special_active(const unit_ability_t &ab, AFFECTS whom) const
bool attack_empty() const
Returns true if this is a dummy attack_type, for example the placeholder that the unit_attack dialog ...
double defense_weight_
std::string id_
double attack_weight_
void modified_attacks(unsigned &min_attacks, unsigned &max_attacks) const
Calculates the number of attacks this weapon has, considering specials.
std::string range_
void remove_special_by_filter(const config &filter)
remove special if matche condition
void set_max_range(int value)
Definition: attack_type.hpp:73
specials_context_t * context_
utils::optional< unit_alignments::type > alignment_
attack_type(const config &cfg)
Definition: attack_type.cpp:55
void set_type(const std::string &value)
Definition: attack_type.hpp:69
void write(config &cfg) const
bool overwrite_special_checking(active_ability_list &overwriters, const unit_ability_t &ab) const
Check whether cfg would be overwritten by any element of overwriters.
Definition: abilities.cpp:1439
int accuracy() const
Definition: attack_type.hpp:55
int max_range() const
Definition: attack_type.hpp:52
void set_range(const std::string &value)
Definition: attack_type.hpp:71
int composite_value(const active_ability_list &abil_list, int base_value) const
Return the special weapon value, considering specials.
static std::string describe_effect(const config &cfg)
Generates a description of the effect specified by cfg, if applicable.
int damage() const
Definition: attack_type.hpp:57
const ability_vector & specials() const
Definition: attack_type.hpp:61
void set_attack_alignment(const std::string &value)
Definition: attack_type.hpp:74
t_string description_
void set_name(const t_string &value)
Definition: attack_type.hpp:67
std::pair< std::string, int > effective_damage_type() const
The type of attack used and the resistance value that does the most damage.
Variant for storing WML attributes.
bool empty() const
Tests for an attribute that either was never set or was set to "".
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:157
config & add_child(std::string_view key)
Definition: config.cpp:436
optional_config_impl< config > optional_child(std::string_view key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:380
auto all_children_view() const
In-order iteration over all children.
Definition: config.hpp:795
bool has_active_special(const attack_type &at, const std::string &tag) const
Returns whether or not *this has a special ability with a tag or id equal to special.
Definition: abilities.cpp:1524
active_ability_list get_abilities_weapons(const std::string &tag, const unit &un) const
Definition: abilities.cpp:1610
bool is_special_active(const specials_combatant &wep, const unit_ability_t &ab, unit_ability_t::affects_t whom) const
Returns whether or not the given special is active for the specified unit, based on the current conte...
Definition: abilities.cpp:1882
specials_combatant attacker
Definition: abilities.hpp:306
self_and_other_red_t self_and_other(const attack_type &self_att) const
Definition: abilities.hpp:292
bool has_active_special_id(const attack_type &at, const std::string &id) const
Definition: abilities.cpp:1535
bool has_active_special_matching_filter(const attack_type &at, const config &filter) const
Definition: abilities.cpp:1837
static specials_context_t make(specials_combatant &&self, specials_combatant &&other, bool attacking)
Definition: abilities.hpp:271
active_ability_list get_active_specials(const attack_type &at, const std::string &tag) const
Definition: abilities.cpp:1586
bool empty() const
Definition: tstring.hpp:199
int get_composite_value() const
Definition: abilities.hpp:359
double get_composite_double_value() const
Definition: abilities.hpp:361
static void parse_vector(const config &abilities_cfg, ability_vector &res, bool inside_attack)
Definition: abilities.cpp:252
static ability_ptr create(std::string tag, config cfg, bool inside_attack)
Definition: abilities.hpp:47
static config add_registry_entries(const config &base_cfg, const std::string &registry_name, const std::map< std::string, config > &registry)
Definition: types.cpp:1061
const std::map< std::string, config > & specials() const
Definition: types.hpp:412
static variant evaluate(const const_formula_ptr &f, const formula_callable &variables, formula_debugger *fdb=nullptr, variant default_res=variant(0))
Definition: formula.hpp:48
bool as_bool() const
Returns a boolean state of the variant value.
Definition: variant.cpp:313
std::string deprecated_message(const std::string &elem_name, DEP_LEVEL level, const version_info &version, const std::string &detail)
Definition: deprecation.cpp:29
const config * cfg
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
#define VNGETTEXT(msgid, msgid_plural, count,...)
std::size_t i
Definition: function.cpp:1032
Interfaces for manipulating version numbers of engine, add-ons, etc.
static std::string _(const char *str)
Definition: gettext.hpp:97
std::string tooltip
Shown when hovering over an entry in the filter's drop-down list.
Definition: manager.cpp:203
Standard logging facilities (interface).
General math utility functions.
bool in_ranges(const Cmp c, const std::vector< std::pair< Cmp, Cmp >> &ranges)
Definition: math.hpp:85
std::stringstream & log_to_chat()
Use this to show WML errors in the ingame chat.
Definition: log.cpp:550
std::string bold(Args &&... data)
Applies bold Pango markup to the input.
Definition: markup.hpp:161
std::string egettext(char const *msgid)
Definition: gettext.cpp:429
constexpr auto filter
Definition: ranges.hpp:38
std::set< std::string > split_set(std::string_view s, char sep, const int flags)
std::vector< std::pair< int, int > > parse_ranges_int(const std::string &str)
Handles a comma-separated list of inputs to parse_range.
bool contains(const Container &container, const Value &value)
Returns true iff value is found in container.
Definition: general.hpp:87
auto * find_if(Container &container, const Predicate &predicate)
Convenience wrapper for using find_if on a container without needing to comare to end()
Definition: general.hpp:151
std::vector< std::pair< int, int > > parse_ranges_unsigned(const std::string &str)
Handles a comma-separated list of inputs to parse_range, in a context that does not expect negative v...
void erase_if(Container &container, const Predicate &predicate)
Convenience wrapper for using std::remove_if on a container.
Definition: general.hpp:107
int apply_modifier(const int number, const std::string &amount, const int minimum)
std::string format_conjunct_list(const t_string &empty, const std::vector< t_string > &elems)
Format a conjunctive list.
std::map< std::string, t_string > string_map
std::string signed_percent(int val)
Convert into a percentage (using the Unicode "−" and +0% convention.
std::vector< std::string > split(const config_attribute_value &val)
std::string print_modifier(const std::string &mod)
Add a "+" or replace the "-" par Unicode minus.
std::shared_ptr< unit_ability_t > ability_ptr
Definition: ptr.hpp:38
std::shared_ptr< unit > unit_ptr
Definition: ptr.hpp:26
Data typedef for active_ability_list.
Definition: abilities.hpp:161
const unit_ability_t & ability() const
Definition: abilities.hpp:183
Encapsulates the map of the game.
Definition: location.hpp:46
The base template for associating string values with enum values.
Definition: enum_base.hpp:33
static constexpr utils::optional< enum_type > get_enum(const std::string_view value)
Converts a string into its enum equivalent.
Definition: enum_base.hpp:57
mock_char c
static map_location::direction s
unit_type_data unit_types
Definition: types.cpp:1514
#define e