The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
attack.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 * Fighting.
19 */
20
21#include "actions/attack.hpp"
22
23#include <utility>
24
26#include "actions/vision.hpp"
27
29#include "game_config.hpp"
30#include "game_data.hpp"
31#include "game_events/pump.hpp"
32#include "gettext.hpp"
33#include "log.hpp"
34#include "map/map.hpp"
36#include "play_controller.hpp"
38#include "random.hpp"
39#include "replay.hpp"
40#include "resources.hpp"
41#include "statistics.hpp"
42#include "synced_checkup.hpp"
43#include "team.hpp"
44#include "tod_manager.hpp"
45#include "units/abilities.hpp"
47#include "units/map.hpp"
48#include "units/udisplay.hpp"
49#include "units/unit.hpp"
50#include "units/types.hpp"
53#include "wml_exception.hpp"
54
56
57static lg::log_domain log_engine("engine");
58#define DBG_NG LOG_STREAM(debug, log_engine)
59#define LOG_NG LOG_STREAM(info, log_engine)
60#define WRN_NG LOG_STREAM(err, log_engine)
61#define ERR_NG LOG_STREAM(err, log_engine)
62
63static lg::log_domain log_attack("engine/attack");
64#define DBG_AT LOG_STREAM(debug, log_attack)
65#define LOG_AT LOG_STREAM(info, log_attack)
66#define WRN_AT LOG_STREAM(err, log_attack)
67#define ERR_AT LOG_STREAM(err, log_attack)
68
69static lg::log_domain log_config("config");
70#define LOG_CF LOG_STREAM(info, log_config)
71
72// ==================================================================================
73// BATTLE CONTEXT UNIT STATS
74// ==================================================================================
75
76namespace biased_rng
77{
82
84{
85 return rng.get_random_int(0, 99);
86}
87
88bool roll_hit(randomness::rng& rng, int strikes_left, int chance_to_hit, int& credit)
89{
90 if(chance_to_hit == 0) {
91 return false;
92 }
93 if(chance_to_hit == 100) {
94 return true;
95 }
96
97 const int expected_hits = (chance_to_hit * strikes_left + credit) / 100;
98 const bool does_hit = rng.get_random_int(0, strikes_left - 1) < expected_hits;
99 credit += chance_to_hit - (does_hit ? 100 : 0);
100 return does_hit;
101}
102} // namespace biased_rng
103
105 const map_location& u_loc,
106 int u_attack_num,
107 bool attacking,
109 const map_location& opp_loc,
110 const const_attack_ptr& opp_weapon,
111 utils::optional<int> opp_terrain_defense,
112 utils::optional<int> lawful_bonus
113 )
114 : weapon(nullptr)
115 , attack_num(u_attack_num)
116 , is_attacker(attacking)
117 , is_poisoned(up->get_state(unit::STATE_POISONED))
118 , is_slowed(up->get_state(unit::STATE_SLOWED))
119 , slows(false)
120 , drains(false)
121 , petrifies(false)
122 , plagues(false)
123 , poisons(false)
124 , swarm(false)
125 , firststrike(false)
126 , disable(false)
127 , leadership_bonus(0)
128 , experience(up->experience())
129 , max_experience(up->max_experience())
130 , level(up->level())
131 , rounds(1)
132 , hp(0)
133 , max_hp(up->max_hitpoints())
134 , chance_to_hit(0)
135 , damage(0)
136 , slow_damage(0)
137 , drain_percent(0)
138 , drain_constant(0)
139 , num_blows(0)
140 , swarm_min(0)
141 , swarm_max(0)
142 , plague_type()
143{
144 const unit& u = *up;
145 const unit& opp = *oppp;
146 // Get the current state of the unit.
147 if(attack_num >= 0) {
148 weapon = u.attacks()[attack_num].shared_from_this();
149 }
150
151 if(u.hitpoints() < 0) {
152 LOG_CF << "Unit with " << u.hitpoints() << " hitpoints found, set to 0 for damage calculations";
153 hp = 0;
154 } else if(u.hitpoints() > u.max_hitpoints()) {
155 // If a unit has more hp than its maximum, the engine will fail with an
156 // assertion failure due to accessing the prob_matrix out of bounds.
157 hp = u.max_hitpoints();
158 } else {
159 hp = u.hitpoints();
160 }
161
162 // Exit if no weapon.
163 if(!weapon) {
164 return;
165 }
166
167 // Get the weapon characteristics as appropriate.
168 auto ctx = specials_context_t::make({ up, u_loc, weapon }, { oppp, opp_loc, opp_weapon }, attacking);
169
170 slows = weapon->has_special_or_ability("slow") && !opp.get_state("unslowable") ;
171 drains = !opp.get_state("undrainable") && weapon->has_special_or_ability("drains");
172 petrifies = !opp.get_state("unpetrifiable") && weapon->has_special_or_ability("petrifies");
173 poisons = !opp.get_state("unpoisonable") && weapon->has_special_or_ability("poison") && !opp.get_state(unit::STATE_POISONED);
174 rounds = weapon->get_specials_and_abilities("berserk").highest("value", 1).first;
175
176 firststrike = weapon->has_special_or_ability("firststrike");
177
178 {
179 const int distance = distance_between(u_loc, opp_loc);
180 const bool out_of_range = distance > weapon->max_range() || distance < weapon->min_range();
181 disable = weapon->has_special_or_ability("disable") || out_of_range;
182 }
183
184 // Handle plague.
185 active_ability_list plague_specials = weapon->get_specials_and_abilities("plague");
186 plagues = !opp.get_state("unplagueable") && !plague_specials.empty() &&
187 opp.undead_variation() != "null" && !resources::gameboard->map().is_village(opp_loc);
188
189 if(plagues) {
190 plague_type = plague_specials.front().ability_cfg()["type"].str();
191
192 if(plague_type.empty()) {
193 plague_type = u.type().parent_id();
194 }
195 }
196
197 // Compute chance to hit.
198
199 int opp_base_cth = opp_terrain_defense ? (100 - *opp_terrain_defense) : opp.defense_modifier(resources::gameboard->map().get_terrain(opp_loc));
200 signed int cth = weapon->modified_chance_to_hit(opp_base_cth);
201
202 if(opp.get_state("invulnerable")) {
203 cth = 0;
204 }
205
206 chance_to_hit = std::clamp(cth, 0, 100);
207
208 // Compute base damage done with the weapon.
209 double base_damage = weapon->modified_damage();
210
211 // Get the damage multiplier applied to the base damage of the weapon.
212 int damage_multiplier = 100;
213
214 // Time of day bonus.
215 unit_alignments::type alignment = weapon->alignment().value_or(u.alignment());
216 if (lawful_bonus) {
217 damage_multiplier += generic_combat_modifier(*lawful_bonus, alignment, u.is_fearless(), 0);
218 }
219 else {
220 damage_multiplier += combat_modifier(
221 resources::gameboard->units(), resources::gameboard->map(), u_loc, alignment, u.is_fearless());
222 }
223
224
225 // Leadership bonus.
227 if(leadership_bonus != 0) {
228 damage_multiplier += leadership_bonus;
229 }
230
231 // Resistance modifier.
232
233 const auto [damage_type, resistance_modifier] = weapon->effective_damage_type();
234 damage_multiplier *= resistance_modifier;
235
236 // Compute both the normal and slowed damage.
237 damage = round_damage(base_damage, damage_multiplier, 10000);
238 slow_damage = round_damage(base_damage, damage_multiplier, 20000);
239
240 if(is_slowed) {
242 }
243
244 // Compute drain amounts only if draining is possible.
245 if(drains) {
246 // Compute the drain percent (with 50% as the base for backward compatibility)
247 drain_percent = weapon->composite_value(weapon->get_specials_and_abilities("drains"), 50);
248 }
249
250 // Add heal_on_hit (the drain constant)
251 drain_constant += weapon->composite_value(weapon->get_specials_and_abilities("heal_on_hit"), 0);
252
254
255 // Compute the number of blows and handle swarm.
256 weapon->modified_attacks(swarm_min, swarm_max);
259 can_advance = u.advances();
260}
261
262
263// ==================================================================================
264// BATTLE CONTEXT
265// ==================================================================================
266
269 const map_location& a_loc,
270 int a_wep_index,
272 const map_location& d_loc,
273 int d_wep_index,
274 bool use_biased_rng)
275 : attacker_stats_()
276 , defender_stats_()
277 , attacker_combatant_()
278 , defender_combatant_()
279 , use_biased_rng_(use_biased_rng)
280{
281 std::size_t a_wep_uindex = static_cast<std::size_t>(a_wep_index);
282 std::size_t d_wep_uindex = static_cast<std::size_t>(d_wep_index);
283
284 const_attack_ptr a_wep(a_wep_uindex < attacker->attacks().size() ? attacker->attacks()[a_wep_index].shared_from_this() : nullptr);
285 const_attack_ptr d_wep(d_wep_uindex < defender->attacks().size() ? defender->attacks()[d_wep_index].shared_from_this() : nullptr);
286
287 attacker_stats_.reset(new battle_context_unit_stats(attacker, a_loc, a_wep_index, true , defender, d_loc, d_wep));
288 defender_stats_.reset(new battle_context_unit_stats(defender, d_loc, d_wep_index, false, attacker, a_loc, a_wep));
289}
290
292{
293 assert((attacker_combatant_ != nullptr) == (defender_combatant_ != nullptr));
294 assert(attacker_stats_);
295 assert(defender_stats_);
298 defender_combatant_.reset(new combatant(*defender_stats_, prev_def));
300 }
301}
302
303// more like a factory method than a constructor, always calls one of the other constructors.
305 const map_location& attacker_loc,
306 const map_location& defender_loc,
307 int attacker_weapon,
308 int defender_weapon,
309 double aggression,
310 const combatant* prev_def,
311 unit_const_ptr attacker,
312 unit_const_ptr defender,
313 bool use_biased_rng)
314 : attacker_stats_(nullptr)
315 , defender_stats_(nullptr)
316 , attacker_combatant_(nullptr)
317 , defender_combatant_(nullptr)
318{
319 //TODO: maybe check before dereferencing units.find(attacker_loc),units.find(defender_loc) ?
320 if(!attacker) {
321 attacker = units.find(attacker_loc).get_shared_ptr();
322 }
323 if(!defender) {
324 defender = units.find(defender_loc).get_shared_ptr();
325 }
326 nonempty_unit_const_ptr n_attacker { attacker };
327 nonempty_unit_const_ptr n_defender { defender };
328
329 const double harm_weight = 1.0 - aggression;
330
331 if(attacker_weapon == -1) {
333 n_attacker, n_defender, attacker_loc, defender_loc, harm_weight, prev_def, use_biased_rng
334 );
335 }
336 else if(defender_weapon == -1) {
338 n_attacker, n_defender, attacker_weapon, attacker_loc, defender_loc, prev_def, use_biased_rng
339 );
340 }
341 else {
342 *this = battle_context(
343 n_attacker, attacker_loc, attacker_weapon, n_defender, defender_loc, defender_weapon, use_biased_rng);
344 }
345
346 assert(attacker_stats_);
347 assert(defender_stats_);
348}
349
351 : attacker_stats_(new battle_context_unit_stats(att))
352 , defender_stats_(new battle_context_unit_stats(def))
353 , attacker_combatant_(nullptr)
354 , defender_combatant_(nullptr)
355{
356}
357
358
359/** @todo FIXME: better to initialize combatant initially (move into
360 battle_context_unit_stats?), just do fight() when required. */
362{
363 // We calculate this lazily, since AI doesn't always need it.
364 simulate(prev_def);
365 return *attacker_combatant_;
366}
367
369{
370 // We calculate this lazily, since AI doesn't always need it.
371 simulate(prev_def);
372 return *defender_combatant_;
373}
374
375// Given this harm_weight, are we better than that other context?
376bool battle_context::better_attack(class battle_context& that, double harm_weight)
377{
378 return better_combat(
383 harm_weight
384 );
385}
386
387// Given this harm_weight, are we better than that other context?
388bool battle_context::better_defense(class battle_context& that, double harm_weight)
389{
390 return better_combat(
395 harm_weight
396 );
397}
398
399// Does combat A give us a better result than combat B?
401 const combatant& them_a,
402 const combatant& us_b,
403 const combatant& them_b,
404 double harm_weight)
405{
406 double a, b;
407
408 // Compare: P(we kill them) - P(they kill us).
409 a = them_a.hp_dist[0] - us_a.hp_dist[0] * harm_weight;
410 b = them_b.hp_dist[0] - us_b.hp_dist[0] * harm_weight;
411
412 if(a - b < -0.01) {
413 return false;
414 }
415
416 if(a - b > 0.01) {
417 return true;
418 }
419
420 // Add poison to calculations, but poison bonus should only be applied if the unit survives
421 double poison_a_us = us_a.poisoned > 0 ? (us_a.poisoned - us_a.hp_dist[0]) * game_config::poison_amount : 0;
422 double poison_a_them = them_a.poisoned > 0 ? (them_a.poisoned - them_a.hp_dist[0]) * game_config::poison_amount : 0;
423 double poison_b_us = us_b.poisoned > 0 ? (us_b.poisoned - us_b.hp_dist[0]) * game_config::poison_amount : 0;
424 double poison_b_them = them_b.poisoned > 0 ? (them_b.poisoned - them_b.hp_dist[0]) * game_config::poison_amount : 0;
425
426 double attack_weight_a = us_a.u_.weapon->attack_weight();
427 double attack_weight_b = us_b.u_.weapon->attack_weight();
428 double damage_a = (them_a.u_.hp - them_a.average_hp()) * attack_weight_a;
429 double damage_b = (them_b.u_.hp - them_b.average_hp()) * attack_weight_b;
430
431 // Compare: damage to them - damage to us (average_hp replaces -damage)
432 a = (us_a.average_hp() - poison_a_us) * harm_weight + damage_a + poison_a_them;
433 b = (us_b.average_hp() - poison_b_us) * harm_weight + damage_b + poison_b_them;
434
435 if(a - b < -0.01) {
436 return false;
437 }
438
439 if(a - b > 0.01) {
440 return true;
441 }
442
443 // All else equal: go for most damage.
444 return damage_a >= damage_b;
445}
446
448 const nonempty_unit_const_ptr& defender,
449 const map_location& attacker_loc,
450 const map_location& defender_loc,
451 double harm_weight,
452 const combatant* prev_def,
453 bool use_biased_rng)
454{
455 log_scope2(log_attack, "choose_attacker_weapon");
456 std::vector<battle_context> choices;
457
458 // What options does attacker have?
459 const auto attacks = attacker->attacks();
460 for(std::size_t i = 0; i < attacks.size(); ++i) {
461 const attack_type& att = attacks[i];
462
463 if(att.attack_weight() <= 0) {
464 continue;
465 }
466 battle_context bc = choose_defender_weapon(attacker, defender, i, attacker_loc, defender_loc, prev_def, use_biased_rng);
467 //choose_defender_weapon will always choose the weapon that disabels the attackers weapon if possible.
468 if(bc.attacker_stats_->disable) {
469 continue;
470 }
471 choices.emplace_back(std::move(bc));
472 }
473
474 if(choices.empty()) {
475 return battle_context(attacker, attacker_loc, -1, defender, defender_loc, -1, use_biased_rng);
476 }
477
478 if(choices.size() == 1) {
479 return std::move(choices[0]);
480 }
481
482 // Multiple options: simulate them, save best.
483 battle_context* best_choice = nullptr;
484 for(auto& choice : choices) {
485 // If choose_defender_weapon didn't simulate, do so now.
486 choice.simulate(prev_def);
487
488 if(!best_choice || choice.better_attack(*best_choice, harm_weight)) {
489 best_choice = &choice;
490 }
491 }
492
493 if(best_choice) {
494 return std::move(*best_choice);
495 }
496 else {
497 return battle_context(attacker, attacker_loc, -1, defender, defender_loc, -1, use_biased_rng);
498 }
499}
500
501/** @todo FIXME: Hand previous defender unit in here. */
504 unsigned attacker_weapon,
505 const map_location& attacker_loc,
506 const map_location& defender_loc,
507 const combatant* prev_def,
508 bool use_biased_rng)
509{
510 log_scope2(log_attack, "choose_defender_weapon");
511 const auto attackers_attacks = attacker->attacks();
512 VALIDATE(attacker_weapon < attackers_attacks.size(), _("An invalid attacker weapon got selected."));
513
514 const attack_type& att = attackers_attacks[attacker_weapon];
515 auto no_weapon = [&]() { return battle_context(attacker, attacker_loc, attacker_weapon, defender, defender_loc, -1, use_biased_rng); };
516 std::vector<battle_context> choices;
517
518 // What options does defender have?
519 const auto defenses = defender->attacks();
520 for(std::size_t i = 0; i < defenses.size(); ++i) {
521 const attack_type& def = defenses[i];
522 if(def.range() != att.range() || def.defense_weight() <= 0) {
523 //no need to calculate the battle_context here.
524 continue;
525 }
526 battle_context bc(attacker, attacker_loc, attacker_weapon, defender, defender_loc, i, use_biased_rng);
527
528 if(bc.defender_stats_->disable) {
529 continue;
530 }
531 if(bc.attacker_stats_->disable) {
532 //the defenders attack disables the attakers attack: always choose this one.
533 return bc;
534 }
535 choices.emplace_back(std::move(bc));
536 }
537
538 if(choices.empty()) {
539 return no_weapon();
540 }
541
542 if(choices.size() == 1) {
543 //only one usable weapon, don't simulate
544 return std::move(choices[0]);
545 }
546
547 // Multiple options:
548 // First pass : get the best weight and the minimum simple rating for this weight.
549 // simple rating = number of blows * damage per blows (resistance taken in account) * cth * weight
550 // Eligible attacks for defense should have a simple rating greater or equal to this weight.
551
552 int min_rating = 0;
553 {
554 double max_weight = 0.0;
555
556 for(const auto& choice : choices) {
557 const double d_weight = choice.defender_stats_->weapon->defense_weight();
558
559 if(d_weight >= max_weight) {
560 const battle_context_unit_stats& def_stats = *choice.defender_stats_;
561
562 max_weight = d_weight;
563 int rating = static_cast<int>(
564 def_stats.num_blows * def_stats.damage * def_stats.chance_to_hit * d_weight);
565
566 if(d_weight > max_weight || rating < min_rating) {
567 min_rating = rating;
568 }
569 }
570 }
571 }
572
573 battle_context* best_choice = nullptr;
574 // Multiple options: simulate them, save best.
575 for(auto& choice : choices) {
576 const double d_weight = choice.defender_stats_->weapon->defense_weight();
577
578 choice.simulate(prev_def);
579
580
581 int simple_rating = static_cast<int>(
582 choice.defender_stats_->num_blows * choice.defender_stats_->damage * choice.defender_stats_->chance_to_hit * d_weight);
583
584 //FIXME: make sure there is no mostake in the better_combat call-
585 if(simple_rating >= min_rating && (!best_choice || choice.better_defense(*best_choice, 1.0))) {
586 best_choice = &choice;
587 }
588 }
589
590 return best_choice ? std::move(*best_choice) : no_weapon();
591}
592
593
594// ==================================================================================
595// HELPERS
596// ==================================================================================
597
598namespace
599{
600void refresh_weapon_index(int& weap_index, const std::string& weap_id, const attack_itors& attacks)
601{
602 // No attacks to choose from.
603 if(attacks.empty()) {
604 weap_index = -1;
605 return;
606 }
607
608 // The currently selected attack fits.
609 if(weap_index >= 0 && weap_index < static_cast<int>(attacks.size()) && attacks[weap_index].id() == weap_id) {
610 return;
611 }
612
613 // Look up the weapon by id.
614 if(!weap_id.empty()) {
615 for(int i = 0; i < static_cast<int>(attacks.size()); ++i) {
616 if(attacks[i].id() == weap_id) {
617 weap_index = i;
618 return;
619 }
620 }
621 }
622
623 // Lookup has failed.
624 weap_index = -1;
625 return;
626}
627
628/** Helper class for performing an attack. */
629class attack
630{
631public:
632 attack(const map_location& attacker,
633 const map_location& defender,
634 int attack_with,
635 int defend_with,
636 bool update_display = true);
637
638 void perform();
639
640private:
641 class attack_end_exception
642 {
643 };
644
645 bool perform_hit(bool, statistics_attack_context&);
646 void fire_event(const std::string& n);
647 void fire_event_impl(const std::string& n, bool reversed);
648 void refresh_bc();
649
650 /** Structure holding unit info used in the attack action. */
651 struct unit_info
652 {
653 const map_location loc_;
654 int weapon_;
655 unit_map& units_;
656 std::size_t id_; /**< unit.underlying_id() */
657 std::string weap_id_;
658 int orig_attacks_;
659 int n_attacks_; /**< Number of attacks left. */
660 int cth_;
661 int damage_;
662 int xp_;
663
664 unit_info(const map_location& loc, int weapon, unit_map& units);
665 unit& get_unit();
666 unit_ptr get_unit_ptr();
667 bool valid();
668
669 std::string dump();
670 };
671
672 /**
673 * Used in perform_hit to confirm a replay is in sync.
674 * Check OOS_error_ after this method, true if error detected.
675 */
676 void check_replay_attack_result(bool&, int, int&, config, unit_info&);
677
678 void unit_killed(
679 unit_info&, unit_info&, const battle_context_unit_stats*&, const battle_context_unit_stats*&, bool);
680
681 std::unique_ptr<battle_context> bc_;
682
683 const battle_context_unit_stats* a_stats_;
684 const battle_context_unit_stats* d_stats_;
685
686 int abs_n_attack_, abs_n_defend_;
687 // update_att_fog_ is not used, other than making some code simpler.
688 bool update_att_fog_, update_def_fog_, update_minimap_;
689
690 unit_info a_, d_;
691 unit_map& units_;
692 std::ostringstream errbuf_;
693
694 bool update_display_;
695 bool OOS_error_;
696
697 bool use_prng_;
698
699 struct hit_credits
700 {
701 int prev_cth;
702 int credit;
703 };
704 hit_credits bias_attacker_ = { -1 , 0};
705 hit_credits bias_defender_ = { -1 , 0};
706};
707
708attack::unit_info::unit_info(const map_location& loc, int weapon, unit_map& units)
709 : loc_(loc)
710 , weapon_(weapon)
711 , units_(units)
712 , id_()
713 , weap_id_()
714 , orig_attacks_(0)
715 , n_attacks_(0)
716 , cth_(0)
717 , damage_(0)
718 , xp_(0)
719{
720 unit_map::iterator i = units_.find(loc_);
721 if(!i.valid()) {
722 return;
723 }
724
725 id_ = i->underlying_id();
726}
727
728unit& attack::unit_info::get_unit()
729{
730 unit_map::iterator i = units_.find(loc_);
731 assert(i.valid() && i->underlying_id() == id_);
732 return *i;
733}
734
735unit_ptr attack::unit_info::get_unit_ptr()
736{
737 unit_map::iterator i = units_.find(loc_);
738 if(i.valid() && i->underlying_id() == id_) {
739 return i.get_shared_ptr();
740 }
741 return unit_ptr();
742}
743
744bool attack::unit_info::valid()
745{
746 unit_map::iterator i = units_.find(loc_);
747 return i.valid() && i->underlying_id() == id_;
748}
749
750std::string attack::unit_info::dump()
751{
752 std::stringstream s;
753 s << get_unit().type_id() << " (" << loc_.wml_x() << ',' << loc_.wml_y() << ')';
754 return s.str();
755}
756
757attack::attack(const map_location& attacker,
758 const map_location& defender,
759 int attack_with,
760 int defend_with,
761 bool update_display)
762 : bc_(nullptr)
763 , a_stats_(nullptr)
764 , d_stats_(nullptr)
765 , abs_n_attack_(0)
766 , abs_n_defend_(0)
767 , update_att_fog_(false)
768 , update_def_fog_(false)
769 , update_minimap_(false)
770 , a_(attacker, attack_with, resources::gameboard->units())
771 , d_(defender, defend_with, resources::gameboard->units())
772 , units_(resources::gameboard->units())
773 , errbuf_()
774 , update_display_(update_display)
775 , OOS_error_(false)
776
777 //new experimental prng mode.
778 , use_prng_(biased_rng::enabled())
779{
780 if(use_prng_) {
781 LOG_NG << "Using experimental PRNG for combat";
782 }
783}
784
785void attack::fire_event(const std::string& n)
786{
787 fire_event_impl(n, false);
788}
789
790void attack::fire_event_impl(const std::string& n, bool reverse)
791{
792 LOG_NG << "attack: firing '" << n << "' event";
793 // FIXME: this passes nullptr to the specials_context, i assume it's to avoid binding the context to the concrete c++ unit objects in case the unit gets replaced by wml
794 // But that doesn't really work, see the related comment in attack_type::special_active_impl
795
796 // prepare the event data for weapon filtering
797 config ev_data;
798 config& a_weapon_cfg = ev_data.add_child(reverse ? "second" : "first");
799 config& d_weapon_cfg = ev_data.add_child(reverse ? "first" : "second");
800
801 // Need these to ensure weapon filters work correctly
802 auto ctx = specials_context_t::make({ nullptr, a_.loc_, a_stats_->weapon }, { nullptr, d_.loc_, d_stats_->weapon }, true);
803
804 if(a_stats_->weapon != nullptr && a_.valid()) {
805 a_stats_->weapon->write(a_weapon_cfg);
806 }
807
808 if(d_stats_->weapon != nullptr && d_.valid()) {
809 d_stats_->weapon->write(d_weapon_cfg);
810 }
811
812 if(a_weapon_cfg["name"].empty()) {
813 a_weapon_cfg["name"] = "none";
814 }
815
816 if(d_weapon_cfg["name"].empty()) {
817 d_weapon_cfg["name"] = "none";
818 }
819
820 if(n == "attack_end") {
821 // We want to fire attack_end event in any case! Even if one of units was removed by WML.
822 resources::game_events->pump().fire(n, a_.loc_, d_.loc_, ev_data);
823 return;
824 }
825
826 // damage_inflicted is set in these events.
827 // TODO: should we set this value from unit_info::damage, or continue using the WML variable?
828 if(n == "attacker_hits" || n == "defender_hits" || n == "unit_hits") {
829 ev_data["damage_inflicted"] = resources::gamedata->get_variable("damage_inflicted");
830 }
831
832 const int defender_side = d_.get_unit().side();
833
834 bool wml_aborted;
835 std::tie(std::ignore, wml_aborted) = resources::game_events->pump().fire(n,
836 game_events::entity_location(reverse ? d_.loc_ : a_.loc_, reverse ? d_.id_ : a_.id_),
837 game_events::entity_location(reverse ? a_.loc_ : d_.loc_, reverse ? a_.id_ : d_.id_), ev_data);
838
839 // The event could have killed either the attacker or
840 // defender, so we have to make sure they still exist.
841 refresh_bc();
842
843 if(wml_aborted || !a_.valid() || !d_.valid()
844 || !resources::gameboard->get_team(a_.get_unit().side()).is_enemy(d_.get_unit().side())
845 ) {
846 actions::recalculate_fog(defender_side);
847
848 if(update_display_) {
850 }
851
852 fire_event("attack_end");
853 throw attack_end_exception();
854 }
855}
856
857void attack::refresh_bc()
858{
859 // Fix index of weapons.
860 if(a_.valid()) {
861 refresh_weapon_index(a_.weapon_, a_.weap_id_, a_.get_unit().attacks());
862 }
863
864 if(d_.valid()) {
865 refresh_weapon_index(d_.weapon_, d_.weap_id_, d_.get_unit().attacks());
866 }
867
868 if(!a_.valid() || !d_.valid()) {
869 // Fix pointer to weapons.
870 const_cast<battle_context_unit_stats*>(a_stats_)->weapon
871 = a_.valid() && a_.weapon_ >= 0 ? a_.get_unit().attacks()[a_.weapon_].shared_from_this() : nullptr;
872
873 const_cast<battle_context_unit_stats*>(d_stats_)->weapon
874 = d_.valid() && d_.weapon_ >= 0 ? d_.get_unit().attacks()[d_.weapon_].shared_from_this() : nullptr;
875
876 return;
877 }
878
879 bc_.reset(new battle_context(units_, a_.loc_, d_.loc_, a_.weapon_, d_.weapon_));
880
881 a_stats_ = &bc_->get_attacker_stats();
882 d_stats_ = &bc_->get_defender_stats();
883
884 a_.cth_ = a_stats_->chance_to_hit;
885 d_.cth_ = d_stats_->chance_to_hit;
886 a_.damage_ = a_stats_->damage;
887 d_.damage_ = d_stats_->damage;
888}
889
890bool attack::perform_hit(bool attacker_turn, statistics_attack_context& stats)
891{
892 unit_info& attacker = attacker_turn ? a_ : d_;
893 unit_info& defender = attacker_turn ? d_ : a_;
894
895 // NOTE: we need to use a reference-to-pointer here so a_stats_ and d_stats_ can be
896 // modified without. Using a pointer directly would render them invalid when that happened.
897 const battle_context_unit_stats*& attacker_stats = attacker_turn ? a_stats_ : d_stats_;
898 const battle_context_unit_stats*& defender_stats = attacker_turn ? d_stats_ : a_stats_;
899
900 int& abs_n = attacker_turn ? abs_n_attack_ : abs_n_defend_;
901 bool& update_fog = attacker_turn ? update_def_fog_ : update_att_fog_;
902
903 int ran_num;
904
905 if(use_prng_) {
906 if(attacker.cth_ == 0 || attacker.cth_ == 100) {
907 // if cth is 0/100 we never/always want to hit, even if bias would give us a chance to do so. We also don't want to modify bias
908 // TODO: should we call randomness::generator->get_random_int() anyways to stay in sync? (doing so would mean we are calling rng the same amount of times in the biased and defaultmode
909 ran_num = 50;
910 } else {
911 auto& bias = attacker_turn ? bias_attacker_ : bias_defender_;
912 if(bias.prev_cth != attacker.cth_) {
914 bias.prev_cth = attacker.cth_;
915 }
916 // attacker.n_attacks_ is the number of strikes left.
917 const bool does_hit = biased_rng::roll_hit(*randomness::generator, attacker.n_attacks_, attacker.cth_, bias.credit);
918 ran_num = does_hit ? 0 : 99;
919 }
920 } else {
921 ran_num = randomness::generator->get_random_int(0, 99);
922 }
923 bool hits = (ran_num < attacker.cth_);
924
925 int damage = 0;
926 if(hits) {
927 damage = attacker.damage_;
928 resources::gamedata->get_variable("damage_inflicted") = damage;
929 }
930
931 // Make sure that if we're serializing a game here,
932 // we got the same results as the game did originally.
933 const config local_results {"chance", attacker.cth_, "hits", hits, "damage", damage};
934
935 config replay_results;
936 bool equals_replay = checkup_instance->local_checkup(local_results, replay_results);
937
938 if(!equals_replay) {
939 check_replay_attack_result(hits, ran_num, damage, replay_results, attacker);
940 }
941
942 // can do no more damage than the defender has hitpoints
943 int damage_done = std::min<int>(defender.get_unit().hitpoints(), attacker.damage_);
944
945 // expected damage = damage potential * chance to hit (as a percentage)
946 double expected_damage = damage_done * attacker.cth_ * 0.01;
947
948 if(attacker_turn) {
949 stats.attack_expected_damage(expected_damage, 0);
950 } else {
951 stats.attack_expected_damage(0, expected_damage);
952 }
953
954 int drains_damage = 0;
955 if(hits && attacker_stats->drains) {
956 drains_damage = damage_done * attacker_stats->drain_percent / 100 + attacker_stats->drain_constant;
957
958 // don't drain so much that the attacker gets more than his maximum hitpoints
959 drains_damage =
960 std::min<int>(drains_damage, attacker.get_unit().max_hitpoints() - attacker.get_unit().hitpoints());
961
962 // if drain is negative, don't allow drain to kill the attacker
963 drains_damage = std::max<int>(drains_damage, 1 - attacker.get_unit().hitpoints());
964 }
965
966 if(update_display_) {
967 std::ostringstream float_text;
968 std::vector<std::string> extra_hit_sounds;
969
970 if(hits) {
971 const unit& defender_unit = defender.get_unit();
972 if(attacker_stats->poisons && !defender_unit.get_state(unit::STATE_POISONED)) {
973 float_text << (defender_unit.gender() == unit_race::FEMALE ? _("female^poisoned") : _("poisoned"))
974 << '\n';
975
976 extra_hit_sounds.push_back(game_config::sounds::status::poisoned);
977 }
978
979 if(attacker_stats->slows && !defender_unit.get_state(unit::STATE_SLOWED)) {
980 float_text << (defender_unit.gender() == unit_race::FEMALE ? _("female^slowed") : _("slowed")) << '\n';
981
982 extra_hit_sounds.push_back(game_config::sounds::status::slowed);
983 }
984
985 if(attacker_stats->petrifies) {
986 float_text << (defender_unit.gender() == unit_race::FEMALE ? _("female^petrified") : _("petrified"))
987 << '\n';
988
989 extra_hit_sounds.push_back(game_config::sounds::status::petrified);
990 }
991 } else {
992 if(prefs::get().show_attack_miss_indicator()) {
993 float_text << _("attack^miss");
994 }
995 }
996
1000 attacker.loc_, defender.loc_,
1001 damage,
1002 *attacker_stats->weapon, defender_stats->weapon,
1003 abs_n, float_text.str(), drains_damage, "",
1004 &extra_hit_sounds, attacker_turn
1005 );
1006 }
1007
1008 bool dies = defender.get_unit().take_hit(damage);
1009 LOG_NG << "defender took " << damage << (dies ? " and died\n" : "\n");
1010
1011 if(attacker_turn) {
1012 stats.attack_result(hits
1013 ? (dies
1016 : statistics_attack_context::MISSES,
1017 attacker.cth_, damage_done, drains_damage
1018 );
1019 } else {
1020 stats.defend_result(hits
1021 ? (dies
1024 : statistics_attack_context::MISSES,
1025 attacker.cth_, damage_done, drains_damage
1026 );
1027 }
1028
1029 replay_results.clear();
1030
1031 // There was also a attribute cfg["unit_hit"] which was never used so i deleted.
1032 equals_replay = checkup_instance->local_checkup(config{"dies", dies}, replay_results);
1033
1034 if(!equals_replay) {
1035 bool results_dies = replay_results["dies"].to_bool();
1036
1037 errbuf_ << "SYNC: In attack " << a_.dump() << " vs " << d_.dump() << ": the data source says the "
1038 << (attacker_turn ? "defender" : "attacker") << ' ' << (results_dies ? "perished" : "survived")
1039 << " while in-game calculations show it " << (dies ? "perished" : "survived")
1040 << " (over-riding game calculations with data source results)\n";
1041
1042 dies = results_dies;
1043
1044 // Set hitpoints to 0 so later checks don't invalidate the death.
1045 if(results_dies) {
1046 defender.get_unit().set_hitpoints(0);
1047 }
1048
1049 OOS_error_ = true;
1050 }
1051
1052 if(hits) {
1053 try {
1054 fire_event(attacker_turn ? "attacker_hits" : "defender_hits");
1055 fire_event_impl("unit_hits", !attacker_turn);
1056 } catch(const attack_end_exception&) {
1057 refresh_bc();
1058 return false;
1059 }
1060 } else {
1061 try {
1062 fire_event(attacker_turn ? "attacker_misses" : "defender_misses");
1063 fire_event_impl("unit_misses", !attacker_turn);
1064 } catch(const attack_end_exception&) {
1065 refresh_bc();
1066 return false;
1067 }
1068 }
1069
1070 refresh_bc();
1071
1072 bool attacker_dies = false;
1073
1074 if(drains_damage > 0) {
1075 attacker.get_unit().heal(drains_damage);
1076 } else if(drains_damage < 0) {
1077 attacker_dies = attacker.get_unit().take_hit(-drains_damage);
1078 }
1079
1080 if(dies) {
1081 unit_killed(attacker, defender, attacker_stats, defender_stats, false);
1082 update_fog = true;
1083 }
1084
1085 if(attacker_dies) {
1086 unit_killed(defender, attacker, defender_stats, attacker_stats, true);
1087 (attacker_turn ? update_att_fog_ : update_def_fog_) = true;
1088 }
1089
1090 if(dies) {
1091 update_minimap_ = true;
1092 return false;
1093 }
1094
1095 if(hits) {
1096 unit& defender_unit = defender.get_unit();
1097
1098 if(attacker_stats->poisons && !defender_unit.get_state(unit::STATE_POISONED)) {
1099 defender_unit.set_state(unit::STATE_POISONED, true);
1100 LOG_NG << "defender poisoned";
1101 }
1102
1103 if(attacker_stats->slows && !defender_unit.get_state(unit::STATE_SLOWED)) {
1104 defender_unit.set_state(unit::STATE_SLOWED, true);
1105 update_fog = true;
1106 defender.damage_ = defender_stats->slow_damage;
1107 LOG_NG << "defender slowed";
1108 }
1109
1110 // If the defender is petrified, the fight stops immediately
1111 if(attacker_stats->petrifies) {
1112 defender_unit.set_state(unit::STATE_PETRIFIED, true);
1113 update_fog = true;
1114 attacker.n_attacks_ = 0;
1115 defender.n_attacks_ = -1; // Petrified.
1116 resources::game_events->pump().fire("petrified", defender.loc_, attacker.loc_);
1117 refresh_bc();
1118 }
1119 }
1120
1121 // Delay until here so that poison and slow go through
1122 if(attacker_dies) {
1123 update_minimap_ = true;
1124 return false;
1125 }
1126
1127 --attacker.n_attacks_;
1128
1129 // If an event removed a unit's weapon, set number of remaining attacks to zero
1130 // for that unit, but let the other unit continue
1131 if (attacker_stats->weapon == nullptr){
1132 attacker.n_attacks_ = 0;
1133 attacker.orig_attacks_ = 0;
1134 }
1135 if (defender_stats->weapon == nullptr){
1136 defender.n_attacks_ = 0;
1137 defender.orig_attacks_ = 0;
1138 }
1139
1140 return true;
1141}
1142
1143void attack::unit_killed(unit_info& attacker,
1144 unit_info& defender,
1145 const battle_context_unit_stats*& attacker_stats,
1146 const battle_context_unit_stats*& defender_stats,
1147 bool drain_killed)
1148{
1149 attacker.xp_ = game_config::kill_xp(defender.get_unit().level());
1150 defender.xp_ = 0;
1151
1152 display::get_singleton()->invalidate(attacker.loc_);
1153
1154 game_events::entity_location death_loc(defender.loc_, defender.id_);
1155 game_events::entity_location attacker_loc(attacker.loc_, attacker.id_);
1156
1157 std::string undead_variation = defender.get_unit().undead_variation();
1158
1159 fire_event("attack_end");
1160 refresh_bc();
1161
1162 // Get weapon info for last_breath and die events.
1163 config dat;
1164 config a_weapon_cfg = attacker_stats->weapon && attacker.valid() ? attacker_stats->weapon->to_config() : config();
1165 config d_weapon_cfg = defender_stats->weapon && defender.valid() ? defender_stats->weapon->to_config() : config();
1166
1167 if(a_weapon_cfg["name"].empty()) {
1168 a_weapon_cfg["name"] = "none";
1169 }
1170
1171 if(d_weapon_cfg["name"].empty()) {
1172 d_weapon_cfg["name"] = "none";
1173 }
1174
1175 dat.add_child("first", d_weapon_cfg);
1176 dat.add_child("second", a_weapon_cfg);
1177
1178 resources::game_events->pump().fire("last_breath", death_loc, attacker_loc, dat);
1179 refresh_bc();
1180
1181 // WML has invalidated the dying unit, abort.
1182 if(!defender.valid() || defender.get_unit().hitpoints() > 0) {
1183 return;
1184 }
1185
1186 if(!attacker.valid()) {
1188 defender.loc_,
1189 defender.get_unit(),
1190 nullptr,
1191 defender_stats->weapon
1192 );
1193 } else {
1195 defender.loc_,
1196 defender.get_unit(),
1197 attacker_stats->weapon,
1198 defender_stats->weapon,
1199 attacker.loc_,
1200 attacker.get_unit_ptr()
1201 );
1202 }
1203
1204 resources::game_events->pump().fire("die", death_loc, attacker_loc, dat);
1205 refresh_bc();
1206
1207 if(!defender.valid() || defender.get_unit().hitpoints() > 0) {
1208 // WML has invalidated the dying unit, abort
1209 return;
1210 }
1211
1212 units_.erase(defender.loc_);
1213 resources::whiteboard->on_kill_unit();
1214
1215 // Plague units make new units on the target hex.
1216 if(attacker.valid() && attacker_stats->plagues && !drain_killed) {
1217 LOG_NG << "trying to reanimate " << attacker_stats->plague_type;
1218
1219 if(const unit_type* reanimator = unit_types.find(attacker_stats->plague_type)) {
1220 LOG_NG << "found unit type:" << reanimator->id();
1221
1222 unit_ptr newunit = unit::create(*reanimator, attacker.get_unit().side(), true, unit_race::MALE);
1223 newunit->set_attacks(0);
1224 newunit->set_movement(0, true);
1225 newunit->set_facing(map_location::get_opposite_direction(attacker.get_unit().facing()));
1226
1227 // Apply variation
1228 if(undead_variation != "null") {
1229 config mod;
1230 config& variation = mod.add_child("effect");
1231 variation["apply_to"] = "variation";
1232 variation["name"] = undead_variation;
1233 newunit->add_modification("variation", mod);
1234 newunit->heal_fully();
1235 }
1236
1237 newunit->set_location(death_loc);
1238 units_.insert(newunit);
1239
1240 game_events::entity_location reanim_loc(defender.loc_, newunit->underlying_id());
1241 resources::game_events->pump().fire("unit_placed", reanim_loc);
1242
1243 prefs::get().encountered_units().insert(newunit->type_id());
1244
1245 if(update_display_) {
1246 display::get_singleton()->invalidate(death_loc);
1247 }
1248 }
1249 } else {
1250 LOG_NG << "unit not reanimated";
1251 }
1252}
1253
1254void attack::perform()
1255{
1256 // Stop the user from issuing any commands while the units are fighting.
1257 const events::command_disabler disable_commands;
1258
1259 if(!a_.valid() || !d_.valid()) {
1260 return;
1261 }
1262
1263 // no attack weapon => stop here and don't attack
1264 if(a_.weapon_ < 0) {
1265 a_.get_unit().set_attacks(a_.get_unit().attacks_left() - 1);
1266 a_.get_unit().set_movement(-1, true);
1267 return;
1268 }
1269
1270 if(a_.get_unit().attacks_left() <= 0) {
1271 LOG_NG << "attack::perform(): not enough ap.";
1272 return;
1273 }
1274
1275 bc_.reset(new battle_context(units_, a_.loc_, d_.loc_, a_.weapon_, d_.weapon_));
1276
1277 a_stats_ = &bc_->get_attacker_stats();
1278 d_stats_ = &bc_->get_defender_stats();
1279
1280 if(a_stats_->weapon) {
1281 a_.weap_id_ = a_stats_->weapon->id();
1282 }
1283
1284 if(d_stats_->weapon) {
1285 d_.weap_id_ = d_stats_->weapon->id();
1286 }
1287
1288 a_.get_unit().set_facing(a_.loc_.get_relative_dir(d_.loc_));
1289 d_.get_unit().set_facing(d_.loc_.get_relative_dir(a_.loc_));
1290
1291 try {
1292 fire_event("pre_attack");
1293 } catch(const attack_end_exception&) {
1294 return;
1295 }
1296
1297 VALIDATE(a_.weapon_ < static_cast<int>(a_.get_unit().attacks().size()),
1298 _("An invalid attacker weapon got selected."));
1299
1300 a_.get_unit().set_attacks(a_.get_unit().attacks_left() - a_.get_unit().attacks()[a_.weapon_].attacks_used());
1301 a_.get_unit().set_movement(a_.get_unit().movement_left() - a_.get_unit().attacks()[a_.weapon_].movement_used(), true);
1302 a_.get_unit().set_state(unit::STATE_NOT_MOVED, false);
1303 a_.get_unit().set_resting(false);
1304 d_.get_unit().set_resting(false);
1305
1306 // If the attacker was invisible, she isn't anymore!
1307 a_.get_unit().set_state(unit::STATE_UNCOVERED, true);
1308
1309 if(a_stats_->disable) {
1310 LOG_NG << "attack::perform(): tried to attack with a disabled attack.";
1311 return;
1312 }
1313
1314 try {
1315 fire_event("attack");
1316 } catch(const attack_end_exception&) {
1317 return;
1318 }
1319
1320 DBG_NG << "getting attack statistics";
1321 statistics_attack_context attack_stats(resources::controller->statistics(),
1322 a_.get_unit(), d_.get_unit(), a_stats_->chance_to_hit, d_stats_->chance_to_hit);
1323
1324 a_.orig_attacks_ = a_stats_->num_blows;
1325 d_.orig_attacks_ = d_stats_->num_blows;
1326 a_.n_attacks_ = a_.orig_attacks_;
1327 d_.n_attacks_ = d_.orig_attacks_;
1328 a_.xp_ = game_config::combat_xp(d_.get_unit().level());
1329 d_.xp_ = game_config::combat_xp(a_.get_unit().level());
1330
1331 bool defender_strikes_first = (d_stats_->firststrike && !a_stats_->firststrike);
1332 unsigned int rounds = std::max<unsigned int>(a_stats_->rounds, d_stats_->rounds) - 1;
1333 const int defender_side = d_.get_unit().side();
1334
1335 LOG_NG << "Fight: (" << a_.loc_ << ") vs (" << d_.loc_ << ") ATT: " << a_stats_->weapon->name() << " "
1336 << a_stats_->damage << "-" << a_stats_->num_blows << "(" << a_stats_->chance_to_hit
1337 << "%) vs DEF: " << (d_stats_->weapon ? d_stats_->weapon->name() : "none") << " " << d_stats_->damage << "-"
1338 << d_stats_->num_blows << "(" << d_stats_->chance_to_hit << "%)"
1339 << (defender_strikes_first ? " defender first-strike" : "");
1340
1341 // Play the pre-fight animation
1342 unit_display::unit_draw_weapon(a_.loc_, a_.get_unit(), a_stats_->weapon, d_stats_->weapon, d_.loc_, d_.get_unit_ptr());
1343
1344 while(true) {
1345 DBG_NG << "start of attack loop...";
1346 ++abs_n_attack_;
1347
1348 if(a_.n_attacks_ > 0 && !defender_strikes_first) {
1349 if(!perform_hit(true, attack_stats)) {
1350 DBG_NG << "broke from attack loop on attacker turn";
1351 break;
1352 }
1353 }
1354
1355 // If the defender got to strike first, they use it up here.
1356 defender_strikes_first = false;
1357 ++abs_n_defend_;
1358
1359 if(d_.n_attacks_ > 0) {
1360 if(!perform_hit(false, attack_stats)) {
1361 DBG_NG << "broke from attack loop on defender turn";
1362 break;
1363 }
1364 }
1365
1366 // Continue the fight to death; if one of the units got petrified,
1367 // either n_attacks or n_defends is -1
1368 if(rounds > 0 && d_.n_attacks_ == 0 && a_.n_attacks_ == 0) {
1369 a_.n_attacks_ = a_.orig_attacks_;
1370 d_.n_attacks_ = d_.orig_attacks_;
1371 --rounds;
1372 defender_strikes_first = (d_stats_->firststrike && !a_stats_->firststrike);
1373 }
1374
1375 if(a_.n_attacks_ <= 0 && d_.n_attacks_ <= 0) {
1376 fire_event("attack_end");
1377 refresh_bc();
1378 break;
1379 }
1380 }
1381
1382 // Set by attacker_hits and defender_hits and unit_hits events.
1383 resources::gamedata->clear_variable("damage_inflicted");
1384
1385 if(update_def_fog_) {
1386 actions::recalculate_fog(defender_side);
1387 }
1388
1389 // TODO: if we knew the viewing team, we could skip this display update
1390 if(update_minimap_ && update_display_) {
1392 }
1393
1394 if(a_.valid()) {
1395 unit& u = a_.get_unit();
1396 u.anim_comp().set_standing();
1397 u.set_experience(u.experience() + a_.xp_);
1398 }
1399
1400 if(d_.valid()) {
1401 unit& u = d_.get_unit();
1402 u.anim_comp().set_standing();
1403 u.set_experience(u.experience() + d_.xp_);
1404 }
1405
1406 unit_display::unit_sheath_weapon(a_.loc_, a_.get_unit_ptr(), a_stats_->weapon, d_stats_->weapon,
1407 d_.loc_, d_.get_unit_ptr());
1408
1409 if(update_display_) {
1413 }
1414
1415 if(OOS_error_) {
1416 replay::process_error(errbuf_.str());
1417 }
1418}
1419
1420void attack::check_replay_attack_result(
1421 bool& hits, int ran_num, int& damage, config replay_results, unit_info& attacker)
1422{
1423 int results_chance = replay_results["chance"].to_int();
1424 bool results_hits = replay_results["hits"].to_bool();
1425 int results_damage = replay_results["damage"].to_int();
1426
1427#if 0
1428 errbuf_ << "SYNC: In attack " << a_.dump() << " vs " << d_.dump()
1429 << " replay data differs from local calculated data:"
1430 << " chance to hit in data source: " << results_chance
1431 << " chance to hit in calculated: " << attacker.cth_
1432 << " chance to hit in data source: " << results_chance
1433 << " chance to hit in calculated: " << attacker.cth_
1434 ;
1435
1436 attacker.cth_ = results_chance;
1437 hits = results_hits;
1438 damage = results_damage;
1439
1440 OOS_error_ = true;
1441#endif
1442
1443 if(results_chance != attacker.cth_) {
1444 errbuf_ << "SYNC: In attack " << a_.dump() << " vs " << d_.dump()
1445 << ": chance to hit is inconsistent. Data source: " << results_chance
1446 << "; Calculation: " << attacker.cth_ << " (over-riding game calculations with data source results)\n";
1447 attacker.cth_ = results_chance;
1448 OOS_error_ = true;
1449 }
1450
1451 if(results_hits != hits) {
1452 errbuf_ << "SYNC: In attack " << a_.dump() << " vs " << d_.dump() << ": the data source says the hit was "
1453 << (results_hits ? "successful" : "unsuccessful") << ", while in-game calculations say the hit was "
1454 << (hits ? "successful" : "unsuccessful") << " random number: " << ran_num << " = " << (ran_num % 100)
1455 << "/" << results_chance << " (over-riding game calculations with data source results)\n";
1456 hits = results_hits;
1457 OOS_error_ = true;
1458 }
1459
1460 if(results_damage != damage) {
1461 errbuf_ << "SYNC: In attack " << a_.dump() << " vs " << d_.dump() << ": the data source says the hit did "
1462 << results_damage << " damage, while in-game calculations show the hit doing " << damage
1463 << " damage (over-riding game calculations with data source results)\n";
1464 damage = results_damage;
1465 OOS_error_ = true;
1466 }
1467}
1468} // end anonymous namespace
1469
1470
1471// ==================================================================================
1472// FREE-STANDING FUNCTIONS
1473// ==================================================================================
1474
1475void attack_unit(const map_location& attacker,
1476 const map_location& defender,
1477 int attack_with,
1478 int defend_with,
1479 bool update_display)
1480{
1481 attack dummy(attacker, defender, attack_with, defend_with, update_display);
1482 dummy.perform();
1483}
1484
1486 const map_location& defender,
1487 int attack_with,
1488 int defend_with,
1489 bool update_display)
1490{
1491 attack_unit(attacker, defender, attack_with, defend_with, update_display);
1492
1494 if(atku != resources::gameboard->units().end()) {
1496 }
1497
1499 if(defu != resources::gameboard->units().end()) {
1501 }
1502}
1503
1505{
1506 active_ability_list abil = u.get_abilities("leadership", loc);
1507 unit_abilities::effect leader_effect(abil, 0, nullptr, unit_abilities::EFFECT_CUMULABLE);
1508 return leader_effect.get_composite_value();
1509}
1510
1511int under_leadership(const unit& u, const specials_context_t& context)
1512{
1513 active_ability_list abil = context.get_abilities_weapons("leadership", u);
1514 unit_abilities::effect leader_effect(abil, 0, nullptr, unit_abilities::EFFECT_CUMULABLE);
1515 return leader_effect.get_composite_value();
1516}
1517
1518int combat_modifier(const unit_map& units,
1519 const gamemap& map,
1520 const map_location& loc,
1521 unit_alignments::type alignment,
1522 bool is_fearless)
1523{
1524 const tod_manager& tod_m = *resources::tod_manager;
1525 const time_of_day& effective_tod = tod_m.get_illuminated_time_of_day(units, map, loc);
1526 return combat_modifier(effective_tod, alignment, is_fearless);
1527}
1528
1529int combat_modifier(const time_of_day& effective_tod,
1530 unit_alignments::type alignment,
1531 bool is_fearless)
1532{
1533 const tod_manager& tod_m = *resources::tod_manager;
1534 const int lawful_bonus = effective_tod.lawful_bonus;
1535 return generic_combat_modifier(lawful_bonus, alignment, is_fearless, tod_m.get_max_liminal_bonus());
1536}
1537
1538int generic_combat_modifier(int lawful_bonus, unit_alignments::type alignment, bool is_fearless, int max_liminal_bonus)
1539{
1540 int bonus;
1541
1542 switch(alignment) {
1543 case unit_alignments::type::lawful:
1544 bonus = lawful_bonus;
1545 break;
1546 case unit_alignments::type::neutral:
1547 bonus = 0;
1548 break;
1549 case unit_alignments::type::chaotic:
1550 bonus = -lawful_bonus;
1551 break;
1552 case unit_alignments::type::liminal:
1553 bonus = max_liminal_bonus-std::abs(lawful_bonus);
1554 break;
1555 default:
1556 bonus = 0;
1557 }
1558
1559 if(is_fearless) {
1560 bonus = std::max<int>(bonus, 0);
1561 }
1562
1563 return bonus;
1564}
1565
1566bool backstab_check(const map_location& attacker_loc,
1567 const map_location& defender_loc,
1568 const unit_map& units,
1569 const std::vector<team>& teams)
1570{
1571 const unit_map::const_iterator defender = units.find(defender_loc);
1572 if(defender == units.end()) {
1573 return false; // No defender
1574 }
1575
1576 const auto adj = get_adjacent_tiles(defender_loc);
1577 unsigned i;
1578
1579 for(i = 0; i < adj.size(); ++i) {
1580 if(adj[i] == attacker_loc) {
1581 break;
1582 }
1583 }
1584
1585 if(i >= 6) {
1586 return false; // Attack not from adjacent location
1587 }
1588
1589 const unit_map::const_iterator opp = units.find(adj[(i + 3) % 6]);
1590
1591 // No opposite unit.
1592 if(opp == units.end()) {
1593 return false;
1594 }
1595
1596 if(opp->incapacitated()) {
1597 return false;
1598 }
1599
1600 // If sides aren't valid teams, then they are enemies.
1601 if(std::size_t(defender->side() - 1) >= teams.size() || std::size_t(opp->side() - 1) >= teams.size()) {
1602 return true;
1603 }
1604
1605 // Defender and opposite are enemies.
1606 if(teams[defender->side() - 1].is_enemy(opp->side())) {
1607 return true;
1608 }
1609
1610 // Defender and opposite are friends.
1611 return false;
1612}
int generic_combat_modifier(int lawful_bonus, unit_alignments::type alignment, bool is_fearless, int max_liminal_bonus)
Returns the amount that a unit's damage should be multiplied by due to a given lawful_bonus.
Definition attack.cpp:1538
static lg::log_domain log_engine("engine")
bool backstab_check(const map_location &attacker_loc, const map_location &defender_loc, const unit_map &units, const std::vector< team > &teams)
Function to check if an attack will satisfy the requirements for backstab.
Definition attack.cpp:1566
int combat_modifier(const unit_map &units, const gamemap &map, const map_location &loc, unit_alignments::type alignment, bool is_fearless)
Returns the amount that a unit's damage should be multiplied by due to the current time of day.
Definition attack.cpp:1518
int under_leadership(const unit &u, const map_location &loc)
Tests if the unit at loc is currently affected by leadership.
Definition attack.cpp:1504
void attack_unit_and_advance(const map_location &attacker, const map_location &defender, int attack_with, int defend_with, bool update_display)
Performs an attack, and advanced the units afterwards.
Definition attack.cpp:1485
#define DBG_NG
Definition attack.cpp:58
#define LOG_NG
Definition attack.cpp:59
void attack_unit(const map_location &attacker, const map_location &defender, int attack_with, int defend_with, bool update_display)
Performs an attack.
Definition attack.cpp:1475
static lg::log_domain log_attack("engine/attack")
static lg::log_domain log_config("config")
Various functions that implement attacks and attack calculations.
int generic_combat_modifier(int lawful_bonus, unit_alignments::type alignment, bool is_fearless, int max_liminal_bonus)
Returns the amount that a unit's damage should be multiplied by due to a given lawful_bonus.
Definition attack.cpp:1538
int combat_modifier(const unit_map &units, const gamemap &map, const map_location &loc, unit_alignments::type alignment, bool is_fearless)
Returns the amount that a unit's damage should be multiplied by due to the current time of day.
Definition attack.cpp:1518
int under_leadership(const unit &u, const map_location &loc)
Tests if the unit at loc is currently affected by leadership.
Definition attack.cpp:1504
map_location loc
Definition move.cpp:172
void advance_unit_at(const advance_unit_params &params)
#define LOG_CF
#define LOG_NG
Various functions that implement advancements of units.
static auto & dummy
boost::iterator_range< boost::indirect_iterator< attack_list::iterator > > attack_itors
active_ability & front()
bool empty() const
double defense_weight() const
double attack_weight() const
const std::string & range() const
Computes the statistics of a battle between an attacker and a defender unit.
Definition attack.hpp:183
std::unique_ptr< battle_context_unit_stats > defender_stats_
Definition attack.hpp:266
std::unique_ptr< combatant > defender_combatant_
Definition attack.hpp:270
std::unique_ptr< battle_context_unit_stats > attacker_stats_
Statistics of the units.
Definition attack.hpp:265
std::unique_ptr< combatant > attacker_combatant_
Outcome of simulated fight.
Definition attack.hpp:269
void simulate(const combatant *prev_def)
Definition attack.cpp:291
static battle_context choose_defender_weapon(nonempty_unit_const_ptr attacker, nonempty_unit_const_ptr defender, unsigned attacker_weapon, const map_location &attacker_loc, const map_location &defender_loc, const combatant *prev_def, bool use_biased_rng)
Definition attack.cpp:502
static battle_context choose_attacker_weapon(nonempty_unit_const_ptr attacker, const nonempty_unit_const_ptr &defender, const map_location &attacker_loc, const map_location &defender_loc, double harm_weight, const combatant *prev_def, bool use_biased_rng)
Definition attack.cpp:447
battle_context(const unit_map &units, const map_location &attacker_loc, const map_location &defender_loc, int attacker_weapon=-1, int defender_weapon=-1, double aggression=0.0, const combatant *prev_def=nullptr, unit_const_ptr attacker_ptr=unit_const_ptr(), unit_const_ptr defender_ptr=unit_const_ptr(), bool use_biased_rng=false)
If no attacker_weapon is given, we select the best one, based on harm_weight (1.0 means 1 hp lost cou...
Definition attack.cpp:304
const combatant & get_attacker_combatant(const combatant *prev_def=nullptr)
Get the simulation results.
Definition attack.cpp:361
const combatant & get_defender_combatant(const combatant *prev_def=nullptr)
Definition attack.cpp:368
static bool better_combat(const combatant &us_a, const combatant &them_a, const combatant &us_b, const combatant &them_b, double harm_weight)
Definition attack.cpp:400
bool better_defense(class battle_context &that, double harm_weight)
Given this harm_weight, is this attack better than that?
Definition attack.cpp:388
bool better_attack(class battle_context &that, double harm_weight)
Given this harm_weight, is this attack better than that?
Definition attack.cpp:376
bool use_biased_rng_
Definition attack.hpp:272
virtual bool local_checkup(const config &expected_data, config &real_data)=0
Compares data to the results calculated during the original game.
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
void clear()
Definition config.cpp:802
void redraw_minimap()
Schedule the minimap to be redrawn.
Definition display.cpp:1486
bool invalidate(const map_location &loc)
Function to invalidate a specific tile for redrawing.
Definition display.cpp:2927
static display * get_singleton()
Returns the display object if a display object exists.
Definition display.hpp:102
virtual const gamemap & map() const override
team & get_team(int i)
virtual const unit_map & units() const override
void clear_variable(const std::string &varname)
Clears attributes config children does nothing if varname is no valid variable name.
config::attribute_value & get_variable(const std::string &varname)
throws invalid_variablename_exception if varname is no valid variable name.
Definition game_data.cpp:66
static game_display * get_singleton()
void invalidate_unit()
Function to invalidate that unit status displayed on the sidebar.
game_events::wml_event_pump & pump()
Definition manager.cpp:257
pump_result_t fire(const std::string &event, const entity_location &loc1=entity_location::null_entity, const entity_location &loc2=entity_location::null_entity, const config &data=config())
Function to fire an event.
Definition pump.cpp:399
Encapsulates the map of the game.
Definition map.hpp:176
bool is_village(const map_location &loc) const
Definition map.cpp:60
static prefs & get()
std::set< std::string > & encountered_units()
this class does not give synced random results derived classes might do.
Definition random.hpp:28
virtual bool is_networked() const
Is this random source networked? If it is it's very important we do actually use this random source t...
Definition random.hpp:89
int get_random_int(int min, int max)
Definition random.hpp:51
static void process_error(const std::string &msg)
Definition replay.cpp:183
active_ability_list get_abilities_weapons(const std::string &tag, const unit &un) const
static specials_context_t make(specials_combatant &&self, specials_combatant &&other, bool attacking)
bool is_enemy(int n) const
Definition team.hpp:270
int get_max_liminal_bonus() const
time_of_day get_illuminated_time_of_day(const unit_map &units, const gamemap &map, const map_location &loc, int for_turn=0) const
Returns time of day object for the passed turn at a location.
int get_composite_value() const
void set_standing(bool with_bars=true)
Sets the animation state to standing.
Container associating units to locations.
Definition map.hpp:98
unit_iterator end()
Definition map.hpp:428
unit_iterator find(std::size_t id)
Definition map.cpp:302
@ FEMALE
Definition race.hpp:28
const unit_type * find(const std::string &key, unit_type::BUILD_STATUS status=unit_type::FULL) const
Finds a unit_type by its id() and makes sure it is built to the specified level.
Definition types.cpp:1253
A single unit type that the player may recruit.
Definition types.hpp:43
const std::string & parent_id() const
The id of the original type from which this (variation) descended.
Definition types.hpp:149
This class represents a single unit of a specific type.
Definition unit.hpp:39
static unit_ptr create(const config &cfg, bool use_traits=false, const vconfig *vcfg=nullptr)
Initializes a unit from a config.
Definition unit.hpp:107
variant a_
Definition function.cpp:821
static std::string _(const char *str)
Definition gettext.hpp:100
active_ability_list get_abilities(const std::string &tag_name, const map_location &loc) const
Gets the unit's active abilities of a particular type if it were on a specified location.
int max_hitpoints() const
The max number of hitpoints this unit can have.
Definition unit.hpp:427
unit_alignments::type alignment() const
The alignment of this unit.
Definition unit.hpp:397
void set_state(const std::string &state, bool value)
Set whether the unit is affected by a status effect.
Definition unit.cpp:1505
const unit_type & type() const
This unit's type, accounting for gender and variation.
Definition unit.hpp:261
const std::string & undead_variation() const
Definition unit.hpp:505
int hitpoints() const
The current number of hitpoints this unit has.
Definition unit.hpp:421
bool get_state(const std::string &state) const
Check if the unit is affected by a status effect.
Definition unit.cpp:1446
int experience() const
The current number of experience points this unit has.
Definition unit.hpp:445
void set_experience(int xp)
Sets the current experience point amount.
Definition unit.hpp:475
unit_race::GENDER gender() const
The gender of this unit.
Definition unit.hpp:387
@ STATE_SLOWED
Definition unit.hpp:775
@ STATE_NOT_MOVED
The unit is uncovered - it was hiding but has been spotted.
Definition unit.hpp:779
@ STATE_PETRIFIED
The unit is poisoned - it loses health each turn.
Definition unit.hpp:777
@ STATE_POISONED
The unit is slowed - it moves slower and does less damage.
Definition unit.hpp:776
@ STATE_UNCOVERED
The unit is petrified - it cannot move or be attacked.
Definition unit.hpp:778
bool advances() const
Checks whether this unit is eligible for level-up.
Definition unit.hpp:189
attack_itors attacks()
Gets an iterator over this unit's attacks.
Definition unit.hpp:848
int defense_modifier(const t_translation::terrain_code &terrain, const map_location &loc) const
The unit's defense on a given terrain.
Definition unit.cpp:1777
unit_animation_component & anim_comp() const
Definition unit.hpp:1537
bool is_fearless() const
Gets whether this unit is fearless - ie, unaffected by time of day.
Definition unit.hpp:1186
std::size_t distance_between(const map_location &a, const map_location &b)
Function which gives the number of hexes between two tiles (i.e.
Definition location.cpp:584
void get_adjacent_tiles(const map_location &a, utils::span< map_location, 6 > res)
Function which, given a location, will place all adjacent locations in res.
Definition location.cpp:513
Standard logging facilities (interface).
#define log_scope2(domain, description)
Definition log.hpp:276
constexpr int round_damage(double base_damage, int bonus, int divisor)
round (base_damage * bonus / divisor) to the closest integer, but up or down towards base_damage
Definition math.hpp:78
void recalculate_fog(int side)
Function that recalculates the fog of war.
Definition vision.cpp:697
int roll_credit(randomness::rng &rng)
Rolls the credit a combatant starts a fight with.
Definition attack.cpp:83
bool enabled()
Returns true if the current game uses the biased random mode with a local RNG.
Definition attack.cpp:78
bool roll_hit(randomness::rng &rng, int strikes_left, int chance_to_hit, int &credit)
Rolls one strike.
Definition attack.cpp:88
int kill_xp(int level)
int combat_xp(int level)
Domain specific events.
bool fire_event(const ui_event event, const std::vector< std::pair< widget *, ui_event > > &event_chain, widget *dispatcher, widget *w, F &&... params)
Helper function for fire_event.
rng * generator
This generator is automatically synced during synced context.
Definition random.cpp:60
::tod_manager * tod_manager
Definition resources.cpp:29
game_board * gameboard
Definition resources.cpp:20
game_data * gamedata
Definition resources.cpp:22
game_events::manager * game_events
Definition resources.cpp:24
game_classification * classification
Definition resources.cpp:34
play_controller * controller
Definition resources.cpp:21
std::shared_ptr< wb::manager > whiteboard
Definition resources.cpp:33
void unit_die(const map_location &loc, unit &loser, const const_attack_ptr &attack, const const_attack_ptr &secondary_attack, const map_location &winner_loc, const unit_ptr &winner)
Show a unit fading out.
Definition udisplay.cpp:571
void unit_draw_weapon(const map_location &loc, unit &attacker, const const_attack_ptr &attack, const const_attack_ptr &secondary_attack, const map_location &defender_loc, const unit_ptr &defender)
Play a pre-fight animation First unit is the attacker, second unit the defender.
Definition udisplay.cpp:521
void unit_attack(display *disp, game_board &board, const map_location &a, const map_location &b, int damage, const attack_type &attack, const const_attack_ptr &secondary_attack, int swing, const std::string &hit_text, int drain_amount, const std::string &att_text, const std::vector< std::string > *extra_hit_sounds, bool attacking)
Make the unit on tile 'a' attack the unit on tile 'b'.
Definition udisplay.cpp:598
void unit_sheath_weapon(const map_location &primary_loc, const unit_ptr &primary_unit, const const_attack_ptr &primary_attack, const const_attack_ptr &secondary_attack, const map_location &secondary_loc, const unit_ptr &secondary_unit)
Play a post-fight animation Both unit can be set to null, only valid units will play their animation.
Definition udisplay.cpp:541
constexpr auto reverse
Definition ranges.hpp:44
std::shared_ptr< const unit > unit_const_ptr
Definition ptr.hpp:27
std::shared_ptr< const attack_type > const_attack_ptr
Definition ptr.hpp:34
std::shared_ptr< unit > unit_ptr
Definition ptr.hpp:26
Define the game's event mechanism.
Replay control code.
const config & ability_cfg() const
advances the unit at loc if it has enough experience, maximum 20 times.
Structure describing the statistics of a unit involved in the battle.
Definition attack.hpp:72
bool slows
Attack slows opponent when it hits.
Definition attack.hpp:78
unsigned int num_blows
Effective number of blows, takes swarm into account.
Definition attack.hpp:99
std::string plague_type
The plague type used by the attack, if any.
Definition attack.hpp:103
bool petrifies
Attack petrifies opponent when it hits.
Definition attack.hpp:80
int drain_percent
Percentage of damage recovered as health.
Definition attack.hpp:97
unsigned int hp
Hitpoints of the unit at the beginning of the battle.
Definition attack.hpp:92
int slow_damage
Effective damage if unit becomes slowed (== damage, if already slowed)
Definition attack.hpp:96
bool drains
Attack drains opponent when it hits.
Definition attack.hpp:79
unsigned int swarm_min
Minimum number of blows with swarm (equal to num_blows if swarm isn't used).
Definition attack.hpp:100
bool swarm
Attack has swarm special.
Definition attack.hpp:83
const_attack_ptr weapon
The weapon used by the unit to attack the opponent, or nullptr if there is none.
Definition attack.hpp:73
bool is_slowed
True if the unit is slowed at the beginning of the battle.
Definition attack.hpp:77
unsigned int rounds
Berserk special can force us to fight more than one round.
Definition attack.hpp:91
unsigned int swarm_max
Maximum number of blows with swarm (equal to num_blows if swarm isn't used).
Definition attack.hpp:101
battle_context_unit_stats(nonempty_unit_const_ptr u, const map_location &u_loc, int u_attack_num, bool attacking, nonempty_unit_const_ptr opp, const map_location &opp_loc, const const_attack_ptr &opp_weapon, utils::optional< int > opp_terrain_defense={}, utils::optional< int > lawful_bonus={})
Definition attack.cpp:104
unsigned int calc_blows(unsigned new_hp) const
Calculates the number of blows we would have if we had new_hp instead of the recorded hp.
Definition attack.hpp:121
int damage
Effective damage of the weapon (all factors accounted for).
Definition attack.hpp:95
bool can_advance
The unit can advance.
Definition attack.hpp:86
bool disable
Attack has disable special.
Definition attack.hpp:85
bool poisons
Attack poisons opponent when it hits.
Definition attack.hpp:82
unsigned int chance_to_hit
Effective chance to hit as a percentage (all factors accounted for).
Definition attack.hpp:94
int drain_constant
Base HP drained regardless of damage dealt.
Definition attack.hpp:98
bool firststrike
Attack has firststrike special.
Definition attack.hpp:84
int attack_num
Index into unit->attacks() or -1 for none.
Definition attack.hpp:74
bool plagues
Attack turns opponent into a zombie when fatal.
Definition attack.hpp:81
All combat-related info.
std::vector< double > hp_dist
Resulting probability distribution (might be not as large as max_hp)
double poisoned
Resulting chance we are poisoned.
const battle_context_unit_stats & u_
double average_hp(unsigned int healing=0) const
What's the average hp (weighted average of hp_dist).
Encapsulates the map of the game.
Definition location.hpp:46
static constexpr direction get_opposite_direction(direction d)
Definition location.hpp:76
void attack_result(hit_result res, int cth, int damage, int drain)
void attack_expected_damage(double attacker_inflict, double defender_inflict)
void defend_result(hit_result res, int cth, int damage, int drain)
Object which defines a time of day with associated bonuses, image, sounds etc.
int lawful_bonus
The % bonus lawful units receive.
bool valid() const
Definition map.hpp:273
pointer get_shared_ptr() const
This is exactly the same as operator-> but it's slightly more readable, and can replace &*iter syntax...
Definition map.hpp:217
checkup * checkup_instance
static map_location::direction n
static map_location::direction s
unit_type_data unit_types
Definition types.cpp:1499
Display units performing various actions: moving, attacking, and dying.
Various functions implementing vision (through fog of war and shroud).
Add a special kind of assert to validate whether the input from WML doesn't contain any problems that...
#define VALIDATE(cond, message)
The macro to use for the validation of WML.
#define i
#define b