diff --git a/changelog_entries/extent_remove_ability_efect_to_attribute_changelog.md b/changelog_entries/extent_remove_ability_efect_to_attribute_changelog.md new file mode 100644 index 00000000000..debbb4b0f10 --- /dev/null +++ b/changelog_entries/extent_remove_ability_efect_to_attribute_changelog.md @@ -0,0 +1,2 @@ +### WML Engine + * Add [overwrite] tags who contain 'priority' attribute for define priority between two abilities with overwrite_specials, and [overwrite][filter_specials] to filter [abilities/specials] which may or may not be overwritten by the overwrite_specials attribute. diff --git a/data/core/macros/utils.cfg b/data/core/macros/utils.cfg index 67198574c7e..31411043931 100644 --- a/data/core/macros/utils.cfg +++ b/data/core/macros/utils.cfg @@ -552,6 +552,9 @@ cumulative=no affect_self=yes overwrite_specials=both_sides + [overwrite] + priority=1000 + [/overwrite] [filter_opponent] {SECOND_FILTER} [/filter_opponent] @@ -605,6 +608,9 @@ cumulative=no affect_self=yes overwrite_specials=both_sides + [overwrite] + priority=1000 + [/overwrite] [filter_opponent] {SECOND_FILTER} [/filter_opponent] diff --git a/data/schema/filters/abilities.cfg b/data/schema/filters/abilities.cfg index b6071b5711a..7642c11790f 100644 --- a/data/schema/filters/abilities.cfg +++ b/data/schema/filters/abilities.cfg @@ -4,9 +4,9 @@ max=0 {SIMPLE_KEY id string_list} {SIMPLE_KEY tag_name string_list} - {SIMPLE_KEY overwrite_specials string_list} + {SIMPLE_KEY overwrite_specials ability_overwrite} {SIMPLE_KEY apply_to string_list} - {SIMPLE_KEY active_on string_list} + {SIMPLE_KEY active_on ability_context} {SIMPLE_KEY value s_int_range_list} {SIMPLE_KEY add s_int_range_list} {SIMPLE_KEY sub s_int_range_list} diff --git a/data/schema/units/abilities.cfg b/data/schema/units/abilities.cfg index 3b95eee8de4..7b2c82862f7 100644 --- a/data/schema/units/abilities.cfg +++ b/data/schema/units/abilities.cfg @@ -5,7 +5,6 @@ max=infinite super="units/unit_type/abilities/~generic~,units/unit_type/attack/specials/" + {NAME} {FILTER_TAG "filter_student" unit {FILTER_TAG "filter_weapon" weapon ()}} - {DEFAULT_KEY overwrite_specials ability_overwrite none} [/tag] #enddef diff --git a/data/schema/units/specials.cfg b/data/schema/units/specials.cfg index b8493cb4759..4b206ed8ab2 100644 --- a/data/schema/units/specials.cfg +++ b/data/schema/units/specials.cfg @@ -54,6 +54,12 @@ {SIMPLE_KEY cumulative bool} {DEPRECATED_KEY backstab bool} {FILTER_TAG "filter_base_value" base_value ()} + {DEFAULT_KEY overwrite_specials ability_overwrite none} + [tag] + name="overwrite" + {FILTER_TAG "filter_specials" abilities ()} + {SIMPLE_KEY priority real} + [/tag] [/tag] [tag] name="attacks" @@ -92,6 +98,12 @@ max=infinite super="units/unit_type/attack/specials/~generic~" {SIMPLE_KEY type string} + {DEFAULT_KEY overwrite_specials ability_overwrite none} + [tag] + name="overwrite" + {FILTER_TAG "filter_specials" abilities ()} + {SIMPLE_KEY priority real} + [/tag] [/tag] [tag] name="swarm" diff --git a/data/test/scenarios/macro_tests/test_force_chance_to_hit_macro.cfg b/data/test/scenarios/macro_tests/test_force_chance_to_hit_macro.cfg index 74bdd5489ac..e230bfa1f53 100644 --- a/data/test/scenarios/macro_tests/test_force_chance_to_hit_macro.cfg +++ b/data/test/scenarios/macro_tests/test_force_chance_to_hit_macro.cfg @@ -42,6 +42,15 @@ [/chance_to_hit] [/set_specials] [/effect] + [effect]#test if macro work when ability with overwrite_special is used + apply_to=new_ability + [abilities] + [chance_to_hit] + value=100 + overwrite_specials=one_side + [/chance_to_hit] + [/abilities] + [/effect] [filter] id=bob [/filter] @@ -64,6 +73,15 @@ [/chance_to_hit] [/set_specials] [/effect] + [effect]#test if macro work when ability with overwrite_special is used + apply_to=new_ability + [abilities] + [chance_to_hit] + value=100 + overwrite_specials=one_side + [/chance_to_hit] + [/abilities] + [/effect] [filter] id=alice [/filter] diff --git a/data/test/scenarios/wml_tests/UnitsWML/AbilitiesWML/test_overwrite_specials_filter.cfg b/data/test/scenarios/wml_tests/UnitsWML/AbilitiesWML/test_overwrite_specials_filter.cfg new file mode 100644 index 00000000000..749ce71c29c --- /dev/null +++ b/data/test/scenarios/wml_tests/UnitsWML/AbilitiesWML/test_overwrite_specials_filter.cfg @@ -0,0 +1,106 @@ +##### +# API(s) being tested: [damage]with overwrite_specials attibute and [overwrite][filter_specials] +## +# Actions: +# Three [damage] abilities are added, one with value=5, other with add=11 and the last with multiply=2. +# The one with multiply=2 have overwrite_specials=both_sides and [overwrite][filter_specials] for overwrite [damage] with add=11 but not value=5. +# Have alice attack bob. +## +# Expected end state: +# Alice has 100 - (5*2)=90 hitpoints because add=11 overwrited but not value=5 who replace 9 pt of damage of bob. +##### +{GENERIC_UNIT_TEST "test_overwrite_specials_filter" ( + [event] + name=start + [modify_unit] + [filter] + [/filter] + max_hitpoints=100 + hitpoints=100 + attacks_left=1 + [/modify_unit] + [object] + silent=yes + [effect] + apply_to=new_ability + [abilities] + [damage] + id=test_dmg + value=5 + [/damage] + [attacks] + id=test_att + value=1 + [/attacks] + [damage] + id=test_overwrite + add=11 + [/damage] + [damage] + id=test_overwrite_specials + multiply=2 + overwrite_specials=both_sides + [overwrite] + [filter_specials] + [not] + type_value=value + [/not] + [/filter_specials] + [/overwrite] + [/damage] + [chance_to_hit] + value=100 + [/chance_to_hit] + [/abilities] + [/effect] + [filter] + id=bob + [/filter] + [/object] + [store_unit] + [filter] + id=alice + [/filter] + variable=a + kill=yes + [/store_unit] + [store_unit] + [filter] + id=bob + [/filter] + variable=b + [/store_unit] + [unstore_unit] + variable=a + find_vacant=yes + x,y=$b.x,$b.y + [/unstore_unit] + [store_unit] + [filter] + id=alice + [/filter] + variable=a + [/store_unit] + + [do_command] + [attack] + weapon=0 + defender_weapon=0 + [source] + x,y=$a.x,$a.y + [/source] + [destination] + x,y=$b.x,$b.y + [/destination] + [/attack] + [/do_command] + [store_unit] + [filter] + id=alice + [/filter] + variable=a + [/store_unit] + {ASSERT ({VARIABLE_CONDITIONAL a.hitpoints equals 90})} + {SUCCEED} + [/event] +)} diff --git a/src/units/abilities.cpp b/src/units/abilities.cpp index 6e2a9f4e35a..e037ab8dd6e 100644 --- a/src/units/abilities.cpp +++ b/src/units/abilities.cpp @@ -1312,17 +1312,17 @@ unit_ability_list attack_type::get_weapon_ability(const std::string& ability) co unit_ability_list attack_type::get_specials_and_abilities(const std::string& special) const { - unit_ability_list abil_list = get_weapon_ability(special); - if(!abil_list.empty()){ - abil_list = overwrite_special_checking(abil_list, abil_list, false); - } - unit_ability_list spe_list = get_specials(special); - if(!spe_list.empty()){ - spe_list = overwrite_special_checking(spe_list, abil_list, true); - if(special == "plague" && !spe_list.empty()){ - return spe_list; - } - abil_list.append(spe_list); + // get all weapon specials of the provided type + unit_ability_list abil_list = get_specials(special); + // append all such weapon specials as abilities as well + abil_list.append(get_weapon_ability(special)); + // get a list of specials/"specials as abilities" that may potentially overwrite others + unit_ability_list overwriters = overwrite_special_overwriter(abil_list, special); + if(!abil_list.empty() && !overwriters.empty()){ + // remove all abilities that would be overwritten + utils::erase_if(abil_list, [&](const unit_ability& j) { + return (overwrite_special_checking(overwriters, *j.ability_cfg, special)); + }); } return abil_list; } @@ -1338,36 +1338,94 @@ static bool overwrite_special_affects(const config& special) return (apply_to == "one_side" || apply_to == "both_sides"); } -unit_ability_list attack_type::overwrite_special_checking(unit_ability_list input, unit_ability_list overwriters, bool is_special) const +unit_ability_list attack_type::overwrite_special_overwriter(unit_ability_list overwriters, const std::string& tag_name) const { - for(unit_ability_list::iterator i = overwriters.begin(); i != overwriters.end();) { - if(!overwrite_special_affects(*i->ability_cfg)) { - i = overwriters.erase(i); - } else { - ++i; - } - } + //remove element without overwrite_specials key, if list empty after check return empty list. + utils::erase_if(overwriters, [&](const unit_ability& i) { + return (!overwrite_special_affects(*i.ability_cfg)); + }); + + // if empty, nothing is doing any overwriting if(overwriters.empty()){ - return input; + return overwriters; } - for(const auto& i : overwriters) { - bool affect_side = ((*i.ability_cfg)["overwrite_specials"] == "one_side"); - utils::erase_if(input, [&](const unit_ability& j) { - bool is_overwritable = (is_special || !overwrite_special_affects(*j.ability_cfg)); - bool one_side_overwritable = true; - if(affect_side && is_overwritable){ - if(special_affects_self(*i.ability_cfg, is_attacker_)){ - one_side_overwritable = special_affects_self(*j.ability_cfg, is_attacker_); - } - else if(special_affects_opponent(*i.ability_cfg, !is_attacker_)){ - one_side_overwritable = special_affects_opponent(*j.ability_cfg, !is_attacker_); - } + // if there are specials/"specials as abilities" that could potentially overwrite each other + if(overwriters.size() >= 2){ + // sort them by overwrite priority from highest to lowest (default priority is 0) + utils::sort_if(overwriters,[](const unit_ability& i, const unit_ability& j){ + auto oi = (*i.ability_cfg).optional_child("overwrite"); + double l = 0; + if(oi && !oi["priority"].empty()){ + l = oi["priority"].to_double(0); } - return (is_overwritable && one_side_overwritable); + auto oj = (*j.ability_cfg).optional_child("overwrite"); + double r = 0; + if(oj && !oj["priority"].empty()){ + r = oj["priority"].to_double(0); + } + return l > r; + }); + // remove any that need to be overwritten + utils::erase_if(overwriters, [&](const unit_ability& i) { + return (overwrite_special_checking(overwriters, *i.ability_cfg, tag_name)); }); } - return input; + return overwriters; +} + +bool attack_type::overwrite_special_checking(unit_ability_list& overwriters, const config& cfg, const std::string& tag_name) const +{ + if(overwriters.empty()){ + return false; + } + + for(const auto& j : overwriters) { + // whether the overwriter affects a single side + bool affect_side = ((*j.ability_cfg)["overwrite_specials"] == "one_side"); + // the overwriter's priority, default of 0 + auto overwrite_specials = (*j.ability_cfg).optional_child("overwrite"); + double priority = overwrite_specials ? overwrite_specials["priority"].to_double(0) : 0.00; + // the cfg being checked for whether it will be overwritten + auto has_overwrite_specials = cfg.optional_child("overwrite"); + // if the overwriter's priority is greater than 0, then true if the cfg being checked has a higher priority + // else true + bool prior = (priority > 0) ? (has_overwrite_specials && has_overwrite_specials["priority"].to_double(0) >= priority) : true; + // true if the cfg being checked affects one or both sides and doesn't have a higher priority, or if it doesn't affect one or both sides + // aka whether the cfg being checked can potentially be overwritten by the current overwriter + bool is_overwritable = (overwrite_special_affects(cfg) && !prior) || !overwrite_special_affects(cfg); + bool one_side_overwritable = true; + + // if the current overwriter affects one side and the cfg being checked can be overwritten by this overwriter + // then check that the current overwriter and the cfg being checked both affect either this unit or its opponent + if(affect_side && is_overwritable){ + if(special_affects_self(*j.ability_cfg, is_attacker_)){ + one_side_overwritable = special_affects_self(cfg, is_attacker_); + } + else if(special_affects_opponent(*j.ability_cfg, !is_attacker_)){ + one_side_overwritable = special_affects_opponent(cfg, !is_attacker_); + } + } + + // check whether the current overwriter is disabled due to a filter + bool special_matches = true; + if(overwrite_specials){ + auto overwrite_filter = (*overwrite_specials).optional_child("filter_specials"); + if(overwrite_filter && is_overwritable && one_side_overwritable){ + if(self_){ + special_matches = (*self_).ability_matches_filter(cfg, tag_name, *overwrite_filter); + } + } + } + + // if the cfg being checked should be overwritten + // and either this unit or its opponent are affected + // and the current overwriter is not disabled due to a filter + if(is_overwritable && one_side_overwritable && special_matches){ + return true; + } + } + return false; } /** diff --git a/src/units/attack_type.hpp b/src/units/attack_type.hpp index b431578c362..a24a2154b18 100644 --- a/src/units/attack_type.hpp +++ b/src/units/attack_type.hpp @@ -98,7 +98,10 @@ public: int composite_value(const unit_ability_list& abil_list, int base_value) const; /** Returns list for weapon like abilities for each ability type. */ unit_ability_list get_weapon_ability(const std::string& ability) const; - /** Returns list who contains get_weapon_ability and get_specials list for each ability type */ + /** + * @param special the tag name to check for + * @return list which contains get_weapon_ability and get_specials list for each ability type, with overwritten items removed + */ unit_ability_list get_specials_and_abilities(const std::string& special) const; /** used for abilities used like weapon * @return True if the ability @a special is active. @@ -136,14 +139,22 @@ private: // Configured as a bit field, in case that is useful. enum AFFECTS { AFFECT_SELF=1, AFFECT_OTHER=2, AFFECT_EITHER=3 }; /** - * Filter a list of abilities or weapon specials, removing any entries that are overridden by - * the overwrite_specials attributes of a second list. + * Filter a list of abilities or weapon specials, removing any entries that don't own + * the overwrite_specials attributes. * - * @param input list to check, a filtered copy of this list is returned by the function. * @param overwriters list that may have overwrite_specials attributes. - * @param is_special if true, input contains weapon specials; if false, it contains abilities. + * @param tag_name type of abilitie/special checked. */ - unit_ability_list overwrite_special_checking(unit_ability_list input, unit_ability_list overwriters, bool is_special) const; + unit_ability_list overwrite_special_overwriter(unit_ability_list overwriters, const std::string& tag_name) const; + /** + * Check whether @a cfg would be overwritten by any element of @a overwriters. + * + * @return True if element checked is overwritable. + * @param overwriters list used for check if element is overwritable. + * @param cfg element checked. + * @param tag_name type of abilitie/special checked. + */ + bool overwrite_special_checking(unit_ability_list& overwriters, const config& cfg, const std::string& tag_name) const; /** check_self_abilities : return an boolean value for checking of activities of abilities used like weapon * @return True if the special @a special is active. * @param cfg the config to one special ability checked. diff --git a/src/units/unit.hpp b/src/units/unit.hpp index f19b58f07f2..6dfece080a0 100644 --- a/src/units/unit.hpp +++ b/src/units/unit.hpp @@ -94,6 +94,7 @@ public: const unit_ability& front() const { return cfgs_.front(); } unit_ability& back() { return cfgs_.back(); } const unit_ability& back() const { return cfgs_.back(); } + std::size_t size() { return cfgs_.size(); } iterator erase(const iterator& erase_it) { return cfgs_.erase(erase_it); } iterator erase(const iterator& first, const iterator& last) { return cfgs_.erase(first, last); } diff --git a/src/utils/general.hpp b/src/utils/general.hpp index 01d925af01b..e8078dbe71f 100644 --- a/src/utils/general.hpp +++ b/src/utils/general.hpp @@ -106,4 +106,14 @@ void erase_if(Container& container, const Predicate& predicate) container.erase(std::remove_if(container.begin(), container.end(), predicate), container.end()); } +/** + * Convenience wrapper for using std::sort on a container. + * + */ +template +void sort_if(Container& container, const Predicate& predicate) +{ + std::sort(container.begin(), container.end(), predicate); +} + } // namespace utils diff --git a/wml_test_schedule b/wml_test_schedule index d9befed6c9e..6b7ce2f8f60 100644 --- a/wml_test_schedule +++ b/wml_test_schedule @@ -331,6 +331,7 @@ 0 trait_exclusion_test 0 trait_requirement_test 0 test_remove_ability_by_filter +0 test_overwrite_specials_filter 0 swarms_filter_student_by_type 0 swarms_effects_not_checkable 0 filter_special_id_active