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