Attack Predictions: added rest of the modifier fields and improved data readability
This commit is contained in:
parent
a3cd8224f6
commit
1387a647c3
2 changed files with 235 additions and 297 deletions
|
@ -8,9 +8,6 @@
|
|||
grow_factor = 0
|
||||
|
||||
[column]
|
||||
#border = "all"
|
||||
#border_size = 5
|
||||
|
||||
[spacer]
|
||||
height = 15
|
||||
[/spacer]
|
||||
|
@ -19,6 +16,55 @@
|
|||
[/row]
|
||||
#enddef
|
||||
|
||||
#define _GUI_DATA_LABEL_ROW _ID_PREFIX _ID _LABEL _DEFINITON
|
||||
|
||||
# FIXME: doesn't work :(
|
||||
#arg _DEFINITON
|
||||
"default_small"
|
||||
#endarg
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "left,right,top"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
id = {_ID_PREFIX} + "_" + {_ID} + "_label"
|
||||
definition = {_DEFINITON}
|
||||
|
||||
label = {_LABEL}
|
||||
|
||||
use_markup = true
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
#enddef
|
||||
|
||||
#define _GUI_DATA_VALUE_ROW _ID_PREFIX _ID
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "left,right,top"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
id = {_ID_PREFIX} + "_" + {_ID}
|
||||
definition = "default_small"
|
||||
|
||||
use_markup = true
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
#enddef
|
||||
|
||||
#
|
||||
# NOTE: every data row *must* have a corresponding value row. MUST!
|
||||
#
|
||||
#define _GUI_DATA_PANEL _ID_PREFIX
|
||||
[grid]
|
||||
|
||||
|
@ -31,72 +77,20 @@
|
|||
|
||||
[grid]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "left,right,top"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
label = _ "Base damage"
|
||||
definition = "default_small"
|
||||
linked_group = "header"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "left,right,top"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
#label = _ "Opponent resistance against"
|
||||
label = _ "Modifiers"
|
||||
definition = "default_small"
|
||||
linked_group = "header"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
{_GUI_DATA_LABEL_ROW ({_ID_PREFIX}) "base_damage" ( _ "Base damage") "default_small"}
|
||||
{_GUI_DATA_LABEL_ROW ({_ID_PREFIX}) "resis" () "default_small"}
|
||||
{_GUI_DATA_LABEL_ROW ({_ID_PREFIX}) "tod_modifier" ( _ "Time of day modifier") "default_small"}
|
||||
{_GUI_DATA_LABEL_ROW ({_ID_PREFIX}) "leadership_modifier" ( _ "Leadership bonus") "default_small"}
|
||||
{_GUI_DATA_LABEL_ROW ({_ID_PREFIX}) "slowed_modifier" ( _ "Slowed penalty") "default_small"}
|
||||
|
||||
{_GUI_SPACER_ROW}
|
||||
|
||||
[row]
|
||||
{_GUI_DATA_LABEL_ROW ({_ID_PREFIX}) "total_damage" ("<b>" + _ "Total damage" + "</b>") "default"}
|
||||
|
||||
[column]
|
||||
border = "left,right,top"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
{_GUI_SPACER_ROW}
|
||||
|
||||
[label]
|
||||
label = _ "Total damage"
|
||||
definition = "default_small"
|
||||
linked_group = "header"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "left,right,top"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
label = _ "Chance of being unscathed"
|
||||
definition = "default_small"
|
||||
linked_group = "header"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
{_GUI_DATA_LABEL_ROW ({_ID_PREFIX}) "chance_to_hit" ( _ "Chance to hit") "default_small"}
|
||||
{_GUI_DATA_LABEL_ROW ({_ID_PREFIX}) "chance_unscathed" ( _ "Chance of being unscathed") "default_small"}
|
||||
|
||||
[/grid]
|
||||
|
||||
|
@ -108,80 +102,20 @@
|
|||
vertical_grow = true
|
||||
|
||||
[grid]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "left,right,top"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
id = {_ID_PREFIX} + "_attack"
|
||||
definition = "default_small"
|
||||
linked_group = "header"
|
||||
|
||||
use_markup = "true"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "left,right,top"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
id = {_ID_PREFIX} + "_resis"
|
||||
definition = "default_small"
|
||||
linked_group = "header"
|
||||
|
||||
use_markup = "true"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
{_GUI_DATA_VALUE_ROW ({_ID_PREFIX}) "base_damage"}
|
||||
{_GUI_DATA_VALUE_ROW ({_ID_PREFIX}) "resis"}
|
||||
{_GUI_DATA_VALUE_ROW ({_ID_PREFIX}) "tod_modifier"}
|
||||
{_GUI_DATA_VALUE_ROW ({_ID_PREFIX}) "leadership_modifier"}
|
||||
{_GUI_DATA_VALUE_ROW ({_ID_PREFIX}) "slowed_modifier"}
|
||||
|
||||
{_GUI_SPACER_ROW}
|
||||
|
||||
[row]
|
||||
{_GUI_DATA_VALUE_ROW ({_ID_PREFIX}) "total_damage"}
|
||||
|
||||
[column]
|
||||
border = "left,right,top"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
{_GUI_SPACER_ROW}
|
||||
|
||||
[label]
|
||||
id = {_ID_PREFIX} + "_damage"
|
||||
definition = "default_small"
|
||||
linked_group = "header"
|
||||
|
||||
use_markup = "true"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
|
||||
[row]
|
||||
|
||||
[column]
|
||||
border = "left,right,top"
|
||||
border_size = 5
|
||||
horizontal_alignment = "left"
|
||||
|
||||
[label]
|
||||
id = {_ID_PREFIX} + "_no_damage_chance"
|
||||
definition = "default_small"
|
||||
linked_group = "header"
|
||||
|
||||
use_markup = "true"
|
||||
[/label]
|
||||
[/column]
|
||||
|
||||
[/row]
|
||||
{_GUI_DATA_VALUE_ROW ({_ID_PREFIX}) "chance_to_hit"}
|
||||
{_GUI_DATA_VALUE_ROW ({_ID_PREFIX}) "chance_unscathed"}
|
||||
|
||||
[/grid]
|
||||
|
||||
|
@ -315,36 +249,6 @@
|
|||
fixed_width = "true"
|
||||
[/linked_group]
|
||||
|
||||
[linked_group]
|
||||
id = "header"
|
||||
fixed_height = "true"
|
||||
[/linked_group]
|
||||
|
||||
[linked_group]
|
||||
id = "attack"
|
||||
fixed_height = "true"
|
||||
[/linked_group]
|
||||
|
||||
[linked_group]
|
||||
id = "resis"
|
||||
fixed_height = "true"
|
||||
[/linked_group]
|
||||
|
||||
[linked_group]
|
||||
id = "damage"
|
||||
fixed_height = "true"
|
||||
[/linked_group]
|
||||
|
||||
[linked_group]
|
||||
id = "chance_unscathed"
|
||||
fixed_height = "true"
|
||||
[/linked_group]
|
||||
|
||||
[linked_group]
|
||||
id = "bonuses"
|
||||
fixed_height = "true"
|
||||
[/linked_group]
|
||||
|
||||
[tooltip]
|
||||
id = "tooltip"
|
||||
[/tooltip]
|
||||
|
@ -436,7 +340,9 @@
|
|||
|
||||
[/window]
|
||||
|
||||
#undef _GUI_DATA_COLUMN
|
||||
#undef _GUI_HP_GRAPH
|
||||
#undef _GUI_DATA_PANEL
|
||||
#undef _GUI_SPACER_ROW
|
||||
#undef _GUI_DATA_LABEL_ROW
|
||||
#undef _GUI_DATA_VALUE_ROW
|
||||
#undef _GUI_DATA_COLUMN
|
||||
#undef _GUI_DATA_PANEL
|
||||
#undef _GUI_HP_GRAPH
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
#include "font/text_formatting.hpp"
|
||||
#include "formatter.hpp"
|
||||
#include "formula/variant.hpp"
|
||||
#include "game_board.hpp"
|
||||
#include "game_config.hpp"
|
||||
#include "gui/auxiliary/find_widget.hpp"
|
||||
#include "gui/widgets/drawing.hpp"
|
||||
|
@ -30,6 +31,7 @@
|
|||
#include "gui/widgets/window.hpp"
|
||||
#include "gettext.hpp"
|
||||
#include "language.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "units/abilities.hpp"
|
||||
#include "units/unit.hpp"
|
||||
|
||||
|
@ -73,148 +75,178 @@ static std::string get_probability_string(const double prob)
|
|||
|
||||
void attack_predictions::set_data(window& window, const combatant_data& attacker, const combatant_data& defender)
|
||||
{
|
||||
const std::string widget_id_prefix = attacker.stats_.is_attacker ? "attacker_" : "defender_";
|
||||
// Each data widget in this dialog has its id prefixed by either of these identifiers.
|
||||
const std::string widget_id_prefix = attacker.stats_.is_attacker ? "attacker" : "defender";
|
||||
|
||||
const auto get_prefixed_widget_id = [&widget_id_prefix](const std::string& id) {
|
||||
return formatter() << widget_id_prefix << "_" << id;
|
||||
};
|
||||
|
||||
// Helpers for setting or hiding labels
|
||||
const auto set_label_helper = [&](const std::string& id, const std::string& value) {
|
||||
find_widget<label>(&window, widget_id_prefix + id, false).set_label(value);
|
||||
find_widget<label>(&window, get_prefixed_widget_id(id), false).set_label(value);
|
||||
};
|
||||
|
||||
const auto hide_label_helper = [&](const std::string& id) {
|
||||
find_widget<label>(&window, get_prefixed_widget_id(id), false).set_visible(widget::visibility::invisible);
|
||||
find_widget<label>(&window, get_prefixed_widget_id(id) + "_label" , false).set_visible(widget::visibility::invisible);
|
||||
};
|
||||
|
||||
std::stringstream ss;
|
||||
|
||||
// With a weapon.
|
||||
if(attacker.stats_.weapon) {
|
||||
// Set specials context (for safety, it should not have changed normally).
|
||||
const attack_type* weapon = attacker.stats_.weapon;
|
||||
weapon->set_specials_context(attacker.unit_.get_location(), defender.unit_.get_location(), attacker.stats_.is_attacker, defender.stats_.weapon);
|
||||
|
||||
// Get damage modifiers.
|
||||
unit_ability_list dmg_specials = weapon->get_specials("damage");
|
||||
unit_abilities::effect dmg_effect(dmg_specials, weapon->damage(), attacker.stats_.backstab_pos);
|
||||
|
||||
// Get the SET damage modifier, if any.
|
||||
auto set_dmg_effect =
|
||||
std::find_if(dmg_effect.begin(), dmg_effect.end(), [](const unit_abilities::individual_effect& e) { return e.type == unit_abilities::SET; });
|
||||
|
||||
// Either user the SET modifier or the base weapon damage.
|
||||
if(set_dmg_effect == dmg_effect.end()) {
|
||||
// TODO: formatting
|
||||
ss << weapon->name() << ": " << weapon->damage();
|
||||
} else {
|
||||
assert(set_dmg_effect->ability);
|
||||
ss << (*set_dmg_effect->ability)["name"] << ": " << set_dmg_effect->value;
|
||||
}
|
||||
|
||||
// Process the ADD damage modifiers.
|
||||
for(const auto& e : dmg_effect) {
|
||||
if(e.type == unit_abilities::ADD) {
|
||||
ss << "\n";
|
||||
ss << (*e.ability)["name"] << ": ";
|
||||
|
||||
if(e.value >= 0) {
|
||||
ss << '+';
|
||||
}
|
||||
|
||||
ss << e.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Process the MUL damage modifiers.
|
||||
for(const auto& e : dmg_effect) {
|
||||
if(e.type == unit_abilities::MUL) {
|
||||
ss << "\n";
|
||||
ss << (*e.ability)["name"] << ": " << font::unicode_multiplication_sign << (e.value / 100);
|
||||
|
||||
if(e.value % 100) {
|
||||
ss << "." << ((e.value % 100) / 10);
|
||||
if(e.value % 10) {
|
||||
ss << (e.value % 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set_label_helper("attack", ss.str());
|
||||
|
||||
#if 0
|
||||
// Time of day modifier.
|
||||
int tod_modifier = combat_modifier(resources::gameboard->units(), resources::gameboard->map(), attacker.unit_.get_location(), u.alignment(), u.is_fearless());
|
||||
if(tod_modifier != 0) {
|
||||
left_strings.push_back(_("Time of day"));
|
||||
str.str("");
|
||||
ss << utils::signed_percent(tod_modifier);
|
||||
right_strings.push_back(str.str());
|
||||
}
|
||||
|
||||
// Leadership bonus.
|
||||
int leadership_bonus = 0;
|
||||
under_leadership(resources::gameboard->units(), attacker.unit_.get_location(), &leadership_bonus);
|
||||
if(leadership_bonus != 0) {
|
||||
left_strings.push_back(_("Leadership"));
|
||||
str.str("");
|
||||
ss << utils::signed_percent(leadership_bonus);
|
||||
right_strings.push_back(str.str());
|
||||
}
|
||||
#endif
|
||||
|
||||
ss.str("");
|
||||
|
||||
// Resistance modifier.
|
||||
const int resistance_modifier = defender.unit_.damage_from(*weapon, !attacker.stats_.is_attacker, defender.unit_.get_location());
|
||||
if(resistance_modifier != 100) {
|
||||
// TODO
|
||||
//if(attacker.stats_.is_attacker) {
|
||||
// ss << "Defender resistance vs ";
|
||||
//} else {
|
||||
// ss << "Attacker vulnerability vs ";
|
||||
//}
|
||||
|
||||
ss << string_table["type_" + weapon->type()] << ": ";
|
||||
ss << font::unicode_multiplication_sign << (resistance_modifier / 100) << "." << ((resistance_modifier % 100) / 10);
|
||||
|
||||
set_label_helper("resis", ss.str());
|
||||
}
|
||||
|
||||
// Slowed penalty.
|
||||
#if 0
|
||||
if(attacker.stats_.is_slowed) {
|
||||
left_strings.push_back(_("Slowed"));
|
||||
right_strings.push_back("/ 2");
|
||||
}
|
||||
#endif
|
||||
|
||||
ss.str("");
|
||||
|
||||
// Total damage.
|
||||
const int base_damage = weapon->damage();
|
||||
|
||||
color_t dmg_color = font::weapon_color;
|
||||
if(attacker.stats_.damage > base_damage) {
|
||||
dmg_color = font::good_dmg_color;
|
||||
} else if(attacker.stats_.damage < base_damage) {
|
||||
dmg_color = font::bad_dmg_color;
|
||||
}
|
||||
|
||||
const color_t cth_color = game_config::red_to_green(attacker.stats_.chance_to_hit);
|
||||
|
||||
ss << font::span_color(dmg_color) << attacker.stats_.damage << "</span>"
|
||||
<< font::weapon_numbers_sep << attacker.stats_.num_blows << " ("
|
||||
<< font::span_color(cth_color) << attacker.stats_.chance_to_hit << "%</span>)";
|
||||
|
||||
set_label_helper("damage", ss.str());
|
||||
} else {
|
||||
// Without a weapon.
|
||||
set_label_helper("attack", _("No usable weapon"));
|
||||
}
|
||||
//
|
||||
// Always visible fields
|
||||
//
|
||||
|
||||
// Unscathed probability
|
||||
const color_t ndc_color = game_config::red_to_green(attacker.combatant_.untouched * 100);
|
||||
|
||||
// Unscathed probability.
|
||||
set_label_helper("no_damage_chance",
|
||||
(formatter() << font::span_color(ndc_color) << get_probability_string(attacker.combatant_.untouched) << "</span>").str());
|
||||
|
||||
drawing& graph_widget = find_widget<drawing>(&window, widget_id_prefix + "hp_graph", false);
|
||||
ss << font::span_color(ndc_color) << get_probability_string(attacker.combatant_.untouched) << "</span>";
|
||||
set_label_helper("chance_unscathed", ss.str());
|
||||
|
||||
// HP probability graph
|
||||
drawing& graph_widget = find_widget<drawing>(&window, get_prefixed_widget_id("hp_graph"), false);
|
||||
draw_hp_graph(graph_widget, attacker, defender);
|
||||
|
||||
//
|
||||
// Weapon detail fields (only shown if a weapon is present)
|
||||
//
|
||||
|
||||
if(!attacker.stats_.weapon) {
|
||||
set_label_helper("attack", _("No usable weapon"));
|
||||
return;
|
||||
}
|
||||
|
||||
ss.str("");
|
||||
|
||||
// Set specials context (for safety, it should not have changed normally).
|
||||
const attack_type* weapon = attacker.stats_.weapon;
|
||||
weapon->set_specials_context(attacker.unit_.get_location(), defender.unit_.get_location(), attacker.stats_.is_attacker, defender.stats_.weapon);
|
||||
|
||||
// Get damage modifiers.
|
||||
unit_ability_list dmg_specials = weapon->get_specials("damage");
|
||||
unit_abilities::effect dmg_effect(dmg_specials, weapon->damage(), attacker.stats_.backstab_pos);
|
||||
|
||||
// Get the SET damage modifier, if any.
|
||||
auto set_dmg_effect = std::find_if(dmg_effect.begin(), dmg_effect.end(),
|
||||
[](const unit_abilities::individual_effect& e) { return e.type == unit_abilities::SET; }
|
||||
);
|
||||
|
||||
// Either user the SET modifier or the base weapon damage.
|
||||
if(set_dmg_effect == dmg_effect.end()) {
|
||||
ss << weapon->damage() << " (<i>" << weapon->name() << "</i>)";
|
||||
} else {
|
||||
assert(set_dmg_effect->ability);
|
||||
ss << set_dmg_effect->value << " (" << (*set_dmg_effect->ability)["name"] << ")";
|
||||
}
|
||||
|
||||
// Process the ADD damage modifiers.
|
||||
for(const auto& e : dmg_effect) {
|
||||
if(e.type == unit_abilities::ADD) {
|
||||
ss << "\n";
|
||||
ss << (*e.ability)["name"] << ": ";
|
||||
|
||||
if(e.value >= 0) {
|
||||
ss << '+';
|
||||
}
|
||||
|
||||
ss << e.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Process the MUL damage modifiers.
|
||||
for(const auto& e : dmg_effect) {
|
||||
if(e.type == unit_abilities::MUL) {
|
||||
ss << "\n";
|
||||
ss << (*e.ability)["name"] << ": " << font::unicode_multiplication_sign << (e.value / 100);
|
||||
|
||||
if(e.value % 100) {
|
||||
ss << "." << ((e.value % 100) / 10);
|
||||
if(e.value % 10) {
|
||||
ss << (e.value % 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set_label_helper("base_damage", ss.str());
|
||||
|
||||
ss.str("");
|
||||
|
||||
// Resistance modifier.
|
||||
const int resistance_modifier = defender.unit_.damage_from(*weapon, !attacker.stats_.is_attacker, defender.unit_.get_location());
|
||||
if(resistance_modifier != 100) {
|
||||
if(attacker.stats_.is_attacker) {
|
||||
ss << _("Defender resistance vs") << " ";
|
||||
} else {
|
||||
ss << _("Attacker vulnerability vs") << " ";
|
||||
}
|
||||
|
||||
ss << string_table["type_" + weapon->type()];
|
||||
|
||||
set_label_helper("resis_label", ss.str());
|
||||
|
||||
ss.str("");
|
||||
ss << font::unicode_multiplication_sign << (resistance_modifier / 100) << "." << ((resistance_modifier % 100) / 10);
|
||||
|
||||
set_label_helper("resis", ss.str());
|
||||
}
|
||||
|
||||
ss.str("");
|
||||
|
||||
// TODO: color format the modifiers
|
||||
|
||||
// Time of day modifier.
|
||||
const unit& u = attacker.unit_;
|
||||
|
||||
const int tod_modifier = combat_modifier(resources::gameboard->units(), resources::gameboard->map(),
|
||||
u.get_location(), u.alignment(), u.is_fearless());
|
||||
|
||||
if(tod_modifier != 0) {
|
||||
set_label_helper("tod_modifier", utils::signed_percent(tod_modifier));
|
||||
} else {
|
||||
hide_label_helper("tod_modifier");
|
||||
}
|
||||
|
||||
// Leadership bonus.
|
||||
int leadership_bonus = 0;
|
||||
under_leadership(resources::gameboard->units(), attacker.unit_.get_location(), &leadership_bonus);
|
||||
|
||||
if(leadership_bonus != 0) {
|
||||
set_label_helper("leadership_modifier", utils::signed_percent(leadership_bonus));
|
||||
} else {
|
||||
hide_label_helper("leadership_modifier");
|
||||
}
|
||||
|
||||
// Slowed penalty.
|
||||
if(attacker.stats_.is_slowed) {
|
||||
set_label_helper("slowed_modifier", "/ 2");
|
||||
} else {
|
||||
hide_label_helper("slowed_modifier");
|
||||
}
|
||||
|
||||
// Total damage.
|
||||
const int base_damage = weapon->damage();
|
||||
|
||||
color_t dmg_color = font::weapon_color;
|
||||
if(attacker.stats_.damage > base_damage) {
|
||||
dmg_color = font::good_dmg_color;
|
||||
} else if(attacker.stats_.damage < base_damage) {
|
||||
dmg_color = font::bad_dmg_color;
|
||||
}
|
||||
|
||||
ss << font::span_color(dmg_color) << attacker.stats_.damage << "</span>"
|
||||
<< font::weapon_numbers_sep << attacker.stats_.num_blows;
|
||||
|
||||
set_label_helper("total_damage", ss.str());
|
||||
|
||||
// Chance to hit
|
||||
const color_t cth_color = game_config::red_to_green(attacker.stats_.chance_to_hit);
|
||||
|
||||
ss.str("");
|
||||
ss << font::span_color(cth_color) << attacker.stats_.chance_to_hit << "%</span>";
|
||||
|
||||
set_label_helper("chance_to_hit", ss.str());
|
||||
}
|
||||
|
||||
void attack_predictions::draw_hp_graph(drawing& hp_graph, const combatant_data& attacker, const combatant_data& defender)
|
||||
|
|
Loading…
Add table
Reference in a new issue