The Battle for Wesnoth  1.19.7+dev
attack_predictions.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2010 - 2024
3  by Mark de Wever <koraq@xs4all.nl>
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 #define GETTEXT_DOMAIN "wesnoth-lib"
17 
19 
20 #include "attack_prediction.hpp"
21 #include "color.hpp"
22 #include "config.hpp"
23 #include "serialization/markup.hpp"
24 #include "formatter.hpp"
25 #include "formula/variant.hpp"
26 #include "game_board.hpp"
27 #include "game_config.hpp"
28 #include "gui/widgets/drawing.hpp"
29 #include "gui/widgets/label.hpp"
30 #include "gettext.hpp"
31 #include "language.hpp"
32 #include "resources.hpp"
33 #include "units/abilities.hpp"
34 #include "units/unit.hpp"
35 
36 #include <iomanip>
37 #include <utility>
38 
39 namespace gui2::dialogs
40 {
41 
42 REGISTER_DIALOG(attack_predictions)
43 
44 const unsigned int attack_predictions::graph_width = 270;
45 const unsigned int attack_predictions::graph_height = 170;
46 const unsigned int attack_predictions::graph_max_rows = 10;
47 
49  : modal_dialog(window_id())
50  , attacker_data_(std::move(attacker), bc.get_attacker_combatant(), bc.get_attacker_stats())
51  , defender_data_(std::move(defender), bc.get_defender_combatant(), bc.get_defender_stats())
52 {
53 }
54 
56 {
59 }
60 
61 static std::string get_probability_string(const double prob)
62 {
63  std::ostringstream ss;
64 
65  if(prob > 0.9995) {
66  ss << "100%";
67  } else {
68  ss << std::fixed << std::setprecision(1) << 100.0 * prob << '%';
69  }
70 
71  return ss.str();
72 }
73 
74 void attack_predictions::set_data(const combatant_data& attacker, const combatant_data& defender)
75 {
76  // Each data widget in this dialog has its id prefixed by either of these identifiers.
77  const std::string widget_id_prefix = attacker.stats_.is_attacker ? "attacker" : "defender";
78 
79  const auto get_prefixed_widget_id = [&widget_id_prefix](const std::string& id) {
80  return (formatter() << widget_id_prefix << "_" << id).str();
81  };
82 
83  // Helpers for setting or hiding labels
84  const auto set_label_helper = [&, this](const std::string& id, const std::string& value) {
85  // MSVC does not compile without this-> (26-09-2024)
86  label& lbl = this->find_widget<label>(get_prefixed_widget_id(id));
87  lbl.set_label(value);
88  };
89 
90  const auto hide_label_helper = [&, this](const std::string& id) {
91  // MSVC does not compile without this-> (26-09-2024)
92  label& lbl = this->find_widget<label>(get_prefixed_widget_id(id));
94  label& lbl2 = this->find_widget<label>(get_prefixed_widget_id(id) + "_label");
96  };
97 
98  std::stringstream ss;
99 
100  //
101  // Always visible fields
102  //
103 
104  // Unscathed probability
105  const color_t ndc_color = game_config::red_to_green(attacker.combatant_.untouched * 100);
106 
108  set_label_helper("chance_unscathed", ss.str());
109 
110  // HP probability graph
111  drawing& graph_widget = find_widget<drawing>(get_prefixed_widget_id("hp_graph"));
112  draw_hp_graph(graph_widget, attacker, defender);
113 
114  //
115  // Weapon detail fields (only shown if a weapon is present)
116  //
117 
118  if(!attacker.stats_.weapon) {
119  set_label_helper("base_damage", _("No usable weapon"));
120 
121  // FIXME: would rather have a list somewhere that I can loop over instead of hardcoding...
122  hide_label_helper("tod_modifier");
123  hide_label_helper("leadership_modifier");
124  hide_label_helper("slowed_modifier");
125 
126  return;
127  }
128 
129  ss.str("");
130 
131  // Set specials context (for safety, it should not have changed normally).
132  const_attack_ptr weapon = attacker.stats_.weapon, opp_weapon = defender.stats_.weapon;
133  auto ctx = weapon->specials_context(attacker.unit_, defender.unit_, attacker.unit_->get_location(), defender.unit_->get_location(), attacker.stats_.is_attacker, opp_weapon);
134  utils::optional<decltype(ctx)> opp_ctx;
135 
136  if(opp_weapon) {
137  opp_ctx.emplace(opp_weapon->specials_context(defender.unit_, attacker.unit_, defender.unit_->get_location(), attacker.unit_->get_location(), defender.stats_.is_attacker, weapon));
138  }
139 
140  // Get damage modifiers.
141  unit_ability_list dmg_specials = weapon->get_specials_and_abilities("damage");
142  unit_abilities::effect dmg_effect(dmg_specials, weapon->damage());
143 
144  // Get the SET damage modifier, if any.
145  auto set_dmg_effect = std::find_if(dmg_effect.begin(), dmg_effect.end(),
146  [](const unit_abilities::individual_effect& e) { return e.type == unit_abilities::SET; }
147  );
148 
149  // Either user the SET modifier or the base weapon damage.
150  if(set_dmg_effect == dmg_effect.end()) {
151  ss << weapon->damage() << " (" << markup::italic(weapon->name()) << ")";
152  } else {
153  assert(set_dmg_effect->ability);
154  ss << set_dmg_effect->value << " (" << markup::italic((*set_dmg_effect->ability)["name"]) << ")";
155  }
156 
157  // Process the ADD damage modifiers.
158  for(const auto& e : dmg_effect) {
159  if(e.type == unit_abilities::ADD) {
160  ss << "\n";
161 
162  if(e.value >= 0) {
163  ss << '+';
164  }
165 
166  ss << e.value;
167  ss << " (" << markup::italic((*e.ability)["name"]) << ")";
168  }
169  }
170 
171  // Process the MUL damage modifiers.
172  for(const auto& e : dmg_effect) {
173  if(e.type == unit_abilities::MUL) {
174  ss << "\n";
175  ss << font::unicode_multiplication_sign << (e.value / 100);
176 
177  if(e.value % 100) {
178  ss << "." << ((e.value % 100) / 10);
179  if(e.value % 10) {
180  ss << (e.value % 10);
181  }
182  }
183 
184  ss << " (" << markup::italic((*e.ability)["name"]) << ")";
185  }
186  }
187 
188  set_label_helper("base_damage", ss.str());
189 
190  ss.str("");
191 
192  // Resistance modifier.
193  const int resistance_modifier = defender.unit_->damage_from(*weapon, !attacker.stats_.is_attacker, defender.unit_->get_location(), opp_weapon);
194  if(resistance_modifier != 100) {
195  if(attacker.stats_.is_attacker) {
196  if(resistance_modifier < 100) {
197  ss << _("Defender resistance vs") << " ";
198  } else {
199  ss << _("Defender vulnerability vs") << " ";
200  }
201  } else {
202  if(resistance_modifier < 100) {
203  ss << _("Attacker resistance vs") << " ";
204  } else {
205  ss << _("Attacker vulnerability vs") << " ";
206  }
207  }
208 
209  std::pair<std::string, std::string> types = weapon->damage_type();
210  std::string type_bis = types.second;
211  if (!type_bis.empty()) {
212  type_bis = ", " + string_table["type_" + type_bis];
213  }
214  ss << string_table["type_" + types.first] + type_bis;
215 
216  set_label_helper("resis_label", ss.str());
217 
218  ss.str("");
219  ss << font::unicode_multiplication_sign << (resistance_modifier / 100) << "." << ((resistance_modifier % 100) / 10);
220 
221  set_label_helper("resis", ss.str());
222  }
223 
224  ss.str("");
225 
226  // TODO: color format the modifiers
227 
228  // Time of day modifier.
229  const unit& u = *attacker.unit_;
230 
231  unit_alignments::type alignment = weapon->alignment().value_or(u.alignment());
232  const int tod_modifier = combat_modifier(resources::gameboard->units(), resources::gameboard->map(),
233  u.get_location(), alignment, u.is_fearless());
234 
235  if(tod_modifier != 0) {
236  set_label_helper("tod_modifier", utils::signed_percent(tod_modifier));
237  } else {
238  hide_label_helper("tod_modifier");
239  }
240 
241  // Leadership bonus.
242  const int leadership_bonus = under_leadership(*attacker.unit_, attacker.unit_->get_location(), weapon, opp_weapon);
243 
244  if(leadership_bonus != 0) {
245  set_label_helper("leadership_modifier", utils::signed_percent(leadership_bonus));
246  } else {
247  hide_label_helper("leadership_modifier");
248  }
249 
250  // Slowed penalty.
251  if(attacker.stats_.is_slowed) {
252  set_label_helper("slowed_modifier", "/ 2");
253  } else {
254  hide_label_helper("slowed_modifier");
255  }
256 
257  // Total damage.
258  const int base_damage = weapon->damage();
259 
260  color_t dmg_color = font::weapon_color;
261  if(attacker.stats_.damage > base_damage) {
262  dmg_color = font::good_dmg_color;
263  } else if(attacker.stats_.damage < base_damage) {
264  dmg_color = font::bad_dmg_color;
265  }
266 
267  ss << markup::span_color(dmg_color, attacker.stats_.damage)
269 
270  set_label_helper("total_damage", ss.str());
271 
272  // Chance to hit
273  const color_t cth_color = game_config::red_to_green(attacker.stats_.chance_to_hit);
274 
275  ss.str("");
276  ss << markup::span_color(cth_color, attacker.stats_.chance_to_hit, "%");
277 
278  set_label_helper("chance_to_hit", ss.str());
279 }
280 
281 void attack_predictions::draw_hp_graph(drawing& hp_graph, const combatant_data& attacker, const combatant_data& defender)
282 {
283  // Font size. If you change this, you must update the separator space.
284  // TODO: probably should remove this.
285  const int fs = font::SIZE_SMALL;
286 
287  // Space before HP separator.
288  const int hp_sep = 30;
289 
290  // Space after percentage separator.
291  const int percent_sep = 50;
292 
293  // Bar space between both separators.
294  const int bar_space = graph_width - hp_sep - percent_sep - 4;
295 
296  // Set some variables for the WML portion of the graph to use.
297  canvas& hp_graph_canvas = hp_graph.get_drawing_canvas();
298 
299  hp_graph_canvas.set_variable("hp_column_width", wfl::variant(hp_sep));
300  hp_graph_canvas.set_variable("chance_column_width", wfl::variant(percent_sep));
301 
302  config cfg, shape;
303 
304  int i = 0;
305 
306  // Draw the rows (lower HP values are at the bottom).
307  for(const auto& probability : get_hitpoint_probabilities(attacker.combatant_.hp_dist)) {
308 
309  // Get the HP and probability.
310  auto [hp, prob] = probability;
311 
312  color_t row_color;
313 
314  // Death line is red.
315  if(hp == 0) {
316  row_color = {229, 0, 0};
317  }
318 
319  // Below current hitpoints value is orange.
320  else if(hp < static_cast<int>(attacker.stats_.hp)) {
321  // Stone is grey.
322  if(defender.stats_.petrifies) {
323  row_color = {154, 154, 154};
324  } else {
325  row_color = {244, 201, 0};
326  }
327  }
328 
329  // Current hitpoints value and above is green.
330  else {
331  row_color = {8, 202, 0};
332  }
333 
334  shape["text"] = hp;
335  shape["x"] = 4;
336  shape["y"] = 2 + (fs + 2) * i;
337  shape["w"] = "(text_width)";
338  shape["h"] = "(text_height)";
339  shape["font_size"] = 12;
340  shape["color"] = "255, 255, 255, 255";
341  shape["text_alignment"] = "(text_alignment)";
342 
343  cfg.add_child("text", shape);
344 
345  shape.clear();
346  shape["text"] = get_probability_string(prob);
347  shape["x"] = graph_width - percent_sep + 2;
348  shape["y"] = 2 + (fs + 2) * i;
349  shape["w"] = "(text_width)";
350  shape["h"] = "(text_height)";
351  shape["font_size"] = 12;
352  shape["color"] = "255, 255, 255, 255";
353  shape["text_alignment"] = "(text_alignment)";
354 
355  cfg.add_child("text", shape);
356 
357  const int bar_len = std::max(static_cast<int>((prob * (bar_space - 4)) + 0.5), 2);
358 
359  const SDL_Rect bar_rect_1 {
360  hp_sep + 4,
361  6 + (fs + 2) * i,
362  bar_len,
363  8
364  };
365 
366  shape.clear();
367  shape["x"] = bar_rect_1.x;
368  shape["y"] = bar_rect_1.y;
369  shape["w"] = bar_rect_1.w;
370  shape["h"] = bar_rect_1.h;
371  shape["fill_color"] = row_color.to_rgba_string();
372 
373  cfg.add_child("rectangle", shape);
374 
375  ++i;
376  }
377 
378  hp_graph.append_drawing_data(cfg);
379 }
380 
382 {
383  hp_probability_vector res, temp_vec;
384 
385  // First, extract any relevant probability values
386  for(int i = 0; i < static_cast<int>(hp_dist.size()); ++i) {
387  const double prob = hp_dist[i];
388 
389  // We keep only values above 0.1%.
390  if(prob > 0.001) {
391  temp_vec.emplace_back(i, prob);
392  }
393  }
394 
395  // Then sort by descending probability.
396  std::sort(temp_vec.begin(), temp_vec.end(), [](const auto& pair1, const auto& pair2) {
397  return pair1.second > pair2.second;
398  });
399 
400  // Take only the highest probability values.;
401  std::copy_n(temp_vec.begin(), std::min<int>(graph_max_rows, temp_vec.size()), std::back_inserter(res));
402 
403  // Then, we sort the hitpoint values in descending order.
404  std::sort(res.begin(), res.end(), [](const auto& pair1, const auto& pair2) {
405  return pair1.first > pair2.first;
406  });
407 
408  return res;
409 }
410 
411 } // namespace dialogs
int under_leadership(const unit &u, const map_location &loc, const_attack_ptr weapon, const_attack_ptr opp_weapon)
Tests if the unit at loc is currently affected by leadership.
Definition: attack.cpp:1593
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:1600
Computes the statistics of a battle between an attacker and a defender unit.
Definition: attack.hpp:167
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:172
void clear()
Definition: config.cpp:828
config & add_child(config_key_type key)
Definition: config.cpp:440
std::ostringstream wrapper.
Definition: formatter.hpp:40
A simple canvas which can be drawn upon.
Definition: canvas.hpp:45
void set_variable(const std::string &key, wfl::variant &&value)
Definition: canvas.hpp:154
static const unsigned int graph_width
attack_predictions(battle_context &bc, unit_const_ptr attacker, unit_const_ptr defender)
void draw_hp_graph(drawing &hp_graph, const combatant_data &attacker, const combatant_data &defender)
static const unsigned int graph_height
void set_data(const combatant_data &attacker, const combatant_data &defender)
static const unsigned int graph_max_rows
virtual void pre_show() override
Actions to be taken before showing the window.
hp_probability_vector get_hitpoint_probabilities(const std::vector< double > &hp_dist) const
Abstract base class for all modal dialogs.
void append_drawing_data(const ::config &cfg)
Definition: drawing.hpp:44
canvas & get_drawing_canvas()
Definition: drawing.hpp:34
virtual void set_label(const t_string &text)
void set_visible(const visibility visible)
Definition: widget.cpp:479
const std::string & id() const
Definition: widget.cpp:110
@ invisible
The user set the widget invisible, that means:
const_iterator end() const
Definition: abilities.hpp:53
const_iterator begin() const
Definition: abilities.hpp:51
This class represents a single unit of a specific type.
Definition: unit.hpp:133
Definitions for the interface to Wesnoth Markup Language (WML).
std::size_t i
Definition: function.cpp:1029
static std::string _(const char *str)
Definition: gettext.hpp:93
unit_alignments::type alignment() const
The alignment of this unit.
Definition: unit.hpp:475
const map_location & get_location() const
The current map location this unit is at.
Definition: unit.hpp:1404
bool is_fearless() const
Gets whether this unit is fearless - ie, unaffected by time of day.
Definition: unit.hpp:1294
symbol_table string_table
Definition: language.cpp:64
const std::string unicode_multiplication_sign
Definition: constants.cpp:46
const int SIZE_SMALL
Definition: constants.cpp:24
const color_t good_dmg_color
const color_t weapon_color
const std::string weapon_numbers_sep
Definition: constants.cpp:49
const color_t bad_dmg_color
color_t red_to_green(double val, bool for_text)
Return a color corresponding to the value val red for val=0.0 to green for val=100....
REGISTER_DIALOG(editor_edit_unit)
static std::string get_probability_string(const double prob)
std::vector< std::pair< int, double > > hp_probability_vector
std::string italic(Args &&... data)
Definition: markup.hpp:140
std::string span_color(const color_t &color, Args &&... data)
Definition: markup.hpp:68
game_board * gameboard
Definition: resources.cpp:20
std::string signed_percent(int val)
Convert into a percentage (using the Unicode "−" and +0% convention.
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
unsigned int num_blows
Effective number of blows, takes swarm into account.
Definition: attack.hpp:76
bool petrifies
Attack petrifies opponent when it hits.
Definition: attack.hpp:59
unsigned int hp
Hitpoints of the unit at the beginning of the battle.
Definition: attack.hpp:69
bool is_attacker
True if the unit is the attacker.
Definition: attack.hpp:54
const_attack_ptr weapon
The weapon used by the unit to attack the opponent, or nullptr if there is none.
Definition: attack.hpp:52
bool is_slowed
True if the unit is slowed at the beginning of the battle.
Definition: attack.hpp:56
int damage
Effective damage of the weapon (all factors accounted for).
Definition: attack.hpp:72
unsigned int chance_to_hit
Effective chance to hit as a percentage (all factors accounted for).
Definition: attack.hpp:71
The basic class for representing 8-bit RGB or RGBA colour values.
Definition: color.hpp:59
std::string to_rgba_string() const
Returns the stored color as an "R,G,B,A" string.
Definition: color.cpp:105
std::vector< double > hp_dist
Resulting probability distribution (might be not as large as max_hp)
double untouched
Resulting chance we were not hit by this opponent (important if it poisons)
#define e