[1.17] add [filter_ability] in [filter] events and [effect]remove_ability and [filter_ability_active] in [filter] events

see https://forums.wesnoth.org/viewtopic.php?p=681371#p681371 in forum

the type of ability used is also filtered.
This commit is contained in:
newfrenchy83 2022-10-14 20:49:17 +02:00 committed by Steve Cotton
parent e9c9bb62d5
commit f9a94e1312
11 changed files with 630 additions and 1 deletions

View file

@ -0,0 +1,2 @@
### WML Engine
* Add [filter_ability] usable instead of [abilities][tag name] to filter attributes including the type of ability used.

View file

@ -0,0 +1,21 @@
[tag]
name="$filter_abilities"
max=0
{SIMPLE_KEY id string_list}
{SIMPLE_KEY tag_name string_list}
{SIMPLE_KEY overwrite_specials string_list}
{SIMPLE_KEY apply_to string_list}
{SIMPLE_KEY active_on string_list}
{SIMPLE_KEY value s_range_list}
{SIMPLE_KEY add s_range_list}
{SIMPLE_KEY sub s_range_list}
{SIMPLE_KEY multiply s_real_range_list}
{SIMPLE_KEY divide s_real_range_list}
{SIMPLE_KEY affect_adjacent s_bool}
{SIMPLE_KEY affect_self s_bool}
{SIMPLE_KEY affect_allies s_bool}
{SIMPLE_KEY affect_enemies s_bool}
{DEFAULT_KEY type_value value_type empty}
{FILTER_BOOLEAN_OPS abilities}
[/tag]

View file

@ -46,6 +46,8 @@
{FILTER_TAG "filter_adjacent" adjacent ()}
{FILTER_TAG "filter_location" location ()}
{FILTER_TAG "filter_side" side ()}
{FILTER_TAG "filter_ability" abilities ()}
{FILTER_TAG "filter_ability_active" abilities ()}
{FILTER_BOOLEAN_OPS unit}
[/tag]

View file

@ -40,6 +40,10 @@
name="ability_overwrite"
value="none|one_side|both_sides"
[/type]
[type]
name="value_type"
value="empty|value|add|sub|multiply|divide"
[/type]
[type]
name="addon_type"
value="sp|mp|hybrid"
@ -88,6 +92,18 @@
value="\d+-infinity"
[/element]
)}
# definition of real_range_list and s_real_range_list, before macros are expanded
{LIST_TYPE_COMPLEX real_range (
[element]
value="\d+(\.\d+)?"
[/element]
[element]
value="\d+(\.\d+)?-\d+(\.\d+)?"
[/element]
[element]
value="\d+(\.\d+)?-infinity"
[/element]
)}
[type]
name=alignment
value="lawful|neutral|chaotic|liminal"
@ -375,6 +391,7 @@
{SUBST_TYPE coordinate}
{SUBST_TYPE coordinates}
{SUBST_TYPE range_list}
{SUBST_TYPE real_range_list}
{SUBST_TYPE terrain_code}
{SUBST_TYPE terrain_list}
{SUBST_TYPE dir}

View file

@ -140,9 +140,14 @@
[/tag]
[/case]
[case]
value=new_ability,remove_ability
value=new_ability
{LINK_TAG "units/unit_type/abilities"}
[/case]
[case]
value=remove_ability
{LINK_TAG "units/unit_type/abilities"}
{FILTER_TAG "filter_ability" abilities ()}
[/case]
[case]
value=new_animation
{SIMPLE_KEY id string}

View file

@ -0,0 +1,132 @@
#####
# API(s) being tested: [effect]apply_to=remove_ability[filter_ability]
##
# Actions:
# Two [chance_to_hit] abilities are added, setting the chance to 0 and 100 respectively.
# The one setting the chance to 100 is removed, filtering by id and tag_name.
# Have alice attack bob.
##
# Expected end state:
# All strikes for both units miss.
#####
{GENERIC_UNIT_TEST "test_remove_ability_by_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]
[chance_to_hit]
id=test_cth_0
value=0
[/chance_to_hit]
[chance_to_hit]
id=test_cth
value=100
[/chance_to_hit]
[/abilities]
[/effect]
[filter]
id=bob
[/filter]
[/object]
[object]
silent=yes
[effect]
apply_to=new_ability
[abilities]
[chance_to_hit]
id=test_cth_0
value=0
[/chance_to_hit]
[chance_to_hit]
id=test_cth
value=100
[/chance_to_hit]
[/abilities]
[/effect]
[filter]
id=alice
[/filter]
[/object]
[object]
[filter]
id=bob
[/filter]
silent=yes
[effect]
apply_to=remove_ability
[filter_ability]
id=test_cth
tag_name=chance_to_hit
[/filter_ability]
[/effect]
[/object]
[object]
[filter]
id=alice
[/filter]
silent=yes
[effect]
apply_to=remove_ability
[filter_ability]
id=test_cth
tag_name=chance_to_hit
[/filter_ability]
[/effect]
[/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]
{SUCCEED}
[/event]
[event]
name=attacker hits,defender hits
first_time_only=no
{FAIL}
[/event]
)}

