The Battle for Wesnoth  1.19.0-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 "font/text_formatting.hpp"
24 #include "formatter.hpp"
25 #include "formula/variant.hpp"
26 #include "game_board.hpp"
27 #include "game_config.hpp"
29 #include "gui/widgets/drawing.hpp"
30 #include "gui/widgets/label.hpp"
31 #include "gui/widgets/window.hpp"
32 #include "gettext.hpp"
33 #include "language.hpp"
34 #include "resources.hpp"
35 #include "units/abilities.hpp"
36 #include "units/unit.hpp"
37 
38 #include <iomanip>
39 
40 namespace gui2::dialogs
41 {
42 
43 REGISTER_DIALOG(attack_predictions)
44 
45 const unsigned int attack_predictions::graph_width = 270;
46 const unsigned int attack_predictions::graph_height = 170;
47 const unsigned int attack_predictions::graph_max_rows = 10;
48 
50  : modal_dialog(window_id())
51  , attacker_data_(attacker, bc.get_attacker_combatant(), bc.get_attacker_stats())
52  , defender_data_(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(window& window, const combatant_data& attacker, const combatant_data& defender) const
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 = [&](const std::string& id, const std::string& value) {
86  find_widget<label>(&window, get_prefixed_widget_id(id), false).set_label(value);
87  };
88 
89  const auto hide_label_helper = [&](const std::string& id) {
90  find_widget<label>(&window, get_prefixed_widget_id(id), false).set_visible(widget::visibility::invisible);
91  find_widget<label>(&window, get_prefixed_widget_id(id) + "_label" , false).set_visible(widget::visibility::invisible);
92  };
93 
94  std::stringstream ss;
95 
96  //
97  // Always visible fields
98  //
99 
100  // Unscathed probability
101  const color_t ndc_color = game_config::red_to_green(attacker.combatant_.untouched * 100);
102 
103  ss << font::span_color(ndc_color) << get_probability_string(attacker.combatant_.untouched) << "</span>";
104  set_label_helper("chance_unscathed", ss.str());
105 
106  // HP probability graph
107  drawing& graph_widget = find_widget<drawing>(&window, get_prefixed_widget_id("hp_graph"), false);
108  draw_hp_graph(graph_widget, attacker, defender);
109 
110  //
111  // Weapon detail fields (only shown if a weapon is present)
112  //
113 
114  if(!attacker.stats_.weapon) {
115  set_label_helper("base_damage", _("No usable weapon"));
116 
117  // FIXME: would rather have a list somewhere that I can loop over instead of hardcoding...
118  hide_label_helper("tod_modifier");
119  hide_label_helper("leadership_modifier");
120  hide_label_helper("slowed_modifier");
121 
122  return;
123  }
124 
125  ss.str("");
126 
127  // Set specials context (for safety, it should not have changed normally).
128  const_attack_ptr weapon = attacker.stats_.weapon, opp_weapon = defender.stats_.weapon;
129  auto ctx = weapon->specials_context(attacker.unit_, defender.unit_, attacker.unit_->get_location(), defender.unit_->get_location(), attacker.stats_.is_attacker, opp_weapon);
130  std::optional<decltype(ctx)> opp_ctx;
131 
132  if(opp_weapon) {
133  opp_ctx.emplace(opp_weapon->specials_context(defender.unit_, attacker.unit_, defender.unit_->get_location(), attacker.unit_->get_location(), defender.stats_.is_attacker, weapon));
134  }
135 
136  // Get damage modifiers.
137  unit_ability_list dmg_specials = weapon->get_specials_and_abilities("damage");
138  unit_abilities::effect dmg_effect(dmg_specials, weapon->damage());
139 
140  // Get the SET damage modifier, if any.
141  auto set_dmg_effect = std::find_if(dmg_effect.begin(), dmg_effect.end(),
142  [](const unit_abilities::individual_effect& e) { return e.type == unit_abilities::SET; }
143  );
144 
145  // Either user the SET modifier or the base weapon damage.
146  if(set_dmg_effect == dmg_effect.end()) {
147  ss << weapon->damage() << " (<i>" << weapon->name() << "</i>)";
148  } else {
149  assert(set_dmg_effect->ability);
150  ss << set_dmg_effect->value << " (<i>" << (*set_dmg_effect->ability)["name"] << "</i>)";
151  }
152 
153  // Process the ADD damage modifiers.
154  for(const auto& e : dmg_effect) {
155  if(e.type == unit_abilities::ADD) {
156  ss << "\n";
157 
158  if(e.value >= 0) {
159  ss << '+';
160  }
161 
162  ss << e.value;
163  ss << " (<i>" << (*e.ability)["name"] << "</i>)";
164  }
165  }
166 
167  // Process the MUL damage modifiers.
168  for(const auto& e : dmg_effect) {
169  if(e.type == unit_abilities::MUL) {
170  ss << "\n";
171  ss << font::unicode_multiplication_sign << (e.value / 100);
172 
173  if(e.value % 100) {
174  ss << "." << ((e.value % 100) / 10);
175  if(e.value % 10) {
176  ss << (e.value % 10);
177  }
178  }
179 
180  ss << " (<i>" << (*e.ability)["name"] << "</i>)";
181  }
182  }
183 
184  set_label_helper("base_damage", ss.str());
185 
186  ss.str("");
187 
188  // Resistance modifier.
189  const int resistance_modifier = defender.unit_->damage_from(*weapon, !attacker.stats_.is_attacker, defender.unit_->get_location(), opp_weapon);
190  if(resistance_modifier != 100) {
191  if(attacker.stats_.is_attacker) {
192  if(resistance_modifier < 100) {
193  ss << _("Defender resistance vs") << " ";
194  } else {
195  ss << _("Defender vulnerability vs") << " ";
196  }
197  } else {
198  if(resistance_modifier < 100) {
199  ss << _("Attacker resistance vs") << " ";
200  } else {
201  ss << _("Attacker vulnerability vs") << " ";
202  }
203  }
204 
205  std::pair<std::string, std::string> types = weapon->damage_type();
206  std::string type_bis = types.second;
207  if (!type_bis.empty()) {
208  type_bis = ", " + string_table["type_" + type_bis];
209  }
210  ss << string_table["type_" + types.first] + type_bis;
211 
212  set_label_helper("resis_label", ss.str());
213 
214  ss.str("");
215  ss << font::unicode_multiplication_sign << (resistance_modifier / 100) << "." << ((resistance_modifier % 100) / 10);
216 
217  set_label_helper("resis", ss.str());
218  }
219 
220  ss.str("");
221 
222  // TODO: color format the modifiers
223 
224  // Time of day modifier.
225  const unit& u = *attacker.unit_;
226 
227  const int tod_modifier = combat_modifier(resources::gameboard->units(), resources::gameboard->map(),
228  u.get_location(), u.alignment(), u.is_fearless());
229 
230  if(tod_modifier != 0) {
231  set_label_helper("tod_modifier", utils::signed_percent(tod_modifier));
232  } else {
233  hide_label_helper("tod_modifier");
234  }
235 
236  // Leadership bonus.
237  const int leadership_bonus = under_leadership(*attacker.unit_, attacker.unit_->get_location(), weapon, opp_weapon);
238 
239  if(leadership_bonus != 0) {
240  set_label_helper("leadership_modifier", utils::signed_percent(leadership_bonus));
241  } else {
242  hide_label_helper("leadership_modifier");
243  }
244 
245  // Slowed penalty.
246  if(attacker.stats_.is_slowed) {
247  set_label_helper("slowed_modifier", "/ 2");
248  } else {
249  hide_label_helper("slowed_modifier");
250  }
251 
252  // Total damage.
253  const int base_damage = weapon->damage();
254 
255  color_t dmg_color = font::weapon_color;
256  if(attacker.stats_.damage > base_damage) {
257  dmg_color = font::good_dmg_color;
258  } else if(attacker.stats_.damage < base_damage) {
259  dmg_color = font::bad_dmg_color;
260  }
261 
262  ss << font::span_color(dmg_color) << attacker.stats_.damage << "</span>"
264 
265  set_label_helper("total_damage", ss.str());
266 
267  // Chance to hit
268  const color_t cth_color = game_config::red_to_green(attacker.stats_.chance_to_hit);
269 
270  ss.str("");
271  ss << font::span_color(cth_color) << attacker.stats_.chance_to_hit << "%</span>";
272 
273  set_label_helper("chance_to_hit", ss.str());
274 }
275 
276 void attack_predictions::draw_hp_graph(drawing& hp_graph, const combatant_data& attacker, const combatant_data& defender) const
277 {
278  // Font size. If you change this, you must update the separator space.
279  // TODO: probably should remove this.
280  const int fs = font::SIZE_SMALL;
281 
282  // Space before HP separator.
283  const int hp_sep = 30;
284 
285  // Space after percentage separator.
286  const int percent_sep = 50;
287 
288  // Bar space between both separators.
289  const int bar_space = graph_width - hp_sep - percent_sep - 4;
290 
291  // Set some variables for the WML portion of the graph to use.
292  canvas& hp_graph_canvas = hp_graph.get_drawing_canvas();
293 
294  hp_graph_canvas.set_variable("hp_column_width", wfl::variant(hp_sep));
295  hp_graph_canvas.set_variable("chance_column_width", wfl::variant(percent_sep));
296 
297  config cfg, shape;
298 
299  int i = 0;
300 
301  // Draw the rows (lower HP values are at the bottom).
302  for(const auto& probability : get_hitpoint_probabilities(attacker.combatant_.hp_dist)) {
303 
304  // Get the HP and probability.
305  auto [hp, prob] = probability;
306 
307  color_t row_color;
308 
309  // Death line is red.
310  if(hp == 0) {
311  row_color = {229, 0, 0};
312  }
313 
314  // Below current hitpoints value is orange.
315  else if(hp < static_cast<int>(attacker.stats_.hp)) {
316  // Stone is grey.
317  if(defender.stats_.petrifies) {
318  row_color = {154, 154, 154};
319  } else {
320  row_color = {244, 201, 0};
321  }
322  }
323 
324  // Current hitpoints value and above is green.
325  else {
326  row_color = {8, 202, 0};
327  }
328 
329  shape["text"] = hp;
330  shape["x"] = 4;
331  shape["y"] = 2 + (fs + 2) * i;
332  shape["w"] = "(text_width)";
333  shape["h"] = "(text_height)";
334  shape["font_size"] = 12;
335  shape["color"] = "255, 255, 255, 255";
336  shape["text_alignment"] = "(text_alignment)";
337 
338  cfg.add_child("text", shape);
339 
340  shape.clear();
341  shape["text"] = get_probability_string(prob);
342  shape["x"] = graph_width - percent_sep + 2;
343  shape["y"] = 2 + (fs + 2) * i;
344  shape["w"] = "(text_width)";
345  shape["h"] = "(text_height)";
346  shape["font_size"] = 12;
347  shape["color"] = "255, 255, 255, 255";
348  shape["text_alignment"] = "(text_alignment)";
349 
350  cfg.add_child("text", shape);
351 
352  const int bar_len = std::max(static_cast<int>((prob * (bar_space - 4)) + 0.5), 2);
353 
354  const SDL_Rect bar_rect_1 {
355  hp_sep + 4,
356  6 + (fs + 2) * i,
357  bar_len,
358  8
359  };
360 
361  shape.clear();
362  shape["x"] = bar_rect_1.x;
363  shape["y"] = bar_rect_1.y;
364  shape["w"] = bar_rect_1.w;
365  shape["h"] = bar_rect_1.h;
366  shape["fill_color"] = row_color.to_rgba_string();
367 
368  cfg.add_child("rectangle", shape);
369 
370  ++i;
371  }
372 
373  hp_graph.append_drawing_data(cfg);
374 }
375 
377 {
378  hp_probability_vector res, temp_vec;
379 
380  // First, extract any relevant probability values
381  for(int i = 0; i < static_cast<int>(hp_dist.size()); ++i) {
382  const double prob = hp_dist[i];
383 
384  // We keep only values above 0.1%.
385  if(prob > 0.001) {
386  temp_vec.emplace_back(i, prob);
387  }
388  }
389 
390  // Then sort by descending probability.
391  std::sort(temp_vec.begin(), temp_vec.end(), [](const auto& pair1, const auto& pair2) {
392  return pair1.second > pair2.second;
393  });
394 
395  // Take only the highest probability values.;
396  std::copy_n(temp_vec.begin(), std::min<int>(graph_max_rows, temp_vec.size()), std::back_inserter(res));
397 
398  // Then, we sort the hitpoint values in descending order.
399  std::sort(res.begin(), res.end(), [](const auto& pair1, const auto& pair2) {
400  return pair1.first > pair2.first;
401  });
402 
403  return res;
404 }
405 
406 } // 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:1575
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:1582
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:159
void clear()
Definition: config.cpp:831
config & add_child(config_key_type key)
Definition: config.cpp:441
std::ostringstream wrapper.
Definition: formatter.hpp:40
A simple canvas which can be drawn upon.
Definition: canvas.hpp:44
void set_variable(const std::string &key, wfl::variant &&value)
Definition: canvas.hpp:153
void draw_hp_graph(drawing &hp_graph, const combatant_data &attacker, const combatant_data &defender) const
static const unsigned int graph_width
attack_predictions(battle_context &bc, unit_const_ptr attacker, unit_const_ptr defender)
void set_data(window &window, const combatant_data &attacker, const combatant_data &defender) const
static const unsigned int graph_height
virtual void pre_show(window &window) override
Actions to be taken before showing the window.
static const unsigned int graph_max_rows
hp_probability_vector get_hitpoint_probabilities(const std::vector< double > &hp_dist) const
Abstract base class for all modal dialogs.
A drawing is widget with a fixed size and gives access to the canvas of the widget in the window inst...
Definition: drawing.hpp:49
void append_drawing_data(const ::config &cfg)
Definition: drawing.hpp:63
canvas & get_drawing_canvas()
Definition: drawing.hpp:53
const std::string & id() const
Definition: widget.cpp:110
@ invisible
The user set the widget invisible, that means:
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition: window.hpp:63
const_iterator end() const
Definition: abilities.hpp:51
const_iterator begin() const
Definition: abilities.hpp:49
This class represents a single unit of a specific type.
Definition: unit.hpp:133
std::size_t i
Definition: function.cpp:968
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:1357
bool is_fearless() const
Gets whether this unit is fearless - ie, unaffected by time of day.
Definition: unit.hpp:1247
This file contains the window object, this object is a top level container which has the event manage...
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
std::string span_color(const color_t &color)
Returns a Pango formatting string using the provided color_t object.
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....
static std::string get_probability_string(const double prob)
REGISTER_DIALOG(tod_new_schedule)
std::vector< std::pair< int, double > > hp_probability_vector
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:95
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