mirror of
https://github.com/wesnoth/wesnoth
synced 2025-04-24 13:35:10 +00:00
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:
parent
553b1a4511
commit
c7080e0ecc
@ -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}
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
/**
|
||||
* @}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user