The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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"
28#include "deprecation.hpp"
29#include "game_version.hpp"
30
31#include "log.hpp"
34#include "gettext.hpp"
35#include "utils/general.hpp"
36#include "utils/math.hpp"
37
38static 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
44static 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
48static lg::log_domain log_wml("wml");
49#define ERR_WML LOG_STREAM(err, log_wml)
50
51static 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
123namespace
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 */
161static 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::FOR_REMOVAL, {1, 21, 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")) {
239 if(!attack.has_filter_special_or_ability(filter)) {
240 return false;
241 }
242 }
243
244 if(filter.has_attribute("special_active")) {
245 deprecated_message("special_active=", DEP_LEVEL::FOR_REMOVAL, {1, 21, 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 */
301bool 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) {
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
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) {
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
693int attack_type::composite_value(const active_ability_list& abil_list, int base_value) const
694{
695 return unit_abilities::effect(abil_list, base_value, context_).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 */
706void 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
728std::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
757std::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 */
792std::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 */
826std::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 */
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
861std::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
880{
881 auto ctx = fallback_context();
882 auto abil_list = context_->get_active_specials(*this, special);
883
884 utils::sort_if(abil_list,[](const active_ability& i, const active_ability& j){
885 double l = i.ability().suppress_special_priority();
886 double r = j.ability().suppress_special_priority();
887 return l > r;
888 });
889 utils::erase_if(abil_list, [&](const active_ability& i) {
890 return (overwrite_special_checking(abil_list, i));
891 });
892 return abil_list;
893}
894/**
895 * Returns whether or not @a *this has a special ability with a tag or id equal to
896 * @a special. the Check is for a special ability
897 * active in the current context (see set_specials_context), including
898 * specials obtained from the opponent's attack.
899 */
900bool attack_type::has_special_or_ability(const std::string& special) const
901{
902 auto ctx = fallback_context();
903 return context_->has_active_special(*this, special);
904}
905
906bool attack_type::has_active_special_or_ability_id(const std::string& special) const
907{
908 auto ctx = fallback_context();
909 return context_->has_active_special_id(*this, special);
910}
911
913{
914 if (!filter["active"].to_bool()) {
915 return utils::find_if(specials(), [&](const ability_ptr& p_ab) { return p_ab->matches_filter(filter); });
916 }
917
918 auto ctx = fallback_context();
919 return context_->has_active_special_matching_filter(*this, filter);
920}
921
922
923/**
924 * Returns a vector of names and descriptions for the specials of *this.
925 */
926std::vector<unit_ability_t::tooltip_info> attack_type::special_tooltips() const
927{
928 std::vector<unit_ability_t::tooltip_info> res;
929 for (const auto& p_ab : specials()) {
930 auto name = p_ab->get_name();
931 auto desc = p_ab->get_description();
932
933 if (!name.empty()) {
934 res.AGGREGATE_EMPLACE(
935 name,
936 desc,
937 p_ab->get_help_topic_id()
938 );
939 }
940 }
941 return res;
942}
std::vector< ability_ptr > ability_vector
Definition abilities.hpp:33
map_location loc
Definition move.cpp:172
#define ERR_NG
static lg::log_domain log_unit("unit")
static lg::log_domain log_engine("engine")
#define ERR_WML
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
bool empty() const
std::string alignment_str() const
Returns alignment specified by alignment() for filtering when exist.
void set_min_range(int value)
const t_string & name() const
int min_range() const
bool has_active_special_or_ability_id(const std::string &special) const
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)
std::vector< unit_ability_t::tooltip_info > special_tooltips() const
Returns a vector of names and descriptions for the specials of *this.
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
std::string accuracy_parry_tooltip() const
std::string accuracy_parry_description() const
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
friend class specials_context_t
void set_defense_weight(double value)
int num_attacks() const
const std::string & id() const
void set_changed(bool value)
active_ability_list get_specials_and_abilities(const std::string &special) const
bool overwrite_special_checking(active_ability_list &overwriters, const active_ability &overwrited) const
Check whether overwrited would be overwritten by any element of overwriters.
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)
void set_attack_weight(double value)
void set_damage(int value)
ability_vector specials_
const std::string & range() const
int attacks_used() const
void set_icon(const std::string &value)
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.
const std::string & type() const
std::string range_
void remove_special_by_filter(const config &filter)
remove special if matche condition
void set_max_range(int value)
specials_context_t * context_
utils::optional< unit_alignments::type > alignment_
attack_type(const config &cfg)
void set_type(const std::string &value)
void write(config &cfg) const
int accuracy() const
int max_range() const
void set_range(const std::string &value)
const ability_vector & specials() const
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
void set_attack_alignment(const std::string &value)
t_string description_
void set_name(const t_string &value)
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
void insert(std::string_view key, T &&value)
Inserts an attribute into the config.
Definition config.hpp:519
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
self_and_other_ref self_and_other(const attack_type &self_att) const
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.
active_ability_list get_abilities_weapons(const std::string &tag, const unit &un) const
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...
specials_combatant attacker
bool has_active_special_id(const attack_type &at, const std::string &id) const
bool has_active_special_matching_filter(const attack_type &at, const config &filter) const
static specials_context_t make(specials_combatant &&self, specials_combatant &&other, bool attacking)
active_ability_list get_active_specials(const attack_type &at, const std::string &tag) const
bool empty() const
Definition tstring.hpp:200
int get_composite_value() const
double get_composite_double_value() const
static void parse_vector(const config &abilities_cfg, ability_vector &res, bool inside_attack)
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:1041
const std::map< std::string, config > & specials() const
Definition types.hpp:411
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:357
std::string deprecated_message(const std::string &elem_name, DEP_LEVEL level, const version_info &version, const std::string &detail)
const config * cfg
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
#define VNGETTEXT(msgid, msgid_plural, count,...)
Interfaces for manipulating version numbers of engine, add-ons, etc.
static std::string _(const char *str)
Definition gettext.hpp:100
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
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:152
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:88
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:108
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.
void sort_if(Container &container, const Predicate &predicate)
Convenience wrapper for using std::sort on a container.
Definition general.hpp:133
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.
const unit_ability_t & ability() const
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
static map_location::direction s
unit_type_data unit_types
Definition types.cpp:1499
#define i
#define e
#define c