Select alternative damage type based on opponent resistances

If two [damage_type]alternative_type= are used with two different types, the chosen type displayed in the pre-combat window will be the one to which the opponent is most vulnerable. That type will then also be used in the attack if it is stronger than the original/replacement_type.

In the sidebar (report) all alternative_types are displayed.

---------

Co-authored-by: Gunter Labes <soliton@wesnoth.org>
This commit is contained in:
newfrenchy83 2024-04-21 13:05:03 +02:00 committed by GitHub
parent 553b1a4511
commit c7080e0ecc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 230 additions and 79 deletions

View File

@ -136,3 +136,63 @@
{SUCCEED}
[/event]
) SIDE2_LEADER=Skeleton}
#####
# API(s) being tested: [damage_type]alternative_type= with two different types
##
# Actions:
# Bob is a Skeleton, with resistance to blade but weakness to arcane and fire
# Give Alice's attacks alternative_type=arcane and alternative_type=fire
# Make Alice teach anti-magic, so that Bob's resistance to arcane is more than his resistance to blade or fire
# Have Alice attack Bob
##
# Expected end state:
# Alice attacked using the fire stats, not the arcane or blade ones.
#####
{COMMON_KEEP_A_B_UNIT_TEST "taught_resistance_with_three_attack_types" (
[event]
name = start
[modify_unit]
[filter]
id=alice
[/filter]
[effect]
apply_to=attack
[set_specials]
mode=replace
[damage_type]
alternative_type=arcane
[/damage_type]
[damage_type]
alternative_type=fire
[/damage_type]
[/set_specials]
[/effect]
[effect]
apply_to=new_ability
# An ability which reduces damage to both friend and foe, based on the anti-magi aura of EoMa's Matriarch of Emptiness
# This doesn't use the TEST_ABILITY macro, because it tests add= rather than value=
[abilities]
[resistance]
add=70
max_value=70
apply_to=arcane
affect_self=no
affect_allies=yes
affect_enemies=yes
[affect_adjacent]
[/affect_adjacent]
[filter_base_value]
less_than=70
[/filter_base_value]
[/resistance]
[/abilities]
[/effect]
[/modify_unit]
# Skeletons have base +40% vs blade, -20% vs arcane or fire. With the +70% buff, is weaker to fire than arcane.
{ATTACK_AND_VALIDATE 100 DAMAGE2=120}
{SUCCEED}
[/event]
) SIDE2_LEADER=Skeleton}

View File

@ -171,9 +171,9 @@
##
# Actions:
# Give both Alice and Bob 100% chance to hit.
# Give Bob one [damage] with replacement_type=fire and two [damage] with replacement_type=cold
# Give Bob one [damage_type] with replacement_type=fire and two [damage_type] with replacement_type=cold
# change resistance of Bob to arcane to 50% and fire to -100%.
# Give Alice one [damage] with replacement_type=cold, two [damage] with replacement_type=arcane and three with replacement_type=fire
# Give Alice one [damage_type] with replacement_type=cold, two [damage_type] with replacement_type=arcane and three with replacement_type=fire
# and change Alice resistance to cold to -100% and fire to 50%.
# Move Alice next to Bob, and have Alice attack Bob.
##
@ -189,9 +189,9 @@
##
# Actions:
# Give both Alice and Bob 100% chance to hit.
# Give Bob one [damage] with replacement_type=fire and two [damage] with replacement_type=cold and filter by type=blade
# Give Bob one [damage_type] with replacement_type=fire and two [damage_type] with replacement_type=cold and filter by type=blade
# change resistance of Bob to arcane to 50% and fire to -100%.
# Give Alice one [damage] with replacement_type=cold, two [damage] with replacement_type=arcane and three with replacement_type=fire
# Give Alice one [damage_type] with replacement_type=cold, two [damage_type] with replacement_type=arcane and three with replacement_type=fire
# and change Alice resistance to cold to -100% and fire to 50%.
# Move Alice next to Bob, and have Alice attack Bob.
##
@ -207,10 +207,10 @@
##
# Actions:
# Give both Alice and Bob 100% chance to hit.
# Give Bob one [damage] with alternative_type=cold
# Give Bob one [damage_type] with alternative_type=cold and another with alternative_type=pierce
# change resistance Bob to blade to -100% and fire to 0%.
# Give Alice one [damage] with alternative_type=fire
# and change resistance to cold to -100% and blade to 0%.
# Give Alice one [damage_type] with alternative_type=fire
# and change resistance to cold to -100%, pierce to -50% and blade to 0%.
# Move Alice next to Bob, and have Alice attack Bob.
##
# Expected end state:
@ -246,6 +246,9 @@
[damage]
value=12
[/damage]
[damage_type]
alternative_type=pierce
[/damage_type]
[damage_type]
alternative_type=cold
[/damage_type]
@ -265,6 +268,7 @@
replace=yes
[resistance]
cold=200
pierce=150
blade=100
[/resistance]
[/effect]

