The Battle for Wesnoth  1.19.20+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  const auto attacks = attacker->attacks();
426  for(std::size_t i = 0; i < attacks.size(); ++i) {
427  const attack_type& att = attacks[i];
428 
429  if(att.attack_weight() <= 0) {
430  continue;
431  }
432  battle_context bc = choose_defender_weapon(attacker, defender, i, attacker_loc, defender_loc, prev_def);
433  //choose_defender_weapon will always choose the weapon that disabels the attackers weapon if possible.
434  if(bc.attacker_stats_->disable) {
435  continue;
436  }
437  choices.emplace_back(std::move(bc));
438  }
439 
440  if(choices.empty()) {
441  return battle_context(attacker, attacker_loc, -1, defender, defender_loc, -1);
442  }
443 
444  if(choices.size() == 1) {
445  return std::move(choices[0]);
446  }
447 
448  // Multiple options: simulate them, save best.
449  battle_context* best_choice = nullptr;
450  for(auto& choice : choices) {
451  // If choose_defender_weapon didn't simulate, do so now.
452  choice.simulate(prev_def);
453 
454  if(!best_choice || choice.better_attack(*best_choice, harm_weight)) {
455  best_choice = &choice;
456  }
457  }
458 
459  if(best_choice) {
460  return std::move(*best_choice);
461  }
462  else {
463  return battle_context(attacker, attacker_loc, -1, defender, defender_loc, -1);
464  }
465 }
466 
467 /** @todo FIXME: Hand previous defender unit in here. */
469  nonempty_unit_const_ptr defender,
470  unsigned attacker_weapon,
471  const map_location& attacker_loc,
472  const map_location& defender_loc,
473  const combatant* prev_def)
474 {
475  log_scope2(log_attack, "choose_defender_weapon");
476  const auto attackers_attacks = attacker->attacks();
477  VALIDATE(attacker_weapon < attackers_attacks.size(), _("An invalid attacker weapon got selected."));
478 
479  const attack_type& att = attackers_attacks[attacker_weapon];
480  auto no_weapon = [&]() { return battle_context(attacker, attacker_loc, attacker_weapon, defender, defender_loc, -1); };
481  std::vector<battle_context> choices;
482 
483  // What options does defender have?
484  const auto defenses = defender->attacks();
485  for(std::size_t i = 0; i < defenses.size(); ++i) {
486  const attack_type& def = defenses[i];
487  if(def.range() != att.range() || def.defense_weight() <= 0) {
488  //no need to calculate the battle_context here.
489  continue;
490  }
491  battle_context bc(attacker, attacker_loc, attacker_weapon, defender, defender_loc, i);
492 
493  if(bc.defender_stats_->disable) {
494  continue;
495  }
496  if(bc.attacker_stats_->disable) {
497  //the defenders attack disables the attakers attack: always choose this one.
498  return bc;
499  }
500  choices.emplace_back(std::move(bc));
501  }
502 
503  if(choices.empty()) {
504  return no_weapon();
505  }
506 
507  if(choices.size() == 1) {
508  //only one usable weapon, don't simulate
509  return std::move(choices[0]);
510  }
511 
512  // Multiple options:
513  // First pass : get the best weight and the minimum simple rating for this weight.
514  // simple rating = number of blows * damage per blows (resistance taken in account) * cth * weight
515  // Eligible attacks for defense should have a simple rating greater or equal to this weight.
516 
517  int min_rating = 0;
518  {
519  double max_weight = 0.0;
520 
521  for(const auto& choice : choices) {
522  const double d_weight = choice.defender_stats_->weapon->defense_weight();
523 
524  if(d_weight >= max_weight) {
525  const battle_context_unit_stats& def_stats = *choice.defender_stats_;
526 
527  max_weight = d_weight;
528  int rating = static_cast<int>(
529  def_stats.num_blows * def_stats.damage * def_stats.chance_to_hit * d_weight);
530 
531  if(d_weight > max_weight || rating < min_rating) {
532  min_rating = rating;
533  }
534  }
535  }
536  }
537 
538  battle_context* best_choice = nullptr;
539  // Multiple options: simulate them, save best.
540  for(auto& choice : choices) {
541  const double d_weight = choice.defender_stats_->weapon->defense_weight();
542 
543  choice.simulate(prev_def);
544 
545 
546  int simple_rating = static_cast<int>(
547  choice.defender_stats_->num_blows * choice.defender_stats_->damage * choice.defender_stats_->chance_to_hit * d_weight);
548 
549  //FIXME: make sure there is no mostake in the better_combat call-
550  if(simple_rating >= min_rating && (!best_choice || choice.better_defense(*best_choice, 1.0))) {
551  best_choice = &choice;
552  }
553  }
554 
555  return best_choice ? std::move(*best_choice) : no_weapon();
556 }
557 
558 
559 // ==================================================================================
560 // HELPERS
561 // ==================================================================================
562 
563 namespace
564 {
565 void refresh_weapon_index(int& weap_index, const std::string& weap_id, const attack_itors& attacks)
566 {
567  // No attacks to choose from.
568  if(attacks.empty()) {
569  weap_index = -1;
570  return;
571  }
572 
573  // The currently selected attack fits.
574  if(weap_index >= 0 && weap_index < static_cast<int>(attacks.size()) && attacks[weap_index].id() == weap_id) {
575  return;
576  }
577 
578  // Look up the weapon by id.
579  if(!weap_id.empty()) {
580  for(int i = 0; i < static_cast<int>(attacks.size()); ++i) {
581  if(attacks[i].id() == weap_id) {
582  weap_index = i;
583  return;
584  }
585  }
586  }
587 
588  // Lookup has failed.
589  weap_index = -1;
590  return;
591 }
592 
593 /** Helper class for performing an attack. */
594 class attack
595 {
596 public:
597  attack(const map_location& attacker,
598  const map_location& defender,
599  int attack_with,
600  int defend_with,
601  bool update_display = true);
602 
603  void perform();
604 
605 private:
606  class attack_end_exception
607  {
608  };
609 
610  bool perform_hit(bool, statistics_attack_context&);
611  void fire_event(const std::string& n);
612  void fire_event_impl(const std::string& n, bool reversed);
613  void refresh_bc();
614 
615  /** Structure holding unit info used in the attack action. */
616  struct unit_info
617  {
618  const map_location loc_;
619  int weapon_;
620  unit_map& units_;
621  std::size_t id_; /**< unit.underlying_id() */
622  std::string weap_id_;
623  int orig_attacks_;
624  int n_attacks_; /**< Number of attacks left. */
625  int cth_;
626  int damage_;
627  int xp_;
628 
629  unit_info(const map_location& loc, int weapon, unit_map& units);
630  unit& get_unit();
631  unit_ptr get_unit_ptr();
632  bool valid();
633 
634  std::string dump();
635  };
636 
637  /**
638  * Used in perform_hit to confirm a replay is in sync.
639  * Check OOS_error_ after this method, true if error detected.
640  */
641  void check_replay_attack_result(bool&, int, int&, config, unit_info&);
642 
643  void unit_killed(
644  unit_info&, unit_info&, const battle_context_unit_stats*&, const battle_context_unit_stats*&, bool);
645 
646  std::unique_ptr<battle_context> bc_;
647 
648  const battle_context_unit_stats* a_stats_;
649  const battle_context_unit_stats* d_stats_;
650 
651  int abs_n_attack_, abs_n_defend_;
652  // update_att_fog_ is not used, other than making some code simpler.
653  bool update_att_fog_, update_def_fog_, update_minimap_;
654 
655  unit_info a_, d_;
656  unit_map& units_;
657  std::ostringstream errbuf_;
658 
659  bool update_display_;
660  bool OOS_error_;
661 
662  bool use_prng_;
663 
664  std::vector<bool> prng_attacker_;
665  std::vector<bool> prng_defender_;
666 };
667 
668 attack::unit_info::unit_info(const map_location& loc, int weapon, unit_map& units)
669  : loc_(loc)
670  , weapon_(weapon)
671  , units_(units)
672  , id_()
673  , weap_id_()
674  , orig_attacks_(0)
675  , n_attacks_(0)
676  , cth_(0)
677  , damage_(0)
678  , xp_(0)
679 {
680  unit_map::iterator i = units_.find(loc_);
681  if(!i.valid()) {
682  return;
683  }
684 
685  id_ = i->underlying_id();
686 }
687 
688 unit& attack::unit_info::get_unit()
689 {
690  unit_map::iterator i = units_.find(loc_);
691  assert(i.valid() && i->underlying_id() == id_);
692  return *i;
693 }
694 
695 unit_ptr attack::unit_info::get_unit_ptr()
696 {
697  unit_map::iterator i = units_.find(loc_);
698  if(i.valid() && i->underlying_id() == id_) {
699  return i.get_shared_ptr();
700  }
701  return unit_ptr();
702 }
703 
704 bool attack::unit_info::valid()
705 {
706  unit_map::iterator i = units_.find(loc_);
707  return i.valid() && i->underlying_id() == id_;
708 }
709 
710 std::string attack::unit_info::dump()
711 {
712  std::stringstream s;
713  s << get_unit().type_id() << " (" << loc_.wml_x() << ',' << loc_.wml_y() << ')';
714  return s.str();
715 }
716 
717 attack::attack(const map_location& attacker,
718  const map_location& defender,
719  int attack_with,
720  int defend_with,
721  bool update_display)
722  : bc_(nullptr)
723  , a_stats_(nullptr)
724  , d_stats_(nullptr)
725  , abs_n_attack_(0)
726  , abs_n_defend_(0)
727  , update_att_fog_(false)
728  , update_def_fog_(false)
729  , update_minimap_(false)
730  , a_(attacker, attack_with, resources::gameboard->units())
731  , d_(defender, defend_with, resources::gameboard->units())
732  , units_(resources::gameboard->units())
733  , errbuf_()
734  , update_display_(update_display)
735  , OOS_error_(false)
736 
737  //new experimental prng mode.
738  , use_prng_(resources::classification->random_mode == "biased" && randomness::generator->is_networked() == false)
739  , prng_attacker_()
740  , prng_defender_()
741 {
742  if(use_prng_) {
743  LOG_NG << "Using experimental PRNG for combat";
744  }
745 }
746 
747 void attack::fire_event(const std::string& n)
748 {
749  fire_event_impl(n, false);
750 }
751 
752 void attack::fire_event_impl(const std::string& n, bool reverse)
753 {
754  LOG_NG << "attack: firing '" << n << "' event";
755  // 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
756  // But that doesn't really work, see the related comment in attack_type::special_active_impl
757 
758  // prepare the event data for weapon filtering
759  config ev_data;
760  config& a_weapon_cfg = ev_data.add_child(reverse ? "second" : "first");
761  config& d_weapon_cfg = ev_data.add_child(reverse ? "first" : "second");
762 
763  // Need these to ensure weapon filters work correctly
764  auto ctx = specials_context_t::make({ nullptr, a_.loc_, a_stats_->weapon }, { nullptr, d_.loc_, d_stats_->weapon }, true);
765 
766  if(a_stats_->weapon != nullptr && a_.valid()) {
767  a_stats_->weapon->write(a_weapon_cfg);
768  }
769 
770  if(d_stats_->weapon != nullptr && d_.valid()) {
771  d_stats_->weapon->write(d_weapon_cfg);
772  }
773 
774  if(a_weapon_cfg["name"].empty()) {
775  a_weapon_cfg["name"] = "none";
776  }
777 
778  if(d_weapon_cfg["name"].empty()) {
779  d_weapon_cfg["name"] = "none";
780  }
781 
782  if(n == "attack_end") {
783  // We want to fire attack_end event in any case! Even if one of units was removed by WML.
784  resources::game_events->pump().fire(n, a_.loc_, d_.loc_, ev_data);
785  return;
786  }
787 
788  // damage_inflicted is set in these events.
789  // TODO: should we set this value from unit_info::damage, or continue using the WML variable?
790  if(n == "attacker_hits" || n == "defender_hits" || n == "unit_hits") {
791  ev_data["damage_inflicted"] = resources::gamedata->get_variable("damage_inflicted");
792  }
793 
794  const int defender_side = d_.get_unit().side();
795 
796  bool wml_aborted;
797  std::tie(std::ignore, wml_aborted) = resources::game_events->pump().fire(n,
798  game_events::entity_location(reverse ? d_.loc_ : a_.loc_, reverse ? d_.id_ : a_.id_),
799  game_events::entity_location(reverse ? a_.loc_ : d_.loc_, reverse ? a_.id_ : d_.id_), ev_data);
800 
801  // The event could have killed either the attacker or
802  // defender, so we have to make sure they still exist.
803  refresh_bc();
804 
805  if(wml_aborted || !a_.valid() || !d_.valid()
806  || !resources::gameboard->get_team(a_.get_unit().side()).is_enemy(d_.get_unit().side())
807  ) {
808  actions::recalculate_fog(defender_side);
809 
810  if(update_display_) {
812  }
813 
814  fire_event("attack_end");
815  throw attack_end_exception();
816  }
817 }
818 
819 void attack::refresh_bc()
820 {
821  // Fix index of weapons.
822  if(a_.valid()) {
823  refresh_weapon_index(a_.weapon_, a_.weap_id_, a_.get_unit().attacks());
824  }
825 
826  if(d_.valid()) {
827  refresh_weapon_index(d_.weapon_, d_.weap_id_, d_.get_unit().attacks());
828  }
829 
830  if(!a_.valid() || !d_.valid()) {
831  // Fix pointer to weapons.
832  const_cast<battle_context_unit_stats*>(a_stats_)->weapon
833  = a_.valid() && a_.weapon_ >= 0 ? a_.get_unit().attacks()[a_.weapon_].shared_from_this() : nullptr;
834 
835  const_cast<battle_context_unit_stats*>(d_stats_)->weapon
836  = d_.valid() && d_.weapon_ >= 0 ? d_.get_unit().attacks()[d_.weapon_].shared_from_this() : nullptr;
837 
838  return;
839  }
840 
841  bc_.reset(new battle_context(units_, a_.loc_, d_.loc_, a_.weapon_, d_.weapon_));
842 
843  a_stats_ = &bc_->get_attacker_stats();
844  d_stats_ = &bc_->get_defender_stats();
845 
846  a_.cth_ = a_stats_->chance_to_hit;
847  d_.cth_ = d_stats_->chance_to_hit;
848  a_.damage_ = a_stats_->damage;
849  d_.damage_ = d_stats_->damage;
850 }
851 
852 bool attack::perform_hit(bool attacker_turn, statistics_attack_context& stats)
853 {
854  unit_info& attacker = attacker_turn ? a_ : d_;
855  unit_info& defender = attacker_turn ? d_ : a_;
856 
857  // NOTE: we need to use a reference-to-pointer here so a_stats_ and d_stats_ can be
858  // modified without. Using a pointer directly would render them invalid when that happened.
859  const battle_context_unit_stats*& attacker_stats = attacker_turn ? a_stats_ : d_stats_;
860  const battle_context_unit_stats*& defender_stats = attacker_turn ? d_stats_ : a_stats_;
861 
862  int& abs_n = attacker_turn ? abs_n_attack_ : abs_n_defend_;
863  bool& update_fog = attacker_turn ? update_def_fog_ : update_att_fog_;
864 
865  int ran_num;
866 
867  if(use_prng_) {
868 
869  std::vector<bool>& prng_seq = attacker_turn ? prng_attacker_ : prng_defender_;
870 
871  if(prng_seq.empty()) {
872  const int ntotal = attacker.cth_*attacker.n_attacks_;
873  int num_hits = ntotal/100;
874  const int additional_hit_chance = ntotal%100;
875  if(additional_hit_chance > 0 && randomness::generator->get_random_int(0, 99) < additional_hit_chance) {
876  ++num_hits;
877  }
878 
879  std::vector<int> indexes;
880  for(int i = 0; i != attacker.n_attacks_; ++i) {
881  prng_seq.push_back(false);
882  indexes.push_back(i);
883  }
884 
885  for(int i = 0; i != num_hits; ++i) {
886  int n = randomness::generator->get_random_int(0, static_cast<int>(indexes.size())-1);
887  prng_seq[indexes[n]] = true;
888  indexes.erase(indexes.begin() + n);
889  }
890  }
891 
892  bool does_hit = prng_seq.back();
893  prng_seq.pop_back();
894  ran_num = does_hit ? 0 : 99;
895  } else {
896  ran_num = randomness::generator->get_random_int(0, 99);
897  }
898  bool hits = (ran_num < attacker.cth_);
899 
900  int damage = 0;
901  if(hits) {
902  damage = attacker.damage_;
903  resources::gamedata->get_variable("damage_inflicted") = damage;
904  }
905 
906  // Make sure that if we're serializing a game here,
907  // we got the same results as the game did originally.
908  const config local_results {"chance", attacker.cth_, "hits", hits, "damage", damage};
909 
910  config replay_results;
911  bool equals_replay = checkup_instance->local_checkup(local_results, replay_results);
912 
913  if(!equals_replay) {
914  check_replay_attack_result(hits, ran_num, damage, replay_results, attacker);
915  }
916 
917  // can do no more damage than the defender has hitpoints
918  int damage_done = std::min<int>(defender.get_unit().hitpoints(), attacker.damage_);
919 
920  // expected damage = damage potential * chance to hit (as a percentage)
921  double expected_damage = damage_done * attacker.cth_ * 0.01;
922 
923  if(attacker_turn) {
924  stats.attack_expected_damage(expected_damage, 0);
925  } else {
926  stats.attack_expected_damage(0, expected_damage);
927  }
928 
929  int drains_damage = 0;
930  if(hits && attacker_stats->drains) {
931  drains_damage = damage_done * attacker_stats->drain_percent / 100 + attacker_stats->drain_constant;
932 
933  // don't drain so much that the attacker gets more than his maximum hitpoints
934  drains_damage =
935  std::min<int>(drains_damage, attacker.get_unit().max_hitpoints() - attacker.get_unit().hitpoints());
936 
937  // if drain is negative, don't allow drain to kill the attacker
938  drains_damage = std::max<int>(drains_damage, 1 - attacker.get_unit().hitpoints());
939  }
940 
941  if(update_display_) {
942  std::ostringstream float_text;
943  std::vector<std::string> extra_hit_sounds;
944 
945  if(hits) {
946  const unit& defender_unit = defender.get_unit();
947  if(attacker_stats->poisons && !defender_unit.get_state(unit::STATE_POISONED)) {
948  float_text << (defender_unit.gender() == unit_race::FEMALE ? _("female^poisoned") : _("poisoned"))
949  << '\n';
950 
951  extra_hit_sounds.push_back(game_config::sounds::status::poisoned);
952  }
953 
954  if(attacker_stats->slows && !defender_unit.get_state(unit::STATE_SLOWED)) {
955  float_text << (defender_unit.gender() == unit_race::FEMALE ? _("female^slowed") : _("slowed")) << '\n';
956 
957  extra_hit_sounds.push_back(game_config::sounds::status::slowed);
958  }
959 
960  if(attacker_stats->petrifies) {
961  float_text << (defender_unit.gender() == unit_race::FEMALE ? _("female^petrified") : _("petrified"))
962  << '\n';
963 
964  extra_hit_sounds.push_back(game_config::sounds::status::petrified);
965  }
966  } else {
967  if(prefs::get().show_attack_miss_indicator()) {
968  float_text << _("attack^miss");
969  }
970  }
971 
975  attacker.loc_, defender.loc_,
976  damage,
977  *attacker_stats->weapon, defender_stats->weapon,
978  abs_n, float_text.str(), drains_damage, "",
979  &extra_hit_sounds, attacker_turn
980  );
981  }
982 
983  bool dies = defender.get_unit().take_hit(damage);
984  LOG_NG << "defender took " << damage << (dies ? " and died\n" : "\n");
985 
986  if(attacker_turn) {
987  stats.attack_result(hits
988  ? (dies
992  attacker.cth_, damage_done, drains_damage
993  );
994  } else {
995  stats.defend_result(hits
996  ? (dies
1000  attacker.cth_, damage_done, drains_damage
1001  );
1002  }
1003 
1004  replay_results.clear();
1005 
1006  // There was also a attribute cfg["unit_hit"] which was never used so i deleted.
1007  equals_replay = checkup_instance->local_checkup(config{"dies", dies}, replay_results);
1008 
1009  if(!equals_replay) {
1010  bool results_dies = replay_results["dies"].to_bool();
1011 
1012  errbuf_ << "SYNC: In attack " << a_.dump() << " vs " << d_.dump() << ": the data source says the "
1013  << (attacker_turn ? "defender" : "attacker") << ' ' << (results_dies ? "perished" : "survived")
1014  << " while in-game calculations show it " << (dies ? "perished" : "survived")
1015  << " (over-riding game calculations with data source results)\n";
1016 
1017  dies = results_dies;
1018 
1019  // Set hitpoints to 0 so later checks don't invalidate the death.
1020  if(results_dies) {
1021  defender.get_unit().set_hitpoints(0);
1022  }
1023 
1024  OOS_error_ = true;
1025  }
1026 
1027  if(hits) {
1028  try {
1029  fire_event(attacker_turn ? "attacker_hits" : "defender_hits");
1030  fire_event_impl("unit_hits", !attacker_turn);
1031  } catch(const attack_end_exception&) {
1032  refresh_bc();
1033  return false;
1034  }
1035  } else {
1036  try {
1037  fire_event(attacker_turn ? "attacker_misses" : "defender_misses");
1038  fire_event_impl("unit_misses", !attacker_turn);
1039  } catch(const attack_end_exception&) {
1040  refresh_bc();
1041  return false;
1042  }
1043  }
1044 
1045  refresh_bc();
1046 
1047  bool attacker_dies = false;
1048 
1049  if(drains_damage > 0) {
1050  attacker.get_unit().heal(drains_damage);
1051  } else if(drains_damage < 0) {
1052  attacker_dies = attacker.get_unit().take_hit(-drains_damage);
1053  }
1054 
1055  if(dies) {
1056  unit_killed(attacker, defender, attacker_stats, defender_stats, false);
1057  update_fog = true;
1058  }
1059 
1060  if(attacker_dies) {
1061  unit_killed(defender, attacker, defender_stats, attacker_stats, true);
1062  (attacker_turn ? update_att_fog_ : update_def_fog_) = true;
1063  }
1064 
1065  if(dies) {
1066  update_minimap_ = true;
1067  return false;
1068  }
1069 
1070  if(hits) {
1071  unit& defender_unit = defender.get_unit();
1072 
1073  if(attacker_stats->poisons && !defender_unit.get_state(unit::STATE_POISONED)) {
1074  defender_unit.set_state(unit::STATE_POISONED, true);
1075  LOG_NG << "defender poisoned";
1076  }
1077 
1078  if(attacker_stats->slows && !defender_unit.get_state(unit::STATE_SLOWED)) {
1079  defender_unit.set_state(unit::STATE_SLOWED, true);
1080  update_fog = true;
1081  defender.damage_ = defender_stats->slow_damage;
1082  LOG_NG << "defender slowed";
1083  }
1084 
1085  // If the defender is petrified, the fight stops immediately
1086  if(attacker_stats->petrifies) {
1087  defender_unit.set_state(unit::STATE_PETRIFIED, true);
1088  update_fog = true;
1089  attacker.n_attacks_ = 0;
1090  defender.n_attacks_ = -1; // Petrified.
1091  resources::game_events->pump().fire("petrified", defender.loc_, attacker.loc_);
1092  refresh_bc();
1093  }
1094  }
1095 
1096  // Delay until here so that poison and slow go through
1097  if(attacker_dies) {
1098  update_minimap_ = true;
1099  return false;
1100  }
1101 
1102  --attacker.n_attacks_;
1103 
1104  // If an event removed a unit's weapon, set number of remaining attacks to zero
1105  // for that unit, but let the other unit continue
1106  if (attacker_stats->weapon == nullptr){
1107  attacker.n_attacks_ = 0;
1108  attacker.orig_attacks_ = 0;
1109  }
1110  if (defender_stats->weapon == nullptr){
1111  defender.n_attacks_ = 0;
1112  defender.orig_attacks_ = 0;
1113  }
1114 
1115  return true;
1116 }
1117 
1118 void attack::unit_killed(unit_info& attacker,
1119  unit_info& defender,
1120  const battle_context_unit_stats*& attacker_stats,
1121  const battle_context_unit_stats*& defender_stats,
1122  bool drain_killed)
1123 {
1124  attacker.xp_ = game_config::kill_xp(defender.get_unit().level());
1125  defender.xp_ = 0;
1126 
1127  display::get_singleton()->invalidate(attacker.loc_);
1128 
1129  game_events::entity_location death_loc(defender.loc_, defender.id_);
1130  game_events::entity_location attacker_loc(attacker.loc_, attacker.id_);
1131 
1132  std::string undead_variation = defender.get_unit().undead_variation();
1133 
1134  fire_event("attack_end");
1135  refresh_bc();
1136 
1137  // Get weapon info for last_breath and die events.
1138  config dat;
1139  config a_weapon_cfg = attacker_stats->weapon && attacker.valid() ? attacker_stats->weapon->to_config() : config();
1140  config d_weapon_cfg = defender_stats->weapon && defender.valid() ? defender_stats->weapon->to_config() : config();
1141 
1142  if(a_weapon_cfg["name"].empty()) {
1143  a_weapon_cfg["name"] = "none";
1144  }
1145 
1146  if(d_weapon_cfg["name"].empty()) {
1147  d_weapon_cfg["name"] = "none";
1148  }
1149 
1150  dat.add_child("first", d_weapon_cfg);
1151  dat.add_child("second", a_weapon_cfg);
1152 
1153  resources::game_events->pump().fire("last_breath", death_loc, attacker_loc, dat);
1154  refresh_bc();
1155 
1156  // WML has invalidated the dying unit, abort.
1157  if(!defender.valid() || defender.get_unit().hitpoints() > 0) {
1158  return;
1159  }
1160 
1161  if(!attacker.valid()) {
1163  defender.loc_,
1164  defender.get_unit(),
1165  nullptr,
1166  defender_stats->weapon
1167  );
1168  } else {
1170  defender.loc_,
1171  defender.get_unit(),
1172  attacker_stats->weapon,
1173  defender_stats->weapon,
1174  attacker.loc_,
1175  attacker.get_unit_ptr()
1176  );
1177  }
1178 
1179  resources::game_events->pump().fire("die", death_loc, attacker_loc, dat);
1180  refresh_bc();
1181 
1182  if(!defender.valid() || defender.get_unit().hitpoints() > 0) {
1183  // WML has invalidated the dying unit, abort
1184  return;
1185  }
1186 
1187  units_.erase(defender.loc_);
1188  resources::whiteboard->on_kill_unit();
1189 
1190  // Plague units make new units on the target hex.
1191  if(attacker.valid() && attacker_stats->plagues && !drain_killed) {
1192  LOG_NG << "trying to reanimate " << attacker_stats->plague_type;
1193 
1194  if(const unit_type* reanimator = unit_types.find(attacker_stats->plague_type)) {
1195  LOG_NG << "found unit type:" << reanimator->id();
1196 
1197  unit_ptr newunit = unit::create(*reanimator, attacker.get_unit().side(), true, unit_race::MALE);
1198  newunit->set_attacks(0);
1199  newunit->set_movement(0, true);
1200  newunit->set_facing(map_location::get_opposite_direction(attacker.get_unit().facing()));
1201 
1202  // Apply variation
1203  if(undead_variation != "null") {
1204  config mod;
1205  config& variation = mod.add_child("effect");
1206  variation["apply_to"] = "variation";
1207  variation["name"] = undead_variation;
1208  newunit->add_modification("variation", mod);
1209  newunit->heal_fully();
1210  }
1211 
1212  newunit->set_location(death_loc);
1213  units_.insert(newunit);
1214 
1215  game_events::entity_location reanim_loc(defender.loc_, newunit->underlying_id());
1216  resources::game_events->pump().fire("unit_placed", reanim_loc);
1217 
1218  prefs::get().encountered_units().insert(newunit->type_id());
1219 
1220  if(update_display_) {
1221  display::get_singleton()->invalidate(death_loc);
1222  }
1223  }
1224  } else {
1225  LOG_NG << "unit not reanimated";
1226  }
1227 }
1228 
1229 void attack::perform()
1230 {
1231  // Stop the user from issuing any commands while the units are fighting.
1232  const events::command_disabler disable_commands;
1233 
1234  if(!a_.valid() || !d_.valid()) {
1235  return;
1236  }
1237 
1238  // no attack weapon => stop here and don't attack
1239  if(a_.weapon_ < 0) {
1240  a_.get_unit().set_attacks(a_.get_unit().attacks_left() - 1);
1241  a_.get_unit().set_movement(-1, true);
1242  return;
1243  }
1244 
1245  if(a_.get_unit().attacks_left() <= 0) {
1246  LOG_NG << "attack::perform(): not enough ap.";
1247  return;
1248  }
1249 
1250  bc_.reset(new battle_context(units_, a_.loc_, d_.loc_, a_.weapon_, d_.weapon_));
1251 
1252  a_stats_ = &bc_->get_attacker_stats();
1253  d_stats_ = &bc_->get_defender_stats();
1254 
1255  if(a_stats_->weapon) {
1256  a_.weap_id_ = a_stats_->weapon->id();
1257  }
1258 
1259  if(d_stats_->weapon) {
1260  d_.weap_id_ = d_stats_->weapon->id();
1261  }
1262 
1263  a_.get_unit().set_facing(a_.loc_.get_relative_dir(d_.loc_));
1264  d_.get_unit().set_facing(d_.loc_.get_relative_dir(a_.loc_));
1265 
1266  try {
1267  fire_event("pre_attack");
1268  } catch(const attack_end_exception&) {
1269  return;
1270  }
1271 
1272  VALIDATE(a_.weapon_ < static_cast<int>(a_.get_unit().attacks().size()),
1273  _("An invalid attacker weapon got selected."));
1274 
1275  a_.get_unit().set_attacks(a_.get_unit().attacks_left() - a_.get_unit().attacks()[a_.weapon_].attacks_used());
1276  a_.get_unit().set_movement(a_.get_unit().movement_left() - a_.get_unit().attacks()[a_.weapon_].movement_used(), true);
1277  a_.get_unit().set_state(unit::STATE_NOT_MOVED, false);
1278  a_.get_unit().set_resting(false);
1279  d_.get_unit().set_resting(false);
1280 
1281  // If the attacker was invisible, she isn't anymore!
1282  a_.get_unit().set_state(unit::STATE_UNCOVERED, true);
1283 
1284  if(a_stats_->disable) {
1285  LOG_NG << "attack::perform(): tried to attack with a disabled attack.";
1286  return;
1287  }
1288 
1289  try {
1290  fire_event("attack");
1291  } catch(const attack_end_exception&) {
1292  return;
1293  }
1294 
1295  DBG_NG << "getting attack statistics";
1296  statistics_attack_context attack_stats(resources::controller->statistics(),
1297  a_.get_unit(), d_.get_unit(), a_stats_->chance_to_hit, d_stats_->chance_to_hit);
1298 
1299  a_.orig_attacks_ = a_stats_->num_blows;
1300  d_.orig_attacks_ = d_stats_->num_blows;
1301  a_.n_attacks_ = a_.orig_attacks_;
1302  d_.n_attacks_ = d_.orig_attacks_;
1303  a_.xp_ = game_config::combat_xp(d_.get_unit().level());
1304  d_.xp_ = game_config::combat_xp(a_.get_unit().level());
1305 
1306  bool defender_strikes_first = (d_stats_->firststrike && !a_stats_->firststrike);
1307  unsigned int rounds = std::max<unsigned int>(a_stats_->rounds, d_stats_->rounds) - 1;
1308  const int defender_side = d_.get_unit().side();
1309 
1310  LOG_NG << "Fight: (" << a_.loc_ << ") vs (" << d_.loc_ << ") ATT: " << a_stats_->weapon->name() << " "
1311  << a_stats_->damage << "-" << a_stats_->num_blows << "(" << a_stats_->chance_to_hit
1312  << "%) vs DEF: " << (d_stats_->weapon ? d_stats_->weapon->name() : "none") << " " << d_stats_->damage << "-"
1313  << d_stats_->num_blows << "(" << d_stats_->chance_to_hit << "%)"
1314  << (defender_strikes_first ? " defender first-strike" : "");
1315 
1316  // Play the pre-fight animation
1317  unit_display::unit_draw_weapon(a_.loc_, a_.get_unit(), a_stats_->weapon, d_stats_->weapon, d_.loc_, d_.get_unit_ptr());
1318 
1319  while(true) {
1320  DBG_NG << "start of attack loop...";
1321  ++abs_n_attack_;
1322 
1323  if(a_.n_attacks_ > 0 && !defender_strikes_first) {
1324  if(!perform_hit(true, attack_stats)) {
1325  DBG_NG << "broke from attack loop on attacker turn";
1326  break;
1327  }
1328  }
1329 
1330  // If the defender got to strike first, they use it up here.
1331  defender_strikes_first = false;
1332  ++abs_n_defend_;
1333 
1334  if(d_.n_attacks_ > 0) {
1335  if(!perform_hit(false, attack_stats)) {
1336  DBG_NG << "broke from attack loop on defender turn";
1337  break;
1338  }
1339  }
1340 
1341  // Continue the fight to death; if one of the units got petrified,
1342  // either n_attacks or n_defends is -1
1343  if(rounds > 0 && d_.n_attacks_ == 0 && a_.n_attacks_ == 0) {
1344  a_.n_attacks_ = a_.orig_attacks_;
1345  d_.n_attacks_ = d_.orig_attacks_;
1346  --rounds;
1347  defender_strikes_first = (d_stats_->firststrike && !a_stats_->firststrike);
1348  }
1349 
1350  if(a_.n_attacks_ <= 0 && d_.n_attacks_ <= 0) {
1351  fire_event("attack_end");
1352  refresh_bc();
1353  break;
1354  }
1355  }
1356 
1357  // Set by attacker_hits and defender_hits and unit_hits events.
1358  resources::gamedata->clear_variable("damage_inflicted");
1359 
1360  if(update_def_fog_) {
1361  actions::recalculate_fog(defender_side);
1362  }
1363 
1364  // TODO: if we knew the viewing team, we could skip this display update
1365  if(update_minimap_ && update_display_) {
1367  }
1368 
1369  if(a_.valid()) {
1370  unit& u = a_.get_unit();
1371  u.anim_comp().set_standing();
1372  u.set_experience(u.experience() + a_.xp_);
1373  }
1374 
1375  if(d_.valid()) {
1376  unit& u = d_.get_unit();
1377  u.anim_comp().set_standing();
1378  u.set_experience(u.experience() + d_.xp_);
1379  }
1380 
1381  unit_display::unit_sheath_weapon(a_.loc_, a_.get_unit_ptr(), a_stats_->weapon, d_stats_->weapon,
1382  d_.loc_, d_.get_unit_ptr());
1383 
1384  if(update_display_) {
1387  display::get_singleton()->invalidate(d_.loc_);
1388  }
1389 
1390  if(OOS_error_) {
1391  replay::process_error(errbuf_.str());
1392  }
1393 }
1394 
1395 void attack::check_replay_attack_result(
1396  bool& hits, int ran_num, int& damage, config replay_results, unit_info& attacker)
1397 {
1398  int results_chance = replay_results["chance"].to_int();
1399  bool results_hits = replay_results["hits"].to_bool();
1400  int results_damage = replay_results["damage"].to_int();
1401 
1402 #if 0
1403  errbuf_ << "SYNC: In attack " << a_.dump() << " vs " << d_.dump()
1404  << " replay data differs from local calculated data:"
1405  << " chance to hit in data source: " << results_chance
1406  << " chance to hit in calculated: " << attacker.cth_
1407  << " chance to hit in data source: " << results_chance
1408  << " chance to hit in calculated: " << attacker.cth_
1409  ;
1410 
1411  attacker.cth_ = results_chance;
1412  hits = results_hits;
1413  damage = results_damage;
1414 
1415  OOS_error_ = true;
1416 #endif
1417 
1418  if(results_chance != attacker.cth_) {
1419  errbuf_ << "SYNC: In attack " << a_.dump() << " vs " << d_.dump()
1420  << ": chance to hit is inconsistent. Data source: " << results_chance
1421  << "; Calculation: " << attacker.cth_ << " (over-riding game calculations with data source results)\n";
1422  attacker.cth_ = results_chance;
1423  OOS_error_ = true;
1424  }
1425 
1426  if(results_hits != hits) {
1427  errbuf_ << "SYNC: In attack " << a_.dump() << " vs " << d_.dump() << ": the data source says the hit was "
1428  << (results_hits ? "successful" : "unsuccessful") << ", while in-game calculations say the hit was "
1429  << (hits ? "successful" : "unsuccessful") << " random number: " << ran_num << " = " << (ran_num % 100)
1430  << "/" << results_chance << " (over-riding game calculations with data source results)\n";
1431  hits = results_hits;
1432  OOS_error_ = true;
1433  }
1434 
1435  if(results_damage != damage) {
1436  errbuf_ << "SYNC: In attack " << a_.dump() << " vs " << d_.dump() << ": the data source says the hit did "
1437  << results_damage << " damage, while in-game calculations show the hit doing " << damage
1438  << " damage (over-riding game calculations with data source results)\n";
1439  damage = results_damage;
1440  OOS_error_ = true;
1441  }
1442 }
1443 } // end anonymous namespace
1444 
1445 
1446 // ==================================================================================
1447 // FREE-STANDING FUNCTIONS
1448 // ==================================================================================
1449 
1450 void attack_unit(const map_location& attacker,
1451  const map_location& defender,
1452  int attack_with,
1453  int defend_with,
1454  bool update_display)
1455 {
1456  attack dummy(attacker, defender, attack_with, defend_with, update_display);
1457  dummy.perform();
1458 }
1459 
1461  const map_location& defender,
1462  int attack_with,
1463  int defend_with,
1464  bool update_display)
1465 {
1466  attack_unit(attacker, defender, attack_with, defend_with, update_display);
1467 
1469  if(atku != resources::gameboard->units().end()) {
1471  }
1472 
1474  if(defu != resources::gameboard->units().end()) {
1476  }
1477 }
1478 
1479 int under_leadership(const unit& u, const map_location& loc)
1480 {
1481  active_ability_list abil = u.get_abilities("leadership", loc);
1482  unit_abilities::effect leader_effect(abil, 0, nullptr, unit_abilities::EFFECT_CUMULABLE);
1483  return leader_effect.get_composite_value();
1484 }
1485 
1486 int under_leadership(const unit& u, const specials_context_t& context)
1487 {
1488  active_ability_list abil = context.get_abilities_weapons("leadership", u);
1489  unit_abilities::effect leader_effect(abil, 0, nullptr, unit_abilities::EFFECT_CUMULABLE);
1490  return leader_effect.get_composite_value();
1491 }
1492 
1493 int combat_modifier(const unit_map& units,
1494  const gamemap& map,
1495  const map_location& loc,
1496  unit_alignments::type alignment,
1497  bool is_fearless)
1498 {
1499  const tod_manager& tod_m = *resources::tod_manager;
1500  const time_of_day& effective_tod = tod_m.get_illuminated_time_of_day(units, map, loc);
1501  return combat_modifier(effective_tod, alignment, is_fearless);
1502 }
1503 
1504 int combat_modifier(const time_of_day& effective_tod,
1505  unit_alignments::type alignment,
1506  bool is_fearless)
1507 {
1508  const tod_manager& tod_m = *resources::tod_manager;
1509  const int lawful_bonus = effective_tod.lawful_bonus;
1510  return generic_combat_modifier(lawful_bonus, alignment, is_fearless, tod_m.get_max_liminal_bonus());
1511 }
1512 
1513 int generic_combat_modifier(int lawful_bonus, unit_alignments::type alignment, bool is_fearless, int max_liminal_bonus)
1514 {
1515  int bonus;
1516 
1517  switch(alignment) {
1518  case unit_alignments::type::lawful:
1519  bonus = lawful_bonus;
1520  break;
1521  case unit_alignments::type::neutral:
1522  bonus = 0;
1523  break;
1524  case unit_alignments::type::chaotic:
1525  bonus = -lawful_bonus;
1526  break;
1527  case unit_alignments::type::liminal:
1528  bonus = max_liminal_bonus-std::abs(lawful_bonus);
1529  break;
1530  default:
1531  bonus = 0;
1532  }
1533 
1534  if(is_fearless) {
1535  bonus = std::max<int>(bonus, 0);
1536  }
1537 
1538  return bonus;
1539 }
1540 
1541 bool backstab_check(const map_location& attacker_loc,
1542  const map_location& defender_loc,
1543  const unit_map& units,
1544  const std::vector<team>& teams)
1545 {
1546  const unit_map::const_iterator defender = units.find(defender_loc);
1547  if(defender == units.end()) {
1548  return false; // No defender
1549  }
1550 
1551  const auto adj = get_adjacent_tiles(defender_loc);
1552  unsigned i;
1553 
1554  for(i = 0; i < adj.size(); ++i) {
1555  if(adj[i] == attacker_loc) {
1556  break;
1557  }
1558  }
1559 
1560  if(i >= 6) {
1561  return false; // Attack not from adjacent location
1562  }
1563 
1564  const unit_map::const_iterator opp = units.find(adj[(i + 3) % 6]);
1565 
1566  // No opposite unit.
1567  if(opp == units.end()) {
1568  return false;
1569  }
1570 
1571  if(opp->incapacitated()) {
1572  return false;
1573  }
1574 
1575  // If sides aren't valid teams, then they are enemies.
1576  if(std::size_t(defender->side() - 1) >= teams.size() || std::size_t(opp->side() - 1) >= teams.size()) {
1577  return true;
1578  }
1579 
1580  // Defender and opposite are enemies.
1581  if(teams[defender->side() - 1].is_enemy(opp->side())) {
1582  return true;
1583  }
1584 
1585  // Defender and opposite are friends.
1586  return false;
1587 }
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:1513
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:1541
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:1493
int under_leadership(const unit &u, const map_location &loc)
Tests if the unit at loc is currently affected by leadership.
Definition: attack.cpp:1479
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:1460
#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:1450
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:468
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:1479
bool invalidate(const map_location &loc)
Function to invalidate a specific tile for redrawing.
Definition: display.cpp:2963
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:1627
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:360
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: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
std::size_t i
Definition: function.cpp:1031
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:686
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:1495
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:1436
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:1767
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:1494
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