The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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"
24#include "formatter.hpp"
25#include "formula/variant.hpp"
26#include "game_board.hpp"
28#include "game_config.hpp"
30#include "gui/widgets/label.hpp"
31#include "gettext.hpp"
32#include "language.hpp"
33#include "resources.hpp"
34#include "units/abilities.hpp"
35#include "units/unit.hpp"
36
37#include <iomanip>
38#include <utility>
39
40namespace gui2::dialogs
41{
42
43REGISTER_DIALOG(attack_predictions)
44
45const unsigned int attack_predictions::graph_width = 270;
46const unsigned int attack_predictions::graph_height = 170;
47const unsigned int attack_predictions::graph_max_rows = 10;
48
50 battle_context& bc, unit_const_ptr attacker, unit_const_ptr defender)
51 : modal_dialog(window_id())
52 , attacker_data_(std::move(attacker), bc.get_attacker_combatant(), bc.get_attacker_stats())
53 , defender_data_(std::move(defender), bc.get_defender_combatant(), bc.get_defender_stats())
54{
55}
56
58{
59 label& title = find_widget<label>("title");
60 title.set_label(_("Damage Calculations") + " (" + rng_mode_name() + ")");
62
65}
66
68{
70
71 if(mode == "biased") {
72 return _("Reduced RNG");
73 }
74
75 if(mode == "deterministic") {
76 return _("Predictable RNG");
77 }
78
79 return _("Default RNG");
80}
81
83{
85
86 // These are the same strings as the options in the campaign selection dialog.
87 std::string description;
88 if(mode == "biased") {
89 // xgettext: no-c-format
90 description = _("Hits and misses are much more consistent. This tends to make small-scale engagements easier to plan.\n\nExample: if you strike three times with 50% accuracy, you will always hit at least once and miss at least once.");
91 } else if(mode == "deterministic") {
92 description = _("Identical to Default RNG, except loading a saved game will not change the outcome of an attack.\n\nExample: you strike twice and get lucky, hitting both strikes. You then load an earlier save and make the same attack again. Both strikes will still hit.");
93 } else {
94 // xgettext: no-c-format
95 description = _("Pure, unbiased randomness; the way Wesnoth is intended to be played.\n\nExample: if you strike twice with 50% accuracy, you’re most likely to hit once and miss once, but might also hit twice or miss twice.");
96 }
97
98 return rng_mode_name() + ": " + description;
99}
100
101static std::string get_probability_string(const double prob)
102{
103 std::ostringstream ss;
104
105 if(prob > 0.9995) {
106 ss << "100%";
107 } else {
108 ss << std::fixed << std::setprecision(1) << 100.0 * prob << '%';
109 }
110
111 return ss.str();
112}
113
114void attack_predictions::set_data(const combatant_data& attacker, const combatant_data& defender)
115{
116 // Each data widget in this dialog has its id prefixed by either of these identifiers.
117 const std::string widget_id_prefix = attacker.stats_.is_attacker ? "attacker" : "defender";
118
119 const auto get_prefixed_widget_id = [&widget_id_prefix](const std::string& id) {
120 return (formatter() << widget_id_prefix << "_" << id).str();
121 };
122
123 // Helpers for setting or hiding labels
124 const auto set_label_helper = [&, this](const std::string& id, const std::string& value) {
125 // MSVC does not compile without this-> (26-09-2024)
126 label& lbl = this->find_widget<label>(get_prefixed_widget_id(id));
127 lbl.set_label(value);
128 };
129
130 const auto hide_label_helper = [&, this](const std::string& id) {
131 // MSVC does not compile without this-> (26-09-2024)
132 label& lbl = this->find_widget<label>(get_prefixed_widget_id(id));
134 label& lbl2 = this->find_widget<label>(get_prefixed_widget_id(id) + "_label");
136 };
137
138 std::stringstream ss;
139
140 //
141 // Always visible fields
142 //
143
144 // Unscathed probability
145 const color_t ndc_color = game_config::red_to_green(attacker.combatant_.untouched * 100);
146
148 set_label_helper("chance_unscathed", ss.str());
149
150 // HP probability graph
151 drawing& graph_widget = find_widget<drawing>(get_prefixed_widget_id("hp_graph"));
152 draw_hp_graph(graph_widget, attacker, defender);
153
154 //
155 // Weapon detail fields (only shown if a weapon is present)
156 //
157
158 if(!attacker.stats_.weapon) {
159 set_label_helper("base_damage", _("No usable weapon"));
160
161 // FIXME: would rather have a list somewhere that I can loop over instead of hardcoding...
162 hide_label_helper("tod_modifier");
163 hide_label_helper("leadership_modifier");
164 hide_label_helper("slowed_modifier");
165
166 return;
167 }
168
169 ss.str("");
170
171 // Set specials context (for safety, it should not have changed normally).
172 const_attack_ptr weapon = attacker.stats_.weapon, opp_weapon = defender.stats_.weapon;
173
174 auto ctx = specials_context_t::make(
175 { attacker.unit_, attacker.unit_->get_location(), weapon },
176 { defender.unit_, defender.unit_->get_location(), opp_weapon }, attacker.stats_.is_attacker);
177
178 // Get damage modifiers.
179 active_ability_list dmg_specials = weapon->get_specials_and_abilities("damage");
180 unit_abilities::effect dmg_effect(dmg_specials, weapon->damage());
181
182 // Get the SET damage modifier, if any.
183 auto set_dmg_effect = std::find_if(dmg_effect.begin(), dmg_effect.end(),
184 [](const unit_abilities::individual_effect& e) { return e.type == unit_abilities::SET; }
185 );
186
187 // Either user the SET modifier or the base weapon damage.
188 if(set_dmg_effect == dmg_effect.end()) {
189 ss << weapon->damage() << " (" << markup::italic(weapon->name()) << ")";
190 } else {
191 assert(set_dmg_effect->ability);
192 ss << set_dmg_effect->value << " (" << markup::italic((*set_dmg_effect->ability)["name"]) << ")";
193 }
194
195 // Process the ADD damage modifiers.
196 for(const auto& e : dmg_effect) {
197 if(e.type == unit_abilities::ADD) {
198 ss << "\n";
199
200 if(e.value >= 0) {
201 ss << '+';
202 }
203
204 ss << e.value;
205 ss << " (" << markup::italic((*e.ability)["name"]) << ")";
206 }
207 }
208
209 // Process the MUL damage modifiers.
210 for(const auto& e : dmg_effect) {
211 if(e.type == unit_abilities::MUL) {
212 ss << "\n";
213 ss << font::unicode_multiplication_sign << (e.value / 100);
214
215 if(e.value % 100) {
216 ss << "." << ((e.value % 100) / 10);
217 if(e.value % 10) {
218 ss << (e.value % 10);
219 }
220 }
221
222 ss << " (" << markup::italic((*e.ability)["name"]) << ")";
223 }
224 }
225
226 set_label_helper("base_damage", ss.str());
227
228 ss.str("");
229
230 // Resistance modifier.
231 const auto [damage_type, resistance_modifier] = weapon->effective_damage_type();
232 if(resistance_modifier != 100) {
233 if(attacker.stats_.is_attacker) {
234 if(resistance_modifier < 100) {
235 ss << _("Defender resistance vs") << " ";
236 } else {
237 ss << _("Defender vulnerability vs") << " ";
238 }
239 } else {
240 if(resistance_modifier < 100) {
241 ss << _("Attacker resistance vs") << " ";
242 } else {
243 ss << _("Attacker vulnerability vs") << " ";
244 }
245 }
246
247 ss << string_table["type_" + damage_type];
248
249 set_label_helper("resis_label", ss.str());
250
251 ss.str("");
252 ss << font::unicode_multiplication_sign << (resistance_modifier / 100) << "." << ((resistance_modifier % 100) / 10);
253
254 set_label_helper("resis", ss.str());
255 }
256
257 ss.str("");
258
259 // TODO: color format the modifiers
260
261 // Time of day modifier.
262 const unit& u = *attacker.unit_;
263
264 unit_alignments::type alignment = weapon->alignment().value_or(u.alignment());
265 const int tod_modifier = combat_modifier(resources::gameboard->units(), resources::gameboard->map(),
266 u.get_location(), alignment, u.is_fearless());
267
268 if(tod_modifier != 0) {
269 set_label_helper("tod_modifier", utils::signed_percent(tod_modifier));
270 } else {
271 hide_label_helper("tod_modifier");
272 }
273
274 if(attacker.stats_.leadership_bonus != 0) {
275 set_label_helper("leadership_modifier", utils::signed_percent(attacker.stats_.leadership_bonus));
276 } else {
277 hide_label_helper("leadership_modifier");
278 }
279
280 // Slowed penalty.
281 if(attacker.stats_.is_slowed) {
282 set_label_helper("slowed_modifier", "/ 2");
283 } else {
284 hide_label_helper("slowed_modifier");
285 }
286
287 // Total damage.
288 const int base_damage = weapon->damage();
289
290 color_t dmg_color = font::weapon_color;
291 if(attacker.stats_.damage > base_damage) {
292 dmg_color = font::good_dmg_color;
293 } else if(attacker.stats_.damage < base_damage) {
294 dmg_color = font::bad_dmg_color;
295 }
296
297 ss << markup::span_color(dmg_color, attacker.stats_.damage)
299
300 set_label_helper("total_damage", ss.str());
301
302 // Chance to hit
303 const color_t cth_color = game_config::red_to_green(attacker.stats_.chance_to_hit);
304
305 ss.str("");
306 ss << markup::span_color(cth_color, attacker.stats_.chance_to_hit, "%");
307
308 set_label_helper("chance_to_hit", ss.str());
309}
310
311void attack_predictions::draw_hp_graph(drawing& hp_graph, const combatant_data& attacker, const combatant_data& defender)
312{
313 // Font size. If you change this, you must update the separator space.
314 // TODO: probably should remove this.
315 const int fs = font::SIZE_SMALL;
316
317 // Space before HP separator.
318 const int hp_sep = 30;
319
320 // Space after percentage separator.
321 const int percent_sep = 50;
322
323 // Bar space between both separators.
324 const int bar_space = graph_width - hp_sep - percent_sep - 4;
325
326 // Set some variables for the WML portion of the graph to use.
327 canvas& hp_graph_canvas = hp_graph.get_drawing_canvas();
328
329 hp_graph_canvas.set_variable("hp_column_width", wfl::variant(hp_sep));
330 hp_graph_canvas.set_variable("chance_column_width", wfl::variant(percent_sep));
331
332 config cfg, shape;
333
334 int i = 0;
335
336 // Draw the rows (lower HP values are at the bottom).
337 for(const auto& probability : get_hitpoint_probabilities(attacker.combatant_.hp_dist)) {
338
339 // Get the HP and probability.
340 auto [hp, prob] = probability;
341
342 color_t row_color;
343
344 // Death line is red.
345 if(hp == 0) {
346 row_color = {229, 0, 0};
347 }
348
349 // Below current hitpoints value is orange.
350 else if(hp < static_cast<int>(attacker.stats_.hp)) {
351 // Stone is grey.
352 if(defender.stats_.petrifies) {
353 row_color = {154, 154, 154};
354 } else {
355 row_color = {244, 201, 0};
356 }
357 }
358
359 // Current hitpoints value and above is green.
360 else {
361 row_color = {8, 202, 0};
362 }
363
364 shape["text"] = hp;
365 shape["x"] = 4;
366 shape["y"] = 2 + (fs + 2) * i;
367 shape["w"] = "(text_width)";
368 shape["h"] = "(text_height)";
369 shape["font_size"] = 12;
370 shape["color"] = "255, 255, 255, 255";
371 shape["text_alignment"] = "(text_alignment)";
372
373 cfg.add_child("text", shape);
374
375 shape.clear();
376 shape["text"] = get_probability_string(prob);
377 shape["x"] = graph_width - percent_sep + 2;
378 shape["y"] = 2 + (fs + 2) * i;
379 shape["w"] = "(text_width)";
380 shape["h"] = "(text_height)";
381 shape["font_size"] = 12;
382 shape["color"] = "255, 255, 255, 255";
383 shape["text_alignment"] = "(text_alignment)";
384
385 cfg.add_child("text", shape);
386
387 const int bar_len = std::max(static_cast<int>((prob * (bar_space - 4)) + 0.5), 2);
388
389 const rect bar_rect_1 {
390 hp_sep + 4,
391 6 + (fs + 2) * i,
392 bar_len,
393 8
394 };
395
396 shape.clear();
397 shape["x"] = bar_rect_1.x;
398 shape["y"] = bar_rect_1.y;
399 shape["w"] = bar_rect_1.w;
400 shape["h"] = bar_rect_1.h;
401 shape["fill_color"] = row_color.to_rgba_string();
402
403 cfg.add_child("rectangle", shape);
404
405 ++i;
406 }
407
408 hp_graph.append_drawing_data(cfg);
409}
410
412{
413 hp_probability_vector res, temp_vec;
414
415 // First, extract any relevant probability values
416 for(int i = 0; i < static_cast<int>(hp_dist.size()); ++i) {
417 const double prob = hp_dist[i];
418
419 // We keep only values above 0.1%.
420 if(prob > 0.001) {
421 temp_vec.emplace_back(i, prob);
422 }
423 }
424
425 // Then sort by descending probability.
426 std::sort(temp_vec.begin(), temp_vec.end(), [](const auto& pair1, const auto& pair2) {
427 return pair1.second > pair2.second;
428 });
429
430 // Take only the highest probability values.;
431 std::copy_n(temp_vec.begin(), std::min<int>(graph_max_rows, temp_vec.size()), std::back_inserter(res));
432
433 // Then, we sort the hitpoint values in descending order.
434 std::sort(res.begin(), res.end(), [](const auto& pair1, const auto& pair2) {
435 return pair1.first > pair2.first;
436 });
437
438 return res;
439}
440
441} // 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:1518
Computes the statistics of a battle between an attacker and a defender unit.
Definition attack.hpp:183
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:165
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
void set_tooltip(const t_string &tooltip)
virtual void set_label(const t_string &text)
void set_visible(const visibility visible)
Definition widget.cpp:479
@ invisible
The user set the widget invisible, that means:
show_mode mode() const
Returns the dialog mode for this window.
Definition window.hpp:669
static specials_context_t make(specials_combatant &&self, specials_combatant &&other, bool attacking)
const_iterator end() const
const_iterator begin() const
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
static std::string _(const char *str)
Definition gettext.hpp:100
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
std::string id
Text to match against addon_info.tags()
Definition manager.cpp:199
auto string_table
Definition language.hpp:81
#define REGISTER_DIALOG(window_id)
Wrapper for REGISTER_DIALOG2.
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....
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
game_classification * classification
Definition resources.cpp:34
STL namespace.
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
std::unique_ptr< MIX_Audio, decltype(&MIX_DestroyAudio)> value
Definition sound.cpp:139
unsigned int num_blows
Effective number of blows, takes swarm into account.
Definition attack.hpp:99
bool petrifies
Attack petrifies opponent when it hits.
Definition attack.hpp:80
unsigned int hp
Hitpoints of the unit at the beginning of the battle.
Definition attack.hpp:92
bool is_attacker
True if the unit is the attacker.
Definition attack.hpp:75
const_attack_ptr weapon
The weapon used by the unit to attack the opponent, or nullptr if there is none.
Definition attack.hpp:73
bool is_slowed
True if the unit is slowed at the beginning of the battle.
Definition attack.hpp:77
int damage
Effective damage of the weapon (all factors accounted for).
Definition attack.hpp:95
unsigned int chance_to_hit
Effective chance to hit as a percentage (all factors accounted for).
Definition attack.hpp:94
The basic class for representing 8-bit RGB or RGBA colour values.
Definition color.hpp:51
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 i
#define e