The Battle for Wesnoth  1.19.18+dev
attack_predictions.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2010 - 2025
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  battle_context& bc, unit_const_ptr attacker, unit_const_ptr defender)
50  : modal_dialog(window_id())
51  , attacker_data_(std::move(attacker), bc.get_attacker_combatant(), bc.get_attacker_stats())
52  , defender_data_(std::move(defender), bc.get_defender_combatant(), bc.get_defender_stats())
53 {
54 }
55 
57 {
60 }
61 
62 static std::string get_probability_string(const double prob)
63 {
64  std::ostringstream ss;
65 
66  if(prob > 0.9995) {
67  ss << "100%";
68  } else {
69  ss << std::fixed << std::setprecision(1) << 100.0 * prob << '%';
70  }
71 
72  return ss.str();
73 }
74 
75 void attack_predictions::set_data(const combatant_data& attacker, const combatant_data& defender)
76 {
77  // Each data widget in this dialog has its id prefixed by either of these identifiers.
78  const std::string widget_id_prefix = attacker.stats_.is_attacker ? "attacker" : "defender";
79 
80  const auto get_prefixed_widget_id = [&widget_id_prefix](const std::string& id) {
81  return (formatter() << widget_id_prefix << "_" << id).str();
82  };
83 
84  // Helpers for setting or hiding labels
85  const auto set_label_helper = [&, this](const std::string& id, const std::string& value) {
86  // MSVC does not compile without this-> (26-09-2024)
87  label& lbl = this->find_widget<label>(get_prefixed_widget_id(id));
88  lbl.set_label(value);
89  };
90 
91  const auto hide_label_helper = [&, this](const std::string& id) {
92  // MSVC does not compile without this-> (26-09-2024)
93  label& lbl = this->find_widget<label>(get_prefixed_widget_id(id));
95  label& lbl2 = this->find_widget<label>(get_prefixed_widget_id(id) + "_label");
97  };
98 
99  std::stringstream ss;
100 
101  //
102  // Always visible fields
103  //
104 
105  // Unscathed probability
106  const color_t ndc_color = game_config::red_to_green(attacker.combatant_.untouched * 100);
107 
109  set_label_helper("chance_unscathed", ss.str());
110 
111  // HP probability graph
112  drawing& graph_widget = find_widget<drawing>(get_prefixed_widget_id("hp_graph"));
113  draw_hp_graph(graph_widget, attacker, defender);
114 
115  //
116  // Weapon detail fields (only shown if a weapon is present)
117  //
118 
119  if(!attacker.stats_.weapon) {
120  set_label_helper("base_damage", _("No usable weapon"));
121 
122  // FIXME: would rather have a list somewhere that I can loop over instead of hardcoding...
123  hide_label_helper("tod_modifier");
124  hide_label_helper("leadership_modifier");
125  hide_label_helper("slowed_modifier");
126 
127  return;
128  }
129 
130  ss.str("");
131 
132  // Set specials context (for safety, it should not have changed normally).
133  const_attack_ptr weapon = attacker.stats_.weapon, opp_weapon = defender.stats_.weapon;
134 
135  auto ctx = specials_context_t::make(
136  { attacker.unit_, attacker.unit_->get_location(), weapon },
137  { defender.unit_, defender.unit_->get_location(), opp_weapon }, attacker.stats_.is_attacker);
138 
139  // Get damage modifiers.
140  active_ability_list dmg_specials = weapon->get_specials_and_abilities("damage");
141  unit_abilities::effect dmg_effect(dmg_specials, weapon->damage());
142 
143  // Get the SET damage modifier, if any.
144  auto set_dmg_effect = std::find_if(dmg_effect.begin(), dmg_effect.end(),
145  [](const unit_abilities::individual_effect& e) { return e.type == unit_abilities::SET; }
146  );
147 
148  // Either user the SET modifier or the base weapon damage.
149  if(set_dmg_effect == dmg_effect.end()) {
150  ss << weapon->damage() << " (" << markup::italic(weapon->name()) << ")";
151  } else {
152  assert(set_dmg_effect->ability);
153  ss << set_dmg_effect->value << " (" << markup::italic((*set_dmg_effect->ability)["name"]) << ")";
154  }
155 
156  // Process the ADD damage modifiers.
157  for(const auto& e : dmg_effect) {
158  if(e.type == unit_abilities::ADD) {
159  ss << "\n";
160 
161  if(e.value >= 0) {
162  ss << '+';
163  }
164 
165  ss << e.value;
166  ss << " (" << markup::italic((*e.ability)["name"]) << ")";
167  }
168  }
169 
170  // Process the MUL damage modifiers.
171  for(const auto& e : dmg_effect) {
172  if(e.type == unit_abilities::MUL) {
173  ss << "\n";
174  ss << font::unicode_multiplication_sign << (e.value / 100);
175 
176  if(e.value % 100) {
177  ss << "." << ((e.value % 100) / 10);
178  if(e.value % 10) {
179  ss << (e.value % 10);
180  }
181  }
182 
183  ss << " (" << markup::italic((*e.ability)["name"]) << ")";
184  }
185  }
186 
187  set_label_helper("base_damage", ss.str());
188 
189  ss.str("");
190 
191  // Resistance modifier.
192  const auto [damage_type, resistance_modifier] = weapon->effective_damage_type();
193  if(resistance_modifier != 100) {
194  if(attacker.stats_.is_attacker) {
195  if(resistance_modifier < 100) {
196  ss << _("Defender resistance vs") << " ";
197  } else {
198  ss << _("Defender vulnerability vs") << " ";
199  }
200  } else {
201  if(resistance_modifier < 100) {
202  ss << _("Attacker resistance vs") << " ";
203  } else {
204  ss << _("Attacker vulnerability vs") << " ";
205  }
206  }
207 
208  ss << string_table["type_" + damage_type];
209 
210  set_label_helper("resis_label", ss.str());
211 
212  ss.str("");
213  ss << font::unicode_multiplication_sign << (resistance_modifier / 100) << "." << ((resistance_modifier % 100) / 10);
214 
215  set_label_helper("resis", ss.str());
216  }
217 
218  ss.str("");
219 
220  // TODO: color format the modifiers
221 
222  // Time of day modifier.
223  const unit& u = *attacker.unit_;
224 
225  unit_alignments::type alignment = weapon->alignment().value_or(u.alignment());
226  const int tod_modifier = combat_modifier(resources::gameboard->units(), resources::gameboard->map(),
227  u.get_location(), alignment, u.is_fearless());
228 
229  if(tod_modifier != 0) {
230  set_label_helper("tod_modifier", utils::signed_percent(tod_modifier));
231  } else {
232  hide_label_helper("tod_modifier");
233  }
234 
235  if(attacker.stats_.leadership_bonus != 0) {
236  set_label_helper("leadership_modifier", utils::signed_percent(attacker.stats_.leadership_bonus));
237  } else {
238  hide_label_helper("leadership_modifier");
239  }
240 
241  // Slowed penalty.
242  if(attacker.stats_.is_slowed) {
243  set_label_helper("slowed_modifier", "/ 2");
244  } else {
245  hide_label_helper("slowed_modifier");
246  }
247 
248  // Total damage.
249  const int base_damage = weapon->damage();
250 
251  color_t dmg_color = font::weapon_color;
252  if(attacker.stats_.damage > base_damage) {
253  dmg_color = font::good_dmg_color;
254  } else if(attacker.stats_.damage < base_damage) {
255  dmg_color = font::bad_dmg_color;
256  }
257 
258  ss << markup::span_color(dmg_color, attacker.stats_.damage)
260 
261  set_label_helper("total_damage", ss.str());
262 
263  // Chance to hit
264  const color_t cth_color = game_config::red_to_green(attacker.stats_.chance_to_hit);
265 
266  ss.str("");
267  ss << markup::span_color(cth_color, attacker.stats_.chance_to_hit, "%");
268 
269  set_label_helper("chance_to_hit", ss.str());
270 }
271 
272 void attack_predictions::draw_hp_graph(drawing& hp_graph, const combatant_data& attacker, const combatant_data& defender)
273 {
274  // Font size. If you change this, you must update the separator space.
275  // TODO: probably should remove this.
276  const int fs = font::SIZE_SMALL;
277 
278  // Space before HP separator.
279  const int hp_sep = 30;
280 
281  // Space after percentage separator.
282  const int percent_sep = 50;
283 
284  // Bar space between both separators.
285  const int bar_space = graph_width - hp_sep - percent_sep - 4;
286 
287  // Set some variables for the WML portion of the graph to use.
288  canvas& hp_graph_canvas = hp_graph.get_drawing_canvas();
289 
290  hp_graph_canvas.set_variable("hp_column_width", wfl::variant(hp_sep));
291  hp_graph_canvas.set_variable("chance_column_width", wfl::variant(percent_sep));
292 
293  config cfg, shape;
294 
295  int i = 0;
296 
297  // Draw the rows (lower HP values are at the bottom).
298  for(const auto& probability : get_hitpoint_probabilities(attacker.combatant_.hp_dist)) {
299 
300  // Get the HP and probability.
301  auto [hp, prob] = probability;
302 
303  color_t row_color;
304 
305  // Death line is red.
306  if(hp == 0) {
307  row_color = {229, 0, 0};
308  }
309 
310  // Below current hitpoints value is orange.
311  else if(hp < static_cast<int>(attacker.stats_.hp)) {
312  // Stone is grey.
313  if(defender.stats_.petrifies) {
314  row_color = {154, 154, 154};
315  } else {
316  row_color = {244, 201, 0};
317  }
318  }
319 
320  // Current hitpoints value and above is green.
321  else {
322  row_color = {8, 202, 0};
323  }
324 
325  shape["text"] = hp;
326  shape["x"] = 4;
327  shape["y"] = 2 + (fs + 2) * i;
328  shape["w"] = "(text_width)";
329  shape["h"] = "(text_height)";
330  shape["font_size"] = 12;
331  shape["color"] = "255, 255, 255, 255";
332  shape["text_alignment"] = "(text_alignment)";
333 
334  cfg.add_child("text", shape);
335 
336  shape.clear();
337  shape["text"] = get_probability_string(prob);
338  shape["x"] = graph_width - percent_sep + 2;
339  shape["y"] = 2 + (fs + 2) * i;
340  shape["w"] = "(text_width)";
341  shape["h"] = "(text_height)";
342  shape["font_size"] = 12;
343  shape["color"] = "255, 255, 255, 255";
344  shape["text_alignment"] = "(text_alignment)";
345 
346  cfg.add_child("text", shape);
347 
348  const int bar_len = std::max(static_cast<int>((prob * (bar_space - 4)) + 0.5), 2);
349 
350  const rect bar_rect_1 {
351  hp_sep + 4,
352  6 + (fs + 2) * i,
353  bar_len,
354  8
355  };
356 
357  shape.clear();
358  shape["x"] = bar_rect_1.x;
359  shape["y"] = bar_rect_1.y;
360  shape["w"] = bar_rect_1.w;
361  shape["h"] = bar_rect_1.h;
362  shape["fill_color"] = row_color.to_rgba_string();
363 
364  cfg.add_child("rectangle", shape);
365 
366  ++i;
367  }
368 
369  hp_graph.append_drawing_data(cfg);
370 }
371 
373 {
374  hp_probability_vector res, temp_vec;
375 
376  // First, extract any relevant probability values
377  for(int i = 0; i < static_cast<int>(hp_dist.size()); ++i) {
378  const double prob = hp_dist[i];
379 
380  // We keep only values above 0.1%.
381  if(prob > 0.001) {
382  temp_vec.emplace_back(i, prob);
383  }
384  }
385 
386  // Then sort by descending probability.
387  std::sort(temp_vec.begin(), temp_vec.end(), [](const auto& pair1, const auto& pair2) {
388  return pair1.second > pair2.second;
389  });
390 
391  // Take only the highest probability values.;
392  std::copy_n(temp_vec.begin(), std::min<int>(graph_max_rows, temp_vec.size()), std::back_inserter(res));
393 
394  // Then, we sort the hitpoint values in descending order.
395  std::sort(res.begin(), res.end(), [](const auto& pair1, const auto& pair2) {
396  return pair1.first > pair2.first;
397  });
398 
399  return res;
400 }
401 
402 } // namespace dialogs
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
Computes the statistics of a battle between an attacker and a defender unit.
Definition: attack.hpp:164
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 clear()
Definition: config.cpp:802
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:157
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:
static specials_context_t make(specials_combatant &&self, specials_combatant &&other, bool attacking)
Definition: abilities.hpp:271
const_iterator end() const
Definition: abilities.hpp:365
const_iterator begin() const
Definition: abilities.hpp:363
This class represents a single unit of a specific type.
Definition: unit.hpp:39
Definitions for the interface to Wesnoth Markup Language (WML).
const config * cfg
std::size_t i
Definition: function.cpp:1032
static std::string _(const char *str)
Definition: gettext.hpp:97
unit_alignments::type alignment() const
The alignment of this unit.
Definition: unit.hpp:397
const map_location & get_location() const
The current map location this unit is at.
Definition: unit.hpp:1327
bool is_fearless() const
Gets whether this unit is fearless - ie, unaffected by time of day.
Definition: unit.hpp:1186
auto string_table
Definition: language.hpp:68
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)
Applies italic Pango markup to the input.
Definition: markup.hpp:176
std::string span_color(const color_t &color, Args &&... data)
Applies Pango markup to the input specifying its display color.
Definition: markup.hpp:110
game_board * gameboard
Definition: resources.cpp:20
auto * find_if(Container &container, const Predicate &predicate)
Convenience wrapper for using find_if on a container without needing to comare to end()
Definition: general.hpp:151
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:80
bool petrifies
Attack petrifies opponent when it hits.
Definition: attack.hpp:62
unsigned int hp
Hitpoints of the unit at the beginning of the battle.
Definition: attack.hpp:73
bool is_attacker
True if the unit is the attacker.
Definition: attack.hpp:57
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
int damage
Effective damage of the weapon (all factors accounted for).
Definition: attack.hpp:76
unsigned int chance_to_hit
Effective chance to hit as a percentage (all factors accounted for).
Definition: attack.hpp:75
The basic class for representing 8-bit RGB or RGBA colour values.
Definition: color.hpp:61
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)
An abstract description of a rectangle with integer coordinates.
Definition: rect.hpp:49
#define e