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