The Battle for Wesnoth  1.19.10+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, const int leadership_bonus)
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  , leadership_bonus_(leadership_bonus)
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(const combatant_data& attacker, const combatant_data& defender, int leadership_bonus)
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 = [&, this](const std::string& id, const std::string& value) {
87  // MSVC does not compile without this-> (26-09-2024)
88  label& lbl = this->find_widget<label>(get_prefixed_widget_id(id));
89  lbl.set_label(value);
90  };
91 
92  const auto hide_label_helper = [&, this](const std::string& id) {
93  // MSVC does not compile without this-> (26-09-2024)
94  label& lbl = this->find_widget<label>(get_prefixed_widget_id(id));
96  label& lbl2 = this->find_widget<label>(get_prefixed_widget_id(id) + "_label");
98  };
99 
100  std::stringstream ss;
101 
102  //
103  // Always visible fields
104  //
105 
106  // Unscathed probability
107  const color_t ndc_color = game_config::red_to_green(attacker.combatant_.untouched * 100);
108 
110  set_label_helper("chance_unscathed", ss.str());
111 
112  // HP probability graph
113  drawing& graph_widget = find_widget<drawing>(get_prefixed_widget_id("hp_graph"));
114  draw_hp_graph(graph_widget, attacker, defender);
115 
116  //
117  // Weapon detail fields (only shown if a weapon is present)
118  //
119 
120  if(!attacker.stats_.weapon) {
121  set_label_helper("base_damage", _("No usable weapon"));
122 
123  // FIXME: would rather have a list somewhere that I can loop over instead of hardcoding...
124  hide_label_helper("tod_modifier");
125  hide_label_helper("leadership_modifier");
126  hide_label_helper("slowed_modifier");
127 
128  return;
129  }
130 
131  ss.str("");
132 
133  // Set specials context (for safety, it should not have changed normally).
134  const_attack_ptr weapon = attacker.stats_.weapon, opp_weapon = defender.stats_.weapon;
135  auto ctx = weapon->specials_context(attacker.unit_, defender.unit_, attacker.unit_->get_location(), defender.unit_->get_location(), attacker.stats_.is_attacker, opp_weapon);
136  utils::optional<decltype(ctx)> opp_ctx;
137 
138  if(opp_weapon) {
139  opp_ctx.emplace(opp_weapon->specials_context(defender.unit_, attacker.unit_, defender.unit_->get_location(), attacker.unit_->get_location(), defender.stats_.is_attacker, weapon));
140  }
141 
142  // Get damage modifiers.
143  unit_ability_list dmg_specials = weapon->get_specials_and_abilities("damage");
144  unit_abilities::effect dmg_effect(dmg_specials, weapon->damage());
145 
146  // Get the SET damage modifier, if any.
147  auto set_dmg_effect = std::find_if(dmg_effect.begin(), dmg_effect.end(),
148  [](const unit_abilities::individual_effect& e) { return e.type == unit_abilities::SET; }
149  );
150 
151  // Either user the SET modifier or the base weapon damage.
152  if(set_dmg_effect == dmg_effect.end()) {
153  ss << weapon->damage() << " (" << markup::italic(weapon->name()) << ")";
154  } else {
155  assert(set_dmg_effect->ability);
156  ss << set_dmg_effect->value << " (" << markup::italic((*set_dmg_effect->ability)["name"]) << ")";
157  }
158 
159  // Process the ADD damage modifiers.
160  for(const auto& e : dmg_effect) {
161  if(e.type == unit_abilities::ADD) {
162  ss << "\n";
163 
164  if(e.value >= 0) {
165  ss << '+';
166  }
167 
168  ss << e.value;
169  ss << " (" << markup::italic((*e.ability)["name"]) << ")";
170  }
171  }
172 
173  // Process the MUL damage modifiers.
174  for(const auto& e : dmg_effect) {
175  if(e.type == unit_abilities::MUL) {
176  ss << "\n";
177  ss << font::unicode_multiplication_sign << (e.value / 100);
178 
179  if(e.value % 100) {
180  ss << "." << ((e.value % 100) / 10);
181  if(e.value % 10) {
182  ss << (e.value % 10);
183  }
184  }
185 
186  ss << " (" << markup::italic((*e.ability)["name"]) << ")";
187  }
188  }
189 
190  set_label_helper("base_damage", ss.str());
191 
192  ss.str("");
193 
194  // Resistance modifier.
195  const int resistance_modifier = defender.unit_->damage_from(*weapon, !attacker.stats_.is_attacker, defender.unit_->get_location(), opp_weapon);
196  if(resistance_modifier != 100) {
197  if(attacker.stats_.is_attacker) {
198  if(resistance_modifier < 100) {
199  ss << _("Defender resistance vs") << " ";
200  } else {
201  ss << _("Defender vulnerability vs") << " ";
202  }
203  } else {
204  if(resistance_modifier < 100) {
205  ss << _("Attacker resistance vs") << " ";
206  } else {
207  ss << _("Attacker vulnerability vs") << " ";
208  }
209  }
210 
211  ss << string_table["type_" + weapon->effective_damage_type().first];
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  unit_alignments::type alignment = weapon->alignment().value_or(u.alignment());
229  const int tod_modifier = combat_modifier(resources::gameboard->units(), resources::gameboard->map(),
230  u.get_location(), alignment, u.is_fearless());
231 
232  if(tod_modifier != 0) {
233  set_label_helper("tod_modifier", utils::signed_percent(tod_modifier));
234  } else {
235  hide_label_helper("tod_modifier");
236  }
237 
238  // Leadership bonus.
239  // defender unit won't move before attack so just do calculation here
240  if (leadership_bonus == 0){
241  leadership_bonus = under_leadership(*attacker.unit_, attacker.unit_->get_location(), weapon, opp_weapon);
242  }
243  if(leadership_bonus != 0) {
244  set_label_helper("leadership_modifier", utils::signed_percent(leadership_bonus));
245  } else {
246  hide_label_helper("leadership_modifier");
247  }
248 
249  // Slowed penalty.
250  if(attacker.stats_.is_slowed) {
251  set_label_helper("slowed_modifier", "/ 2");
252  } else {
253  hide_label_helper("slowed_modifier");
254  }
255 
256  // Total damage.
257  const int base_damage = weapon->damage();
258 
259  color_t dmg_color = font::weapon_color;
260  if(attacker.stats_.damage > base_damage) {
261  dmg_color = font::good_dmg_color;
262  } else if(attacker.stats_.damage < base_damage) {
263  dmg_color = font::bad_dmg_color;
264  }
265 
266  ss << markup::span_color(dmg_color, attacker.stats_.damage)
268 
269  set_label_helper("total_damage", ss.str());
270 
271  // Chance to hit
272  const color_t cth_color = game_config::red_to_green(attacker.stats_.chance_to_hit);
273 
274  ss.str("");
275  ss << markup::span_color(cth_color, attacker.stats_.chance_to_hit, "%");
276 
277  set_label_helper("chance_to_hit", ss.str());
278 }
279 
280 void attack_predictions::draw_hp_graph(drawing& hp_graph, const combatant_data& attacker, const combatant_data& defender)
281 {
282  // Font size. If you change this, you must update the separator space.
283  // TODO: probably should remove this.
284  const int fs = font::SIZE_SMALL;
285 
286  // Space before HP separator.
287  const int hp_sep = 30;
288 
289  // Space after percentage separator.
290  const int percent_sep = 50;
291 
292  // Bar space between both separators.
293  const int bar_space = graph_width - hp_sep - percent_sep - 4;
294 
295  // Set some variables for the WML portion of the graph to use.
296  canvas& hp_graph_canvas = hp_graph.get_drawing_canvas();
297 
298  hp_graph_canvas.set_variable("hp_column_width", wfl::variant(hp_sep));
299  hp_graph_canvas.set_variable("chance_column_width", wfl::variant(percent_sep));
300 
301  config cfg, shape;
302 
303  int i = 0;
304 
305  // Draw the rows (lower HP values are at the bottom).
306  for(const auto& probability : get_hitpoint_probabilities(attacker.combatant_.hp_dist)) {
307 
308  // Get the HP and probability.
309  auto [hp, prob] = probability;
310 
311  color_t row_color;
312 
313  // Death line is red.
314  if(hp == 0) {
315  row_color = {229, 0, 0};
316  }
317 
318  // Below current hitpoints value is orange.
319  else if(hp < static_cast<int>(attacker.stats_.hp)) {
320  // Stone is grey.
321  if(defender.stats_.petrifies) {
322  row_color = {154, 154, 154};
323  } else {
324  row_color = {244, 201, 0};
325  }
326  }
327 
328  // Current hitpoints value and above is green.
329  else {
330  row_color = {8, 202, 0};
331  }
332 
333  shape["text"] = hp;
334  shape["x"] = 4;
335  shape["y"] = 2 + (fs + 2) * i;
336  shape["w"] = "(text_width)";
337  shape["h"] = "(text_height)";
338  shape["font_size"] = 12;
339  shape["color"] = "255, 255, 255, 255";
340  shape["text_alignment"] = "(text_alignment)";
341 
342  cfg.add_child("text", shape);
343 
344  shape.clear();
345  shape["text"] = get_probability_string(prob);
346  shape["x"] = graph_width - percent_sep + 2;
347  shape["y"] = 2 + (fs + 2) * i;
348  shape["w"] = "(text_width)";
349  shape["h"] = "(text_height)";
350  shape["font_size"] = 12;
351  shape["color"] = "255, 255, 255, 255";
352  shape["text_alignment"] = "(text_alignment)";
353 
354  cfg.add_child("text", shape);
355 
356  const int bar_len = std::max(static_cast<int>((prob * (bar_space - 4)) + 0.5), 2);
357 
358  const SDL_Rect bar_rect_1 {
359  hp_sep + 4,
360  6 + (fs + 2) * i,
361  bar_len,
362  8
363  };
364 
365  shape.clear();
366  shape["x"] = bar_rect_1.x;
367  shape["y"] = bar_rect_1.y;
368  shape["w"] = bar_rect_1.w;
369  shape["h"] = bar_rect_1.h;
370  shape["fill_color"] = row_color.to_rgba_string();
371 
372  cfg.add_child("rectangle", shape);
373 
374  ++i;
375  }
376 
377  hp_graph.append_drawing_data(cfg);
378 }
379 
381 {
382  hp_probability_vector res, temp_vec;
383 
384  // First, extract any relevant probability values
385  for(int i = 0; i < static_cast<int>(hp_dist.size()); ++i) {
386  const double prob = hp_dist[i];
387 
388  // We keep only values above 0.1%.
389  if(prob > 0.001) {
390  temp_vec.emplace_back(i, prob);
391  }
392  }
393 
394  // Then sort by descending probability.
395  std::sort(temp_vec.begin(), temp_vec.end(), [](const auto& pair1, const auto& pair2) {
396  return pair1.second > pair2.second;
397  });
398 
399  // Take only the highest probability values.;
400  std::copy_n(temp_vec.begin(), std::min<int>(graph_max_rows, temp_vec.size()), std::back_inserter(res));
401 
402  // Then, we sort the hitpoint values in descending order.
403  std::sort(res.begin(), res.end(), [](const auto& pair1, const auto& pair2) {
404  return pair1.first > pair2.first;
405  });
406 
407  return res;
408 }
409 
410 } // 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:158
void clear()
Definition: config.cpp:824
config & add_child(config_key_type key)
Definition: config.cpp:436
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
void set_data(const combatant_data &attacker, const combatant_data &defender, int leadership_bonus=0)
void draw_hp_graph(drawing &hp_graph, const combatant_data &attacker, const combatant_data &defender)
static const unsigned int graph_height
attack_predictions(battle_context &bc, unit_const_ptr attacker, unit_const_ptr defender, const int leadership_bonus=0)
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:55
const_iterator begin() const
Definition: abilities.hpp:53
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:1030
static std::string _(const char *str)
Definition: gettext.hpp:97
unit_alignments::type alignment() const
The alignment of this unit.
Definition: unit.hpp:491
const map_location & get_location() const
The current map location this unit is at.
Definition: unit.hpp:1413
bool is_fearless() const
Gets whether this unit is fearless - ie, unaffected by time of day.
Definition: unit.hpp:1303
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
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:60
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