diff --git a/changelog_entries/add_filter_special.md b/changelog_entries/add_filter_special.md new file mode 100644 index 0000000000000..33699c2ede568 --- /dev/null +++ b/changelog_entries/add_filter_special.md @@ -0,0 +1,3 @@ +### WML Engine + * add a [remove_specials] tag in [effect] to be able to remove specials with other criteria than the id (type of the special, active_on, apply_to or other attributes) + * add [filter_special] to [has_attack/filter_weapon] in order to simultaneously check specials with id and type, and/or other attributes \ No newline at end of file diff --git a/data/schema/filters/weapon.cfg b/data/schema/filters/weapon.cfg index b61f4dac5b8da..b2a92c4d922e7 100644 --- a/data/schema/filters/weapon.cfg +++ b/data/schema/filters/weapon.cfg @@ -19,5 +19,6 @@ {SIMPLE_KEY accuracy s_unsigned_range_list} {SIMPLE_KEY movement_used s_unsigned_range_list} {SIMPLE_KEY attacks_used s_unsigned_range_list} + {FILTER_TAG "filter_special" abilities {SIMPLE_KEY active s_bool}} {FILTER_BOOLEAN_OPS weapon} [/tag] diff --git a/data/schema/units/modifications.cfg b/data/schema/units/modifications.cfg index 586fcfed91c89..e49ead0cdfc75 100644 --- a/data/schema/units/modifications.cfg +++ b/data/schema/units/modifications.cfg @@ -52,6 +52,10 @@ super="units/unit_type~core/attack/specials" {DEFAULT_KEY mode effect_set_special_mode replace} [/tag] + [tag] + name="remove_specials" + super="$filter_abilities" + [/tag] [/case] [case] value=movement diff --git a/data/test/scenarios/wml_tests/ScenarioWML/EventWML/events-test_filter_ability.cfg b/data/test/scenarios/wml_tests/ScenarioWML/EventWML/events-test_filter_ability.cfg index 917daf843801d..bd46206147787 100644 --- a/data/test/scenarios/wml_tests/ScenarioWML/EventWML/events-test_filter_ability.cfg +++ b/data/test/scenarios/wml_tests/ScenarioWML/EventWML/events-test_filter_ability.cfg @@ -379,6 +379,109 @@ [/event] )} +##### +# API(s) being tested: [event][filter_attack][filter_special]active=yes +## +# Actions: +# Use the common setup from FILTER_ABILITY_TEST. +# Add an event with a filter matching Alice's drains ability, but only when it's active. +# Alice attacks Bob and then Bob attacks Alice, as defined in FILTER_ABILITY_TEST. +## +# Expected end state: +# The filtered event is triggered exactly once. +##### +{GENERIC_UNIT_TEST event_test_filter_special_active ( + {FILTER_ABILITY_TEST} + [event] + name=attack + first_time_only=no + [filter_attack] + [filter_special] + active=yes + tag_name=drains + value=25 + [/filter_special] + [/filter_attack] + {ASSERT ({VARIABLE_CONDITIONAL side_number equals 1})} + {ASSERT ({VARIABLE_CONDITIONAL triggers equals 0})} + {VARIABLE_OP triggers add 1} + [/event] + [event] + name=turn 2 + {RETURN ({VARIABLE_CONDITIONAL triggers equals 1})} + [/event] +)} + +##### +# API(s) being tested: [event][filter_attack][filter_special]active=yes +## +# Actions: +# Use the common setup from FILTER_ABILITY_TEST. +# Add an event with a filter matching Alice's drains ability, but only when it's active. +# Give Alice the Illuminates ability, which makes it the wrong time of day for her drains ability. +# The events in FILTER_ABILITY_TEST make Alice attacks Bob and then Bob attack Alice. +## +# Expected end state: +# The filtered event is never triggered. +##### +{GENERIC_UNIT_TEST event_test_filter_special_active_inactive ( + {MORNING} + {FILTER_ABILITY_TEST} + + [event] + name=attack + first_time_only=no + [filter_attack] + [filter_special] + active=yes + tag_name=drains + value=25 + [/filter_special] + [/filter_attack] + {FAIL} + [/event] + + [event] + name=turn 2 + {SUCCEED} + [/event] +)} + +##### +# API(s) being tested: [event][filter_attack][filter_special] +## +# Actions: +# Use the common setup from FILTER_ABILITY_TEST. +# Add an event with a filter matching Alice's drains ability. +# Give Alice the Illuminates ability, which makes it the wrong time of day for her drains ability. +# The events in FILTER_ABILITY_TEST make Alice attacks Bob and then Bob attack Alice. +## +# Expected end state: +# The filtered event is triggered exactly once. +##### +{GENERIC_UNIT_TEST event_test_filter_special_simple_check ( + {MORNING} + {FILTER_ABILITY_TEST} + + [event] + name=attack + first_time_only=no + [filter_attack] + [filter_special] + tag_name=drains + value=25 + [/filter_special] + [/filter_attack] + {ASSERT ({VARIABLE_CONDITIONAL side_number equals 1})} + {ASSERT ({VARIABLE_CONDITIONAL triggers equals 0})} + {VARIABLE_OP triggers add 1} + [/event] + [event] + name=turn 2 + {RETURN ({VARIABLE_CONDITIONAL triggers equals 1})} + [/event] +)} + #undef FILTER_ABILITY_TEST ## diff --git a/src/units/abilities.cpp b/src/units/abilities.cpp index ace3bc5d9c60b..74741bf0a3d52 100644 --- a/src/units/abilities.cpp +++ b/src/units/abilities.cpp @@ -2095,6 +2095,105 @@ bool attack_type::special_matches_filter(const config & cfg, const std::string& return common_matches_filter(cfg, tag_name, filter); } +bool attack_type::has_special_with_filter(const config & filter) const +{ + using namespace utils::config_filters; + bool check_if_active = filter["active"].to_bool(); + for(const auto [key, cfg] : specials().all_children_view()) { + if(special_matches_filter(cfg, key, filter)){ + if(!check_if_active){ + return true; + } + if ( special_active(cfg, AFFECT_SELF, key) ) { + return true; + } + } + } + + if(!check_if_active || !other_attack_){ + return false; + } + + for(const auto [key, cfg] : other_attack_->specials().all_children_view()) { + if(other_attack_->special_matches_filter(cfg, key, filter)){ + if ( other_attack_->special_active(cfg, AFFECT_OTHER, key) ) { + return true; + } + } + } + + return false; +} + +bool attack_type::has_ability_with_filter(const config & filter) const +{ + bool check_if_active = filter["active"].to_bool(); + const unit_map& units = get_unit_map(); + if(self_){ + for(const auto [key, cfg] : (*self_).abilities().all_children_view()) { + if(self_->ability_matches_filter(cfg, key, filter)){ + if(!check_if_active){ + return true; + } + if(check_self_abilities(cfg, key)){ + return true; + } + } + } + + if(!check_if_active){ + return false; + } + + const auto adjacent = get_adjacent_tiles(self_loc_); + for(unsigned i = 0; i < adjacent.size(); ++i) { + const unit_map::const_iterator it = units.find(adjacent[i]); + if (it == units.end() || it->incapacitated()) + continue; + if ( &*it == self_.get() ) + continue; + + for(const auto [key, cfg] : it->abilities().all_children_view()) { + if(it->ability_matches_filter(cfg, key, filter) && check_adj_abilities(cfg, key, i , *it)){ + return true; + } + } + } + } + + if(other_){ + for(const auto [key, cfg] : (*other_).abilities().all_children_view()) { + if(other_->ability_matches_filter(cfg, key, filter) && check_self_abilities_impl(other_attack_, shared_from_this(), cfg, other_, other_loc_, AFFECT_OTHER, key)){ + return true; + } + } + + const auto adjacent = get_adjacent_tiles(other_loc_); + for(unsigned i = 0; i < adjacent.size(); ++i) { + const unit_map::const_iterator it = units.find(adjacent[i]); + if (it == units.end() || it->incapacitated()) + continue; + if ( &*it == other_.get() ) + continue; + + for(const auto [key, cfg] : it->abilities().all_children_view()) { + if(it->ability_matches_filter(cfg, key, filter) && check_adj_abilities_impl(other_attack_, shared_from_this(), cfg, other_, *it, i, other_loc_, AFFECT_OTHER, key)){ + return true; + } + } + } + } + return false; +} + +bool attack_type::has_special_or_ability_with_filter(const config & filter) const +{ + if(range().empty()){ + return false; + } + return (has_special_with_filter(filter) || has_ability_with_filter(filter)); +} + bool attack_type::special_active(const config& special, AFFECTS whom, const std::string& tag_name, const std::string& filter_self) const { diff --git a/src/units/attack_type.cpp b/src/units/attack_type.cpp index 591e553f53a77..8b6990172ef7b 100644 --- a/src/units/attack_type.cpp +++ b/src/units/attack_type.cpp @@ -260,6 +260,14 @@ static bool matches_simple_filter(const attack_type & attack, const config & fil } } + //children filter_special are checked later, + //but only when the function doesn't return earlier + if(auto sub_filter_special = filter.optional_child("filter_special")) { + if(!attack.has_special_or_ability_with_filter(*sub_filter_special)) { + return false; + } + } + if (!filter_formula.empty()) { try { const wfl::attack_type_callable callable(attack); @@ -306,6 +314,18 @@ bool attack_type::matches_filter(const config& filter, const std::string& check_ return matches; } +void attack_type::remove_special_by_filter(const config& filter) +{ + config::all_children_iterator i = specials_.ordered_begin(); + while (i != specials_.ordered_end()) { + if(special_matches_filter(i->cfg, i->key, filter)) { + i = specials_.erase(i); + } else { + ++i; + } + } +} + /** * Modifies *this using the specifications in @a cfg, but only if *this matches * @a cfg viewed as a filter. @@ -330,6 +350,7 @@ bool attack_type::apply_modification(const config& cfg) const std::string& set_min_range = cfg["set_min_range"]; const std::string& increase_max_range = cfg["increase_max_range"]; const std::string& set_max_range = cfg["set_max_range"]; + auto remove_specials = cfg.optional_child("remove_specials"); const std::string& increase_damage = cfg["increase_damage"]; const std::string& set_damage = cfg["set_damage"]; const std::string& increase_attacks = cfg["increase_attacks"]; @@ -415,6 +436,10 @@ bool attack_type::apply_modification(const config& cfg) max_range_ = utils::apply_modifier(max_range_, increase_max_range); } + if(remove_specials) { + remove_special_by_filter(*remove_specials); + } + if(set_damage.empty() == false) { damage_ = std::stoi(set_damage); if (damage_ < 0) { diff --git a/src/units/attack_type.hpp b/src/units/attack_type.hpp index c3f62329e7923..d42bbfa7b10d7 100644 --- a/src/units/attack_type.hpp +++ b/src/units/attack_type.hpp @@ -145,6 +145,17 @@ class attack_type : public std::enable_shared_from_this * uses when a defender has no weapon for a given range. */ bool attack_empty() const {return (id().empty() && name().empty() && type().empty() && range().empty());} + /** remove special if matche condition + * @param filter if special check with filter, it will be removed. + */ + void remove_special_by_filter(const config& filter); + /** check if special matche + * @return True if special matche with filter(if 'active' filter is true, check if special active). + * @param filter if special check with filter, return true. + */ + bool has_special_with_filter(const config & filter) const; + bool has_ability_with_filter(const config & filter) const; + bool has_special_or_ability_with_filter(const config & filter) const; // In unit_types.cpp: diff --git a/wml_test_schedule b/wml_test_schedule index b0f92c379b454..310d0e6208a4a 100644 --- a/wml_test_schedule +++ b/wml_test_schedule @@ -155,6 +155,9 @@ 0 event_test_filter_ability_wml_no_match 0 event_test_filter_ability_active 0 event_test_filter_ability_active_inactive +0 event_test_filter_special_active +0 event_test_filter_special_active_inactive +0 event_test_filter_special_simple_check 0 event_test_filter_ability_with_value_by_default 0 event_test_filter_ability_no_match_by_default 0 event_test_filter_ability_apply_to_resistance