View file

@ -0,0 +1,219 @@
# wmllint: no translatables
##
# Common code for the tests in this file.
##
# Actions:
# Give both Alice and Bob 100% chance to hit.
# Give Alice a drains ability which is only active during liminal times of day.
# During Alice's turn, move Alice next to Bob, and have Alice attack Bob.
# During Bob's turn, have Bob attack Alice.
##
#define FILTER_ABILITY_TEST
[event]
name=start
# Make sure the attacks hit
{FORCE_CHANCE_TO_HIT (id=bob) (id=alice) 100 ()}
{FORCE_CHANCE_TO_HIT (id=alice) (id=bob) 100 ()}
[modify_unit]
[filter]
[/filter]
# Make sure they don't die during the attacks
[status]
invulnerable=yes
[/status]
[/modify_unit]
[object]
silent=yes
[effect]
apply_to=new_ability
[abilities]
[drains]
value=25
[filter]
[filter_location]
time_of_day=neutral
[/filter_location]
[/filter]
[/drains]
[/abilities]
[/effect]
[filter]
id=alice
[/filter]
[/object]
{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]
#enddef
#####
# API(s) being tested: [event][filter][filter_ability]
##
# Actions:
# Use the common setup from FILTER_ABILITY_TEST.
# Add an event with a filter matching Alice's drains ability.
# 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_ability (
{FILTER_ABILITY_TEST}
[event]
name=attack
[filter]
[filter_ability]
tag_name=drains
value=25
[/filter_ability]
[/filter]
{ASSERT ({VARIABLE_CONDITIONAL side_number equals 1})}
{VARIABLE_OP triggers add 1}
[/event]
[event]
name=turn 2
{RETURN ({VARIABLE_CONDITIONAL triggers equals 1})}
[/event]
)}
#####
# API(s) being tested: [event][filter][filter_ability]
##
# Actions:
# Use the common setup from FILTER_ABILITY_TEST.
# Add an event with a filter for a drains ability, but a different value to Alice's ability.
# Alice attacks Bob and then Bob attacks Alice, as defined in FILTER_ABILITY_TEST.
##
# Expected end state:
# The filtered event is never triggered.
#####
{GENERIC_UNIT_TEST event_test_filter_ability_no_match (
{FILTER_ABILITY_TEST}
[event]
name=attack
[filter]
[filter_ability]
tag_name=drains
value=45
[/filter_ability]
[/filter]
{FAIL}
[/event]
[event]
name=turn 2
{SUCCEED}
[/event]
)}
#####
# API(s) being tested: [event][filter][filter_ability_active]
##
# 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_ability_active (
{FILTER_ABILITY_TEST}
[event]
name=attack
[filter]
[filter_ability_active]
tag_name=drains
value=25
[/filter_ability_active]
[/filter]
{ASSERT ({VARIABLE_CONDITIONAL side_number equals 1})}
{VARIABLE_OP triggers add 1}
[/event]
[event]
name=turn 2
{RETURN ({VARIABLE_CONDITIONAL triggers equals 1})}
[/event]
)}
#####
# API(s) being tested: [event][filter][filter_ability_active]
##
# 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_ability_active_inactive (
{FILTER_ABILITY_TEST}
[event]
name=start
[object]
silent=yes
[effect]
apply_to=new_ability
[abilities]
{ABILITY_ILLUMINATES}
[/abilities]
[/effect]
[filter]
id=alice
[/filter]
[/object]
[/event]
[event]
name=attack
[filter]
[filter_ability_active]
tag_name=drains
value=25
[/filter_ability_active]
[/filter]
{FAIL}
[/event]
[event]
name=turn 2
{SUCCEED}
[/event]
)}
#undef FILTER_ABILITY_TEST

View file

@ -806,6 +806,49 @@ void unit_filter_compound::fill(vconfig cfg)
return side_filter(c, args.fc).match(args.u.side());
});
}
else if (child.first == "filter_ability") {
create_child(child.second, [](const vconfig& c, const unit_filter_args& args) {
for(const config::any_child ab : args.u.abilities().all_children_range()) {
if(args.u.ability_matches_filter(ab.cfg, ab.key, c.get_parsed_config())) {
return true;
}
}
return false;
});
}
else if (child.first == "filter_ability_active") {
create_child(child.second, [](const vconfig& c, const unit_filter_args& args) {
if(!display::get_singleton()){
return false;
}
const unit_map& units = display::get_singleton()->get_units();
for(const config::any_child ab : args.u.abilities().all_children_range()) {
if(args.u.ability_matches_filter(ab.cfg, ab.key, c.get_parsed_config())) {
if (args.u.get_self_ability_bool(ab.cfg, ab.key, args.loc)) {
return true;
}
}
}
const auto adjacent = get_adjacent_tiles(args.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 == (args.u.shared_from_this()).get())
continue;
for(const config::any_child ab : it->abilities().all_children_range()) {
if(it->ability_matches_filter(ab.cfg, ab.key, c.get_parsed_config())) {
if (args.u.get_adj_ability_bool(ab.cfg, ab.key, i, args.loc, *it)) {
return true;
}
}
}
}
return false;
});
}
else if (child.first == "has_attack") {
create_child(child.second, [](const vconfig& c, const unit_filter_args& args) {
for(const attack_type& a : args.u.attacks()) {

View file

@ -1420,6 +1420,171 @@ void unit::remove_ability_by_id(const std::string& ability)
}
}
static bool bool_matches_if_present(const config& filter, const config& cfg, const std::string& attribute, bool def)
{
if(filter[attribute].empty()) {
return true;
}
return filter[attribute].to_bool() == cfg[attribute].to_bool(def);
}
static bool string_matches_if_present(const config& filter, const config& cfg, const std::string& attribute, const std::string& def)
{
if(filter[attribute].empty()) {
return true;
}
const std::vector<std::string> filter_attribute = utils::split(filter[attribute]);
return ( std::find(filter_attribute.begin(), filter_attribute.end(), cfg[attribute].str(def)) != filter_attribute.end() );
}
static bool int_matches_if_present(const config& filter, const config& cfg, const std::string& attribute)
{
if(filter[attribute].empty()) {
return true;
}
if(cfg[attribute].empty() && (attribute == "add" || attribute == "sub")){
if(attribute == "add"){
return in_ranges<int>(-cfg["sub"].to_int(0), utils::parse_ranges(filter[attribute].str()));
} else if(attribute == "sub"){
return in_ranges<int>(-cfg["add"].to_int(0), utils::parse_ranges(filter[attribute].str()));
} else {
return false;
}
}
return in_ranges<int>(cfg[attribute].to_int(0), utils::parse_ranges(filter[attribute].str()));
}
static bool double_matches_if_present(const config& filter, const config& cfg, const std::string& attribute)
{
if(filter[attribute].empty()) {
return true;
}
return in_ranges<double>(cfg[attribute].to_double(1), utils::parse_ranges_real(filter[attribute].str()));
}
static bool type_value_if_present(const config& filter, const config& cfg)
{
if(filter["type_value"].empty()) {
return true;
}
std::string cfg_type_value;
const std::vector<std::string> filter_attribute = utils::split(filter["type_value"]);
if(!cfg["value"].empty()){
cfg_type_value ="value";
} else if(!cfg["add"].empty()){
cfg_type_value ="add";
} else if(!cfg["sub"].empty()){
cfg_type_value ="sub";
} else if(!cfg["multiply"].empty()){
cfg_type_value ="multiply";
} else if(!cfg["divide"].empty()){
cfg_type_value ="divide";
}
return ( std::find(filter_attribute.begin(), filter_attribute.end(), cfg_type_value) != filter_attribute.end() );
}
static bool matches_ability_filter(const config & cfg, const std::string& tag_name, const config & filter)
{
if(!filter["affect_adjacent"].empty()){
auto cfg_adjacent = cfg.optional_child("affect_adjacent");
bool adjacent = cfg_adjacent ? true : false;;
if(filter["affect_adjacent"].to_bool() != adjacent){
return false;
}
}
if(!bool_matches_if_present(filter, cfg, "affect_self", true))
return false;
if(!bool_matches_if_present(filter, cfg, "affect_allies", true))
return false;
if(!bool_matches_if_present(filter, cfg, "affect_enemies", false))
return false;
if(!bool_matches_if_present(filter, cfg, "cumulative", false))
return false;
const std::vector<std::string> filter_type = utils::split(filter["tag_name"]);
if ( !filter_type.empty() && std::find(filter_type.begin(), filter_type.end(), tag_name) == filter_type.end() )
return false;
if(!string_matches_if_present(filter, cfg, "id", ""))
return false;
if(!string_matches_if_present(filter, cfg, "apply_to", "self"))
return false;
if(!string_matches_if_present(filter, cfg, "overwrite_specials", "none"))
return false;
if(!string_matches_if_present(filter, cfg, "active_on", "both"))
return false;
if(!int_matches_if_present(filter, cfg, "value"))
return false;
if(!int_matches_if_present(filter, cfg, "add"))
return false;
if(!int_matches_if_present(filter, cfg, "sub"))
return false;
if(!double_matches_if_present(filter, cfg, "multiply"))
return false;
if(!double_matches_if_present(filter, cfg, "divide"))
return false;
if(!type_value_if_present(filter, cfg))
return false;
// Passed all tests.
return true;
}
bool unit::ability_matches_filter(const config & cfg, const std::string& tag_name, const config & filter) const
{
// Handle the basic filter.
bool matches = matches_ability_filter(cfg, tag_name, filter);
// Handle [and], [or], and [not] with in-order precedence
for (const config::any_child condition : filter.all_children_range() )
{
// Handle [and]
if ( condition.key == "and" )
matches = matches && ability_matches_filter(cfg, tag_name, condition.cfg);
// Handle [or]
else if ( condition.key == "or" )
matches = matches || ability_matches_filter(cfg, tag_name, condition.cfg);
// Handle [not]
else if ( condition.key == "not" )
matches = matches && !ability_matches_filter(cfg, tag_name, condition.cfg);
}
return matches;
}
void unit::remove_ability_by_attribute(const config& filter)
{
set_attr_changed(UA_ABILITIES);
config::all_children_iterator i = abilities_.ordered_begin();
while (i != abilities_.ordered_end()) {
if(ability_matches_filter(i->cfg, i->key, filter)) {
i = abilities_.erase(i);
} else {
++i;
}
}
}
bool unit::get_attacks_changed() const
{
for(const auto& a_ptr : attacks_) {
@ -2127,10 +2292,14 @@ void unit::apply_builtin_effect(std::string apply_to, const config& effect)
}
} else if(apply_to == "remove_ability") {
if(auto ab_effect = effect.optional_child("abilities")) {
deprecated_message("[effect]apply_to=remove_ability [abilities]", DEP_LEVEL::INDEFINITE, "", "Use [filter_ability] instead in [effect]apply_to=remove_ability");
for(const config::any_child ab : ab_effect->all_children_range()) {
remove_ability_by_id(ab.cfg["id"]);
}
}
if(auto fab_effect = effect.optional_child("filter_ability")) {
remove_ability_by_attribute(*fab_effect);
}
} else if(apply_to == "image_mod") {
LOG_UT << "applying image_mod";
std::string mod = effect["replace"];

View file

@ -1772,6 +1772,20 @@ public:
*/
void remove_ability_by_id(const std::string& ability);
/**
* Removes a unit's abilities with a specific ID or other attribute.
* @param filter the config of ability to remove.
*/
void remove_ability_by_attribute(const config& filter);
/**
* Verify what abilities attributes match with filter.
* @param cfg the config of ability to check.
* @param tag_name the tag name of ability to check.
* @param filter the filter used for checking.
*/
bool ability_matches_filter(const config & cfg, const std::string& tag_name, const config & filter) const;
private:

View file

@ -142,6 +142,10 @@
0 event_test_filter_condition
0 event_test_filter_side
0 event_test_filter_unit
0 event_test_filter_ability
0 event_test_filter_ability_no_match
0 event_test_filter_ability_active
0 event_test_filter_ability_active_inactive
0 test_ability_id_active
0 test_ability_id_not_active
0 event_test_filter_attack
@ -320,6 +324,7 @@
0 test_force_chance_to_hit_macro
0 trait_exclusion_test
0 trait_requirement_test
0 test_remove_ability_by_filter
0 swarms_filter_student_by_type
0 swarms_effects_not_checkable
0 filter_special_id_active