diff --git a/data/schema/filters/weapon.cfg b/data/schema/filters/weapon.cfg index b2a92c4d922..726a59d9aa6 100644 --- a/data/schema/filters/weapon.cfg +++ b/data/schema/filters/weapon.cfg @@ -6,6 +6,7 @@ {SIMPLE_KEY alignment alignment} {SIMPLE_KEY name string_list} {SIMPLE_KEY type string_list} + {SIMPLE_KEY base_type string_list} {SIMPLE_KEY special string_list} {SIMPLE_KEY special_active string_list} {SIMPLE_KEY special_id string_list} diff --git a/data/test/scenarios/wml_tests/ScenarioWML/EventWML/events-test_filter_attack_type.cfg b/data/test/scenarios/wml_tests/ScenarioWML/EventWML/events-test_filter_attack_type.cfg index 29e7a6fd315..1df290c8fd7 100644 --- a/data/test/scenarios/wml_tests/ScenarioWML/EventWML/events-test_filter_attack_type.cfg +++ b/data/test/scenarios/wml_tests/ScenarioWML/EventWML/events-test_filter_attack_type.cfg @@ -1,17 +1,6 @@ # wmllint: no translatables -##### -# API(s) being tested: [event][filter_attack]type= -## -# Actions: -# Give Alice an ability that adds a damage special with addition of arcnae type to all of his weapons. -# Define events that use filter_attack matching Alice's arcane type. -# Have Alice attack Bob. -## -# Expected end state: -# An event triggers when Alice attacks during side 1's turn because type=arcane detected. -##### -{GENERIC_UNIT_TEST event_test_filter_attack_type ( +#define FILTER_TYPE VALUE [event] name=start [object] @@ -29,6 +18,19 @@ id=alice [/filter] [/object] + [object] + silent=yes + [effect] + apply_to=resistance + replace=yes + [resistance] + arcane={VALUE} + [/resistance] + [/effect] + [filter] + id=bob + [/filter] + [/object] [modify_unit] [filter] [/filter] @@ -84,8 +86,171 @@ {ASSERT ({VARIABLE_CONDITIONAL triggers equals 0})} {VARIABLE_OP triggers add 1} [/event] +#enddef + +##### +# API(s) being tested: [event][filter_attack]type= +## +# Actions: +# Give Alice an ability that adds a damage special with addition of arcnae type to all of his weapons. +# Give Bob resistance to -100% to arcane +# Define events that use filter_attack matching Alice's arcane type. +# Have Alice attack Bob. +## +# Expected end state: +# The event triggers when Alice attacks, because the result is calculated using arcane as the damage type +##### +{GENERIC_UNIT_TEST event_test_filter_attack_type ( + {FILTER_TYPE 200} + [event] name=turn 2 {RETURN ({VARIABLE_CONDITIONAL triggers equals 1})} [/event] )} + +##### +# API(s) being tested: [event][filter_attack]type= +## +# Actions: +# Give Alice an ability that adds a damage special with addition of arcane type to all of his weapons. +# Give Bob resistance to 50% to arcane +# Define events that use filter_attack matching Alice's arcane type. +# Have Alice attack Bob. +## +# Expected end state: +# The event does not trigger when Alice attacks, because the result is calculated using blade or pierce as the damage type +##### +{GENERIC_UNIT_TEST event_test_filter_attack_type_no_used ( + {FILTER_TYPE 50} + + [event] + name=turn 2 + {RETURN ({VARIABLE_CONDITIONAL triggers equals 0})} + [/event] +)} + +#undef FILTER_TYPE + +#define FILTER_BASE_TYPE TYPE + [event] + name=start + [object] + silent=yes + [effect] + apply_to=attack + set_type=pierce + [/effect] + [effect] + apply_to=new_ability + [abilities] + [damage_type] + id=test_arcane_damage + replacement_type=arcane + [/damage_type] + [/abilities] + [/effect] + [filter] + id=alice + [/filter] + [/object] + [modify_unit] + [filter] + [/filter] + # Make sure they don't die during the attacks + [status] + invulnerable=yes + [/status] + [/modify_unit] + {VARIABLE triggers 0} + [/event] + [event] + name=side 1 turn 1 + [do_command] + [move] + x=7,13 + y=3,4 + [/move] + [attack] + [source] + x,y=13,4 + [/source] + [destination] + x,y=13,3 + [/destination] + [/attack] + [/do_command] + [end_turn][/end_turn] + [/event] + + [event] + name=side 2 turn + [do_command] + [attack] + [source] + x,y=13,3 + [/source] + [destination] + x,y=13,4 + [/destination] + [/attack] + [/do_command] + [end_turn][/end_turn] + [/event] + + # Event when Alice attacks + [event] + name=attack + first_time_only=no + [filter_attack] + base_type={TYPE} + [/filter_attack] + {ASSERT ({VARIABLE_CONDITIONAL side_number equals 1})} + {ASSERT ({VARIABLE_CONDITIONAL triggers equals 0})} + {VARIABLE_OP triggers add 1} + [/event] +#enddef + +##### +# API(s) being tested: [event][filter_attack]base_type= +## +# Actions: +# Change Alice attack type to pierce. +# Give Alice an ability that adds a damage special with addition of arcane type to all of his weapons. +# Define events that use filter_attack matching Alice's pierce original type. +# Have Alice attack Bob. +## +# Expected end state: +# The event triggers when Alice attacks, because filter matche with pierce original type +##### +{GENERIC_UNIT_TEST event_test_filter_original_attack_type ( + {FILTER_BASE_TYPE pierce} + + [event] + name=turn 2 + {RETURN ({VARIABLE_CONDITIONAL triggers equals 1})} + [/event] +)} + +##### +# API(s) being tested: [event][filter_attack]base_type= +## +# Actions: +# Change Alice attack type to pierce. +# Give Alice an ability that adds a damage special with addition of arcane type to all of his weapons. +# Define events that use filter_attack matching Alice's arcane original type. +# Have Alice attack Bob. +## +# Expected end state: +# The event does not trigger when Alice attacks, because the original type is pierce +##### +{GENERIC_UNIT_TEST event_test_filter_attack_base_type_no_match ( + {FILTER_BASE_TYPE arcane} + + [event] + name=turn 2 + {RETURN ({VARIABLE_CONDITIONAL triggers equals 0})} + [/event] +)} + +#undef FILTER_BASE_TYPE diff --git a/src/ai/formula/function_table.cpp b/src/ai/formula/function_table.cpp index fc9b987ad1c..e05d41b4166 100644 --- a/src/ai/formula/function_table.cpp +++ b/src/ai/formula/function_table.cpp @@ -74,12 +74,8 @@ class unit_adapter { */ int damage_from(const attack_type& attack) const { if(unit_type_ != nullptr) { - std::pair types = attack.damage_type(); - int res = unit_type_->movement_type().resistance_against(types.first); - if(!(types.second).empty()){ - // max not min, resistance_against() returns the percentage taken, so higher means more damage - res = std::max(res, unit_type_->movement_type().resistance_against(types.second)); - } + std::string type = attack.effective_damage_type().first; + int res = unit_type_->movement_type().resistance_against(type); return res; } else { return unit_->damage_from(attack, false, map_location()); diff --git a/src/formula/callable_objects.cpp b/src/formula/callable_objects.cpp index 12438673731..5bc29eb8ae9 100644 --- a/src/formula/callable_objects.cpp +++ b/src/formula/callable_objects.cpp @@ -87,8 +87,10 @@ variant attack_type_callable::get_value(const std::string& key) const return variant(att_->id()); } else if(key == "description") { return variant(att_->name()); - } else if(key == "type") { + } else if(key == "base_type") { return variant(att_->type()); + } else if(key == "type") { + return variant(att_->effective_damage_type().first); } else if(key == "icon") { return variant(att_->icon()); } else if(key == "range") { @@ -133,6 +135,7 @@ void attack_type_callable::get_inputs(formula_input_vector& inputs) const { add_input(inputs, "name"); add_input(inputs, "type"); + add_input(inputs, "base_type"); add_input(inputs, "description"); add_input(inputs, "icon"); add_input(inputs, "range"); diff --git a/src/gui/dialogs/attack_predictions.cpp b/src/gui/dialogs/attack_predictions.cpp index c97f6f3a3a2..6181498ce48 100644 --- a/src/gui/dialogs/attack_predictions.cpp +++ b/src/gui/dialogs/attack_predictions.cpp @@ -206,12 +206,7 @@ void attack_predictions::set_data(const combatant_data& attacker, const combatan } } - std::pair types = weapon->damage_type(); - std::string type_bis = types.second; - if (!type_bis.empty()) { - type_bis = ", " + string_table["type_" + type_bis]; - } - ss << string_table["type_" + types.first] + type_bis; + ss << string_table["type_" + weapon->effective_damage_type().first]; set_label_helper("resis_label", ss.str()); diff --git a/src/gui/dialogs/unit_attack.cpp b/src/gui/dialogs/unit_attack.cpp index fa6f74e11fa..5feb1af2398 100644 --- a/src/gui/dialogs/unit_attack.cpp +++ b/src/gui/dialogs/unit_attack.cpp @@ -109,24 +109,16 @@ void unit_attack::pre_show() attacker_itor_->get_location(), false, attacker.weapon ); - std::pair types = attacker_weapon.damage_type(); - std::string attw_type_second = types.second; - std::string attw_type = !(types.first).empty() ? types.first : attacker_weapon.type(); + std::string types = attacker_weapon.effective_damage_type().first; + std::string attw_type = !(types).empty() ? types : attacker_weapon.type(); if (!attw_type.empty()) { attw_type = string_table["type_" + attw_type]; } - if (!attw_type_second.empty()) { - attw_type_second = ", " + string_table["type_" + attw_type_second]; - } - std::pair def_types = defender_weapon.damage_type(); - std::string defw_type_second = def_types.second; - std::string defw_type = !(def_types.first).empty() ? def_types.first : defender_weapon.type(); + std::string def_types = defender_weapon.effective_damage_type().first; + std::string defw_type = !(def_types).empty() ? def_types : defender_weapon.type(); if (!defw_type.empty()) { defw_type = string_table["type_" + defw_type]; } - if (!defw_type_second.empty()) { - defw_type_second = ", " + string_table["type_" + defw_type_second]; - } const std::set checking_tags_other = {"damage_type", "disable", "berserk", "drains", "heal_on_hit", "plague", "slow", "petrifies", "firststrike", "poison"}; std::string attw_specials = attacker_weapon.weapon_specials(); @@ -176,26 +168,26 @@ void unit_attack::pre_show() // Use attacker/defender.num_blows instead of attacker/defender_weapon.num_attacks() because the latter does not consider the swarm weapon special attacker_stats << markup::bold(attw_name) << "\n" - << attw_type << attw_type_second << "\n" + << attw_type << "\n" << attacker.damage << font::weapon_numbers_sep << attacker.num_blows << attw_specials << "\n" << markup::span_color(a_cth_color, attacker.chance_to_hit, "%"); attacker_tooltip << _("Weapon: ") << markup::bold(attw_name) << "\n" - << _("Type: ") << attw_type << attw_type_second << "\n" + << _("Type: ") << attw_type << "\n" << _("Damage: ") << attacker.damage << markup::italic(attw_specials_dmg) << "\n" << _("Attacks: ") << attacker.num_blows << markup::italic(attw_specials_atk) << "\n" << _("Chance to hit: ") << markup::span_color(a_cth_color, attacker.chance_to_hit, "%") << markup::italic(attw_specials_cth) << attw_specials_others; defender_stats << markup::bold(defw_name) << "\n" - << defw_type << defw_type_second << "\n" + << defw_type << "\n" << defender.damage << font::weapon_numbers_sep << defender.num_blows << defw_specials << "\n" << markup::span_color(d_cth_color, defender.chance_to_hit, "%"); defender_tooltip << _("Weapon: ") << markup::bold(defw_name) << "\n" - << _("Type: ") << defw_type << defw_type_second << "\n" + << _("Type: ") << defw_type << "\n" << _("Damage: ") << defender.damage << markup::italic(defw_specials_dmg) << "\n" << _("Attacks: ") << defender.num_blows << markup::italic(defw_specials_atk) << "\n" << _("Chance to hit: ") << markup::span_color(d_cth_color, defender.chance_to_hit, "%") diff --git a/src/reports.cpp b/src/reports.cpp index 82f7de4d7a1..73666c33093 100644 --- a/src/reports.cpp +++ b/src/reports.cpp @@ -873,8 +873,9 @@ 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::string type = at.damage_type().first; - std::set alt_types = at.alternative_damage_types(); + std::pair> all_damage_types = at.damage_types(); + std::string type = all_damage_types.first; + std::set alt_types = all_damage_types.second; std::string lang_type = string_table["type_" + type]; for(auto alt_t : alt_types){ lang_type += ", " + string_table["type_" + alt_t]; diff --git a/src/units/abilities.cpp b/src/units/abilities.cpp index b02b7e3ce2f..bd6600961b1 100644 --- a/src/units/abilities.cpp +++ b/src/units/abilities.cpp @@ -1258,7 +1258,7 @@ void attack_type::modified_attacks(unsigned & min_attacks, } } -static std::string select_replacement_type(const unit_ability_list& damage_type_list) +std::string attack_type::select_replacement_type(const unit_ability_list& damage_type_list) const { std::map type_count; unsigned int max = 0; @@ -1273,7 +1273,7 @@ static std::string select_replacement_type(const unit_ability_list& damage_type_ } } - if (type_count.empty()) return ""; + if (type_count.empty()) return type(); std::vector type_list; for(auto& i : type_count){ @@ -1282,27 +1282,29 @@ static std::string select_replacement_type(const unit_ability_list& damage_type_ } } - if(type_list.empty()) return ""; + if(type_list.empty()) return type(); return type_list.front(); } -static std::string select_alternative_type(const unit_ability_list& damage_type_list, const unit_ability_list& resistance_list, const unit& u) +std::pair attack_type::select_alternative_type(const unit_ability_list& damage_type_list, const unit_ability_list& resistance_list) const { std::map type_res; - int max_res = std::numeric_limits::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]); + int max_res = INT_MIN; + if(other_){ + 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] = (*other_).resistance_value(resistance_list, type); + max_res = std::max(max_res, type_res[type]); + } } } } - if (type_res.empty()) return ""; + if (type_res.empty()) return {"", INT_MIN}; std::vector type_list; for(auto& i : type_res){ @@ -1310,66 +1312,63 @@ static std::string select_alternative_type(const unit_ability_list& damage_type_ type_list.push_back(i.first); } } - if(type_list.empty()) return ""; + if(type_list.empty()) return {"", INT_MIN}; - return type_list.front(); -} - -std::string attack_type::select_damage_type(const unit_ability_list& damage_type_list, const std::string& key_name, const 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 ""; + return {type_list.front(), max_res}; } /** - * Returns the type of damage inflicted. + * The type of attack used and the resistance value that does the most damage. */ -std::pair attack_type::damage_type() const +std::pair attack_type::effective_damage_type() const { if(attack_empty()){ - return {"", ""}; + return {"", 100}; } - unit_ability_list damage_type_list = get_specials_and_abilities("damage_type"); - if(damage_type_list.empty()){ - return {type(), ""}; - } - unit_ability_list resistance_list; if(other_){ resistance_list = (*other_).get_abilities_weapons("resistance", other_loc_, other_attack_, shared_from_this()); + utils::erase_if(resistance_list, [&](const unit_ability& i) { + return (!((*i.ability_cfg)["active_on"].empty() || (!is_attacker_ && (*i.ability_cfg)["active_on"] == "offense") || (is_attacker_ && (*i.ability_cfg)["active_on"] == "defense"))); + }); } - 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}; + unit_ability_list damage_type_list = get_specials_and_abilities("damage_type"); + int res = other_ ? (*other_).resistance_value(resistance_list, type()) : 100; + if(damage_type_list.empty()){ + return {type(), res}; } - return {type_damage, ""}; + std::string replacement_type = select_replacement_type(damage_type_list); + std::pair alternative_type = select_alternative_type(damage_type_list, resistance_list); + + if(other_){ + res = replacement_type != type() ? (*other_).resistance_value(resistance_list, replacement_type) : res; + replacement_type = alternative_type.second > res ? alternative_type.first : replacement_type; + res = std::max(res, alternative_type.second); + } + return {replacement_type, res}; } -std::set attack_type::alternative_damage_types() const +/** + * Return a type()/replacement_type and a list of alternative_types that should be displayed in the selected unit's report. + */ +std::pair> attack_type::damage_types() const { unit_ability_list damage_type_list = get_specials_and_abilities("damage_type"); + std::set alternative_damage_types; if(damage_type_list.empty()){ - return {}; + return {type(), alternative_damage_types}; } - std::set damage_types; + std::string replacement_type = select_replacement_type(damage_type_list); for(auto& i : damage_type_list) { const config& c = *i.ability_cfg; if(c.has_attribute("alternative_type")){ - damage_types.insert(c["alternative_type"].str()); + alternative_damage_types.insert(c["alternative_type"].str()); } } - return damage_types; + return {replacement_type, alternative_damage_types}; } - /** * Returns the damage per attack of this weapon, considering specials. */ diff --git a/src/units/attack_type.cpp b/src/units/attack_type.cpp index 12a75a549cb..0093bc99a80 100644 --- a/src/units/attack_type.cpp +++ b/src/units/attack_type.cpp @@ -116,6 +116,7 @@ static bool matches_simple_filter(const attack_type & attack, const config & fil const std::set filter_alignment = utils::split_set(filter["alignment"].str()); const std::set filter_name = utils::split_set(filter["name"].str()); const std::set filter_type = utils::split_set(filter["type"].str()); + const std::set filter_base_type = utils::split_set(filter["base_type"].str()); const std::vector filter_special = utils::split(filter["special"]); const std::vector filter_special_id = utils::split(filter["special_id"]); const std::vector filter_special_type = utils::split(filter["special_type"]); @@ -168,13 +169,15 @@ static bool matches_simple_filter(const attack_type & attack, const config & fil } } else { //if the type is different from "damage_type" then damage_type() can be called for safe checking. - std::pair damage_type = attack.damage_type(); - if (filter_type.count(damage_type.first) == 0 && filter_type.count(damage_type.second) == 0){ + if (filter_type.count(attack.effective_damage_type().first) == 0){ return false; } } } + if ( !filter_base_type.empty() && filter_base_type.count(attack.type()) == 0 ) + return false; + if(!filter_special.empty()) { deprecated_message("special=", DEP_LEVEL::PREEMPTIVE, {1, 17, 0}, "Please use special_id or special_type instead"); bool found = false; diff --git a/src/units/attack_type.hpp b/src/units/attack_type.hpp index a780eac1735..7be204a361f 100644 --- a/src/units/attack_type.hpp +++ b/src/units/attack_type.hpp @@ -98,18 +98,10 @@ 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, const 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 damage_type() const; - /** @return A list of alternative_type damage types. */ - std::set alternative_damage_types() const; + /** @return A type()/replacement_type and a list of alternative_types that should be displayed in the selected unit's report. */ + std::pair> damage_types() const; + /** @return The type of attack used and the resistance value that does the most damage. */ + std::pair effective_damage_type() const; /** Returns the damage per attack of this weapon, considering specials. */ double modified_damage() const; @@ -235,6 +227,19 @@ private: * @return true if all attribute with ability checked */ bool special_matches_filter(const config & cfg, const std::string& tag_name, const config & filter) const; + /** + * Select best damage type based on frequency count for replacement_type. + * + * @param damage_type_list list of [damage_type] to check. + */ + std::string select_replacement_type(const unit_ability_list& damage_type_list) const; + /** + * Select best damage type based on highest damage for alternative_type. + * + * @param damage_type_list list of [damage_type] to check. + * @param resistance_list list of "resistance" abilities to check for each type of damage checked. + */ + std::pair select_alternative_type(const unit_ability_list& damage_type_list, const unit_ability_list& resistance_list) const; /** * Filter a list of abilities or weapon specials, removing any entries that don't own * the overwrite_specials attributes. diff --git a/src/units/unit.cpp b/src/units/unit.cpp index 1fc5a328935..659b5e1ea7a 100644 --- a/src/units/unit.cpp +++ b/src/units/unit.cpp @@ -1772,25 +1772,13 @@ static bool resistance_filter_matches_base(const config& cfg, bool attacker) int unit::resistance_against(const std::string& damage_name, bool attacker, const map_location& loc, const_attack_ptr weapon, const const_attack_ptr& opp_weapon) const { + if(opp_weapon){ + return opp_weapon->effective_damage_type().second; + } unit_ability_list resistance_list = get_abilities_weapons("resistance",loc, std::move(weapon), opp_weapon); utils::erase_if(resistance_list, [&](const unit_ability& i) { return !resistance_filter_matches_base(*i.ability_cfg, attacker); }); - if(opp_weapon){ - 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; - } return resistance_value(resistance_list, damage_name); } diff --git a/wml_test_schedule b/wml_test_schedule index 2cbf8751dd4..bd55a88f4ae 100644 --- a/wml_test_schedule +++ b/wml_test_schedule @@ -168,6 +168,9 @@ 0 event_test_filter_attack 0 event_test_filter_attack_no_defense 0 event_test_filter_attack_type +0 event_test_filter_attack_type_no_used +0 event_test_filter_original_attack_type +0 event_test_filter_attack_base_type_no_match 0 event_test_filter_attack_specials 0 event_test_filter_attack_on_moveto 0 event_test_filter_attack_opponent_weapon_condition