View File

@ -872,22 +872,26 @@ static int attack_info(const reports::context& rc, const attack_type &at, config
const string_with_tooltip damage_and_num_attacks {flush(str), flush(tooltip)};
std::string range = string_table["range_" + at.range()];
std::pair<std::string, std::string> types = at.damage_type();
std::string secondary_lang_type = types.second;
if (!secondary_lang_type.empty()) {
secondary_lang_type = ", " + string_table["type_" + secondary_lang_type];
std::string type = at.damage_type().first;
std::set<std::string> alt_types = at.alternative_damage_types();
std::string lang_type = string_table["type_" + type];
for(auto alt_t : alt_types){
lang_type += ", " + string_table["type_" + alt_t];
}
std::string lang_type = string_table["type_" + types.first] + secondary_lang_type;
// SCALE_INTO() is needed in case the 72x72 images/misc/missing-image.png is substituted.
const std::string range_png = std::string("icons/profiles/") + at.range() + "_attack.png~SCALE_INTO(16,16)";
const std::string type_png = std::string("icons/profiles/") + types.first + ".png~SCALE_INTO(16,16)";
const std::string secondary_type_png = !(types.second).empty() ? std::string("icons/profiles/") + types.second + ".png~SCALE_INTO(16,16)" : "";
const std::string type_png = std::string("icons/profiles/") + type + ".png~SCALE_INTO(16,16)";
std::vector<std::string> secondary_types_png;
bool secondary_type_png_exist = true;
for(auto alt_t : alt_types){
secondary_types_png.push_back(std::string("icons/profiles/") + alt_t + ".png~SCALE_INTO(16,16)");
if(!image::locator(alt_t).file_exists() && secondary_type_png_exist) {secondary_type_png_exist=false;}
}
const bool range_png_exists = image::locator(range_png).file_exists();
const bool type_png_exists = image::locator(type_png).file_exists();
const bool secondary_type_png_exists = image::locator(secondary_type_png).file_exists();
if(!range_png_exists || !type_png_exists || (!secondary_type_png_exists && !secondary_lang_type.empty())) {
if(!range_png_exists || !type_png_exists || (!secondary_type_png_exist && !alt_types.empty())) {
str << span_color(font::weapon_details_color) << " " << " "
<< range << font::weapon_details_sep
<< lang_type << "</span>\n";
@ -944,8 +948,10 @@ static int attack_info(const reports::context& rc, const attack_type &at, config
const std::string spacer = "misc/blank.png~CROP(0, 0, 16, 21)"; // 21 == 16+5
add_image(res, spacer + "~BLIT(" + range_png + ",0,5)", damage_versus.tooltip);
add_image(res, spacer + "~BLIT(" + type_png + ",0,5)", damage_versus.tooltip);
if(secondary_type_png_exists){
add_image(res, spacer + "~BLIT(" + secondary_type_png + ",0,5)", damage_versus.tooltip);
for(auto sec_exist : secondary_types_png){
if(image::locator(sec_exist).file_exists()){
add_image(res, spacer + "~BLIT(" + sec_exist + ",0,5)", damage_versus.tooltip);
}
}
add_text(res, damage_and_num_attacks.str, damage_and_num_attacks.tooltip);
add_text(res, damage_versus.str, damage_versus.tooltip); // This string is usually empty

View File

@ -1168,31 +1168,72 @@ void attack_type::modified_attacks(unsigned & min_attacks,
}
}
//Functions used for change damage_type list with damage
static std::optional<std::string> select_damage_type(const unit_ability_list& abil_list, const std::string& type)
static std::string select_replacement_type(const unit_ability_list& damage_type_list)
{
std::vector<std::string> type_list;
for(auto& i : abil_list) {
if(!(*i.ability_cfg)[type].str().empty()){
type_list.push_back((*i.ability_cfg)[type].str());
}
}
if(type_list.size() >= 2){
std::sort(type_list.begin(), type_list.end());
if(type_list.size() >= 3){
std::unordered_map<std::string, unsigned int> type_count;
for( const std::string& character : type_list ){
type_count[character]++;
std::map<std::string, unsigned int> type_count;
unsigned int max = 0;
for(auto& i : damage_type_list) {
const config& c = *i.ability_cfg;
if(c.has_attribute("replacement_type")) {
std::string type = c["replacement_type"].str();
unsigned int count = ++type_count[type];
if((count > max)) {
max = count;
}
std::sort( std::begin( type_list ) , std::end( type_list ) , [&]( const std::string& rhs , const std::string& lhs ){
return type_count[lhs] < type_count[rhs];
});
}
}
if(!type_list.empty()){
return type_list.front();
if (type_count.empty()) return "";
std::vector<std::string> type_list;
for(auto& i : type_count){
if(i.second == max){
type_list.push_back(i.first);
}
}
return std::nullopt;
if(type_list.empty()) return "";
return type_list.front();
}
static std::string select_alternative_type(const unit_ability_list& damage_type_list, unit_ability_list resistance_list, const unit& u)
{
std::map<std::string, int> type_res;
int max_res = INT_MIN;
for(auto& i : damage_type_list) {
const config& c = *i.ability_cfg;
if(c.has_attribute("alternative_type")) {
std::string type = c["alternative_type"].str();
if(type_res.count(type) == 0){
type_res[type] = u.resistance_value(resistance_list, type);
max_res = std::max(max_res, type_res[type]);
}
}
}
if (type_res.empty()) return "";
std::vector<std::string> type_list;
for(auto& i : type_res){
if(i.second == max_res){
type_list.push_back(i.first);
}
}
if(type_list.empty()) return "";
return type_list.front();
}
std::string attack_type::select_damage_type(const unit_ability_list& damage_type_list, const std::string& key_name, unit_ability_list resistance_list) const
{
bool is_alternative = (key_name == "alternative_type");
if(is_alternative && other_){
return select_alternative_type(damage_type_list, resistance_list, (*other_));
} else if(!is_alternative){
return select_replacement_type(damage_type_list);
}
return "";
}
/**
@ -1200,20 +1241,39 @@ static std::optional<std::string> select_damage_type(const unit_ability_list& ab
*/
std::pair<std::string, std::string> attack_type::damage_type() const
{
unit_ability_list abil_list = get_specials_and_abilities("damage_type");
if(abil_list.empty()){
unit_ability_list damage_type_list = get_specials_and_abilities("damage_type");
if(damage_type_list.empty()){
return {type(), ""};
}
std::optional<std::string> replacement_type = select_damage_type(abil_list, "replacement_type");
std::optional<std::string> alternative_type = select_damage_type(abil_list, "alternative_type");
std::string type_damage = replacement_type.value_or(type());
if(alternative_type && type_damage != *alternative_type){
return {type_damage, *alternative_type};
unit_ability_list resistance_list;
if(other_){
resistance_list = (*other_).get_abilities_weapons("resistance", other_loc_, other_attack_, shared_from_this());
}
std::string replacement_type = select_damage_type(damage_type_list, "replacement_type", resistance_list);
std::string alternative_type = select_damage_type(damage_type_list, "alternative_type", resistance_list);
std::string type_damage = replacement_type.empty() ? type() : replacement_type;
if(!alternative_type.empty() && type_damage != alternative_type){
return {type_damage, alternative_type};
}
return {type_damage, ""};
}
std::set<std::string> attack_type::alternative_damage_types() const
{
unit_ability_list damage_alternative_type_list = get_specials_and_abilities("damage_type");
if(damage_alternative_type_list.empty()){
return {};
}
std::set<std::string> damage_types;
for(auto& i : damage_alternative_type_list) {
const config& c = *i.ability_cfg;
damage_types.insert(c["alternative_type"].str());
}
return damage_types;
}
/**
* Returns the damage per attack of this weapon, considering specials.

View File

@ -87,8 +87,18 @@ public:
void modified_attacks(unsigned & min_attacks,
unsigned & max_attacks) const;
/**
* Select best damage type based on frequency count for replacement_type and based on highest damage for alternative_type.
*
* @param damage_type_list list of [damage_type] to check.
* @param key_name name of attribute checked 'alternative_type' or 'replacement_type'.
* @param resistance_list list of "resistance" abilities to check for each type of damage checked.
*/
std::string select_damage_type(const unit_ability_list& damage_type_list, const std::string& key_name, unit_ability_list resistance_list) const;
/** return a modified damage type and/or add a secondary_type for hybrid use if special is active. */
std::pair<std::string, std::string> damage_type() const;
/** @return A list of alternative_type damage types. */
std::set<std::string> alternative_damage_types() const;
/** Returns the damage per attack of this weapon, considering specials. */
int modified_damage() const;

View File

@ -1764,12 +1764,8 @@ int unit::defense_modifier(const t_translation::terrain_code & terrain) const
return def;
}
bool unit::resistance_filter_matches(const config& cfg, bool attacker, const std::string& damage_name, int res) const
bool unit::resistance_filter_matches(const config& cfg, const std::string& damage_name, int res) const
{
if(!(cfg["active_on"].empty() || (attacker && cfg["active_on"] == "offense") || (!attacker && cfg["active_on"] == "defense"))) {
return false;
}
const std::string& apply_to = cfg["apply_to"];
if(!apply_to.empty()) {
if(damage_name != apply_to) {
@ -1792,15 +1788,15 @@ bool unit::resistance_filter_matches(const config& cfg, bool attacker, const std
return true;
}
int unit::resistance_ability(unit_ability_list resistance_abilities, const std::string& damage_name, bool attacker) const
int unit::resistance_value(unit_ability_list resistance_list, const std::string& damage_name) const
{
int res = movement_type_.resistance_against(damage_name);
utils::erase_if(resistance_abilities, [&](const unit_ability& i) {
return !resistance_filter_matches(*i.ability_cfg, attacker, damage_name, 100-res);
utils::erase_if(resistance_list, [&](const unit_ability& i) {
return !resistance_filter_matches(*i.ability_cfg, damage_name, 100-res);
});
if(!resistance_abilities.empty()) {
unit_abilities::effect resist_effect(resistance_abilities, 100-res, nullptr, unit_abilities::EFFECT_CLAMP_MIN_MAX);
if(!resistance_list.empty()) {
unit_abilities::effect resist_effect(resistance_list, 100-res, nullptr, unit_abilities::EFFECT_CLAMP_MIN_MAX);
res = 100 - resist_effect.get_composite_value();
}
@ -1808,22 +1804,37 @@ int unit::resistance_ability(unit_ability_list resistance_abilities, const std::
return res;
}
int unit::resistance_against(const std::string& damage_name,bool attacker,const map_location& loc, const_attack_ptr weapon, const_attack_ptr opp_weapon) const
static bool resistance_filter_matches_base(const config& cfg, bool attacker)
{
std::pair<std::string, std::string> types;
if(!(!cfg.has_attribute("active_on") || (attacker && cfg["active_on"] == "offense") || (!attacker && cfg["active_on"] == "defense"))) {
return false;
}
return true;
}
int unit::resistance_against(const std::string& damage_name, bool attacker, const map_location& loc, const_attack_ptr weapon, const_attack_ptr opp_weapon) const
{
unit_ability_list resistance_list = get_abilities_weapons("resistance",loc, weapon, opp_weapon);
utils::erase_if(resistance_list, [&](const unit_ability& i) {
return !resistance_filter_matches_base(*i.ability_cfg, attacker);
});
if(opp_weapon){
types = opp_weapon->damage_type();
} else{
types.first = damage_name;
unit_ability_list damage_type_list = opp_weapon->get_specials_and_abilities("damage_type");
if(damage_type_list.empty()){
return resistance_value(resistance_list, damage_name);
}
std::string replacement_type = opp_weapon->select_damage_type(damage_type_list, "replacement_type", resistance_list);
std::string type_damage = replacement_type.empty() ? damage_name : replacement_type;
int max_res = resistance_value(resistance_list, type_damage);
for(auto& i : damage_type_list) {
if((*i.ability_cfg).has_attribute("alternative_type")){
max_res = std::max(max_res , resistance_value(resistance_list, (*i.ability_cfg)["alternative_type"].str()));
}
}
return max_res;
}
unit_ability_list resistance_abilities = get_abilities_weapons("resistance",loc, weapon, opp_weapon);
int res = resistance_ability(resistance_abilities, types.first, attacker);
if(!(types.second).empty()){
res = std::max(res , resistance_ability(resistance_abilities, types.second, attacker));
}
return res;
return resistance_value(resistance_list, damage_name);
}
std::map<std::string, std::string> unit::advancement_icons() const

View File

@ -1023,6 +1023,15 @@ public:
*/
int defense_modifier(const t_translation::terrain_code& terrain) const;
/**
* For the provided list of resistance abilities, determine the damage resistance based on which are active and any max_value that's present.
*
* @param resistance_list A list of resistance abilities that the unit has.
* @param damage_name The name of the damage type, for example "blade".
* @return The resistance value for a unit with the provided resistance abilities to the provided damage type.
*/
int resistance_value(unit_ability_list resistance_list, const std::string& damage_name) const;
/**
* The unit's resistance against a given damage type
* @param damage_name The damage type
@ -1052,17 +1061,7 @@ public:
}
private:
bool resistance_filter_matches(const config& cfg, bool attacker, const std::string& damage_name, int res) const;
/**
* For the provided list of resistance abilities, determine the damage resistance based on which are active and any max_value that's present.
*
* @param resistance_abilities A list of resistance abilities that the unit has.
* @param damage_name The name of the damage type, for example "blade".
* @param attacker True if the unit is attacking, false if defending.
* @return The resistance value for a unit with the provided resistance abilities to the provided damage type.
*/
int resistance_ability(unit_ability_list resistance_abilities, const std::string& damage_name, bool attacker) const;
bool resistance_filter_matches(const config& cfg, const std::string& damage_name, int res) const;
/**
* @}

View File

@ -372,6 +372,7 @@
0 negative_resistance_with_two_attack_types
0 positive_resistance_with_two_attack_types
0 taught_resistance_with_two_attack_types
0 taught_resistance_with_three_attack_types
0 swarms_filter_student_by_type
0 swarms_effects_not_checkable
0 filter_special_id_active