check alternative_type and replacement_type when filtering on type

Also move them from being attributes of [damage] to [damage_type], to avoid infinite recursion when determining what the damage type should be.
This commit is contained in:
newfrenchy83 2023-11-10 23:35:57 +01:00 committed by GitHub
parent 47fec1e90f
commit 9474525612
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 235 additions and 80 deletions

View file

@ -1,2 +1,2 @@
### WML Engine
* Add a 'replacement_type' and 'alternative_type' attribute to [damage] to change the type of attack under specific conditions (terrain, time of day, leadership etc...)
* Add a [damage_type]'replacement_type' and 'alternative_type' attribute weapon special to change the type of attack under specific conditions (terrain, time of day, leadership etc...)

View file

@ -743,13 +743,10 @@
[effect]
apply_to=attack
range=melee
# the type=arcane or alternative_type=arcane must be filtered in case Garak picked up the holy
# Exclude arcane attacks in case Garak picked up the holy
# water in the second scenario - in that case he gets to keep it
[not]
type=arcane
[or]
alternative_type=arcane
[/or]
[/not]
set_type=fire
[/effect]

View file

@ -203,11 +203,11 @@
#enddef
#define WEAPON_SPECIAL_ARCANE
[damage]
[damage_type]
id=arcane_damage
name= _ "arcane"
description= _ "This attack combines the arcane type with the type of weapon used so that resistance to the arcane type does not penalize the user."
special_note=_ "This unit can use the arcane type when the opponent is particularly sensitive to it in relation to the weapon on which it is applied."
alternative_type=arcane
[/damage]
[/damage_type]
#enddef

View file

@ -6,6 +6,9 @@
{SIMPLE_KEY tag_name string_list}
{SIMPLE_KEY overwrite_specials ability_overwrite}
{SIMPLE_KEY apply_to string_list}
{SIMPLE_KEY type string_list}
{SIMPLE_KEY replacement_type string_list}
{SIMPLE_KEY alternative_type string_list}
{SIMPLE_KEY active_on ability_context}
{SIMPLE_KEY value s_int_range_list}
{SIMPLE_KEY add s_int_range_list}

View file

@ -5,8 +5,6 @@
{SIMPLE_KEY range string_list}
{SIMPLE_KEY name string_list}
{SIMPLE_KEY type string_list}
{SIMPLE_KEY replacement_type string_list}
{SIMPLE_KEY alternative_type string_list}
{SIMPLE_KEY special string_list}
{SIMPLE_KEY special_active string_list}
{SIMPLE_KEY special_id string_list}

View file

@ -133,6 +133,7 @@
{BASED_ON_SPECIAL "swarm"}
{BASED_ON_SPECIAL "chance_to_hit"}
{BASED_ON_SPECIAL "damage"}
{BASED_ON_SPECIAL "damage_type"}
{BASED_ON_SPECIAL "drains"}
{BASED_ON_SPECIAL "heal_on_hit"}
{BASED_ON_SPECIAL "berserk"}

View file

@ -75,8 +75,19 @@
name="damage"
max=infinite
super="units/unit_type/attack/specials/~value~"
[/tag]
[tag]
name="damage_type"
max=infinite
super="units/unit_type/attack/specials/~generic~"
{SIMPLE_KEY replacement_type string}
{SIMPLE_KEY alternative_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="drains"

View file

@ -0,0 +1,91 @@
# 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 (
[event]
name=start
[object]
silent=yes
[effect]
apply_to=new_ability
[abilities]
[damage_type]
id=test_arcane_damage
alternative_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]
type=arcane
[/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]
)}

View file

@ -1,20 +1,15 @@
#textdomain wesnoth-test
#####
# API(s) being tested: [damage]replacement_type=
##
# 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
# 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
# and change Alice resistance to cold to -100% and fire to 50%.
# Move Alice next to Bob, and have Alice attack Bob.
##
# Expected end state:
# Alice attack with fire and Bob use cold.
#####
{GENERIC_UNIT_TEST "damage_type_test" (
# macro used for two tests below
#define FILTER_TYPE_BLADE
[filter_self]
[filter_weapon]
type=blade
[/filter_weapon]
[/filter_self]
#enddef
#define DAMAGE_TYPE_TEST FILTER_WEAPON
[event]
name=start
[modify_unit]
@ -44,15 +39,18 @@
[damage]
value=12
[/damage]
[damage]
[damage_type]
replacement_type=fire
[/damage]
[damage]
{FILTER_WEAPON}
[/damage_type]
[damage_type]
replacement_type=cold
[/damage]
[damage]
{FILTER_WEAPON}
[/damage_type]
[damage_type]
replacement_type=cold
[/damage]
{FILTER_WEAPON}
[/damage_type]
[chance_to_hit]
value=100
[/chance_to_hit]
@ -82,24 +80,24 @@
[damage]
value=12
[/damage]
[damage]
[damage_type]
replacement_type=cold
[/damage]
[damage]
[/damage_type]
[damage_type]
replacement_type=arcane
[/damage]
[damage]
[/damage_type]
[damage_type]
replacement_type=arcane
[/damage]
[damage]
[/damage_type]
[damage_type]
replacement_type=fire
[/damage]
[damage]
[/damage_type]
[damage_type]
replacement_type=fire
[/damage]
[damage]
[/damage_type]
[damage_type]
replacement_type=fire
[/damage]
[/damage_type]
[chance_to_hit]
value=100
[/chance_to_hit]
@ -166,6 +164,42 @@
{ASSERT ({VARIABLE_CONDITIONAL b.hitpoints equals 76})}
{SUCCEED}
[/event]
#enddef
#####
# API(s) being tested: [damage]replacement_type=
##
# 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
# 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
# and change Alice resistance to cold to -100% and fire to 50%.
# Move Alice next to Bob, and have Alice attack Bob.
##
# Expected end state:
# Alice attack with fire and Bob use cold.
#####
{GENERIC_UNIT_TEST "damage_type_test" (
{DAMAGE_TYPE_TEST ()}
)}
#####
# API(s) being tested: [damage]replacement_type=[filter_self][filter_weapon]type=blade
##
# 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
# 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
# and change Alice resistance to cold to -100% and fire to 50%.
# Move Alice next to Bob, and have Alice attack Bob.
##
# Expected end state:
# Alice attack with fire; and Bob use cold because Bob [damage] filter must check type=blade and not the replacement type, that who cause infinite recursion.
#####
{GENERIC_UNIT_TEST "damage_type_with_filter_test" (
{DAMAGE_TYPE_TEST {FILTER_TYPE_BLADE}}
)}
#####
@ -212,9 +246,9 @@
[damage]
value=12
[/damage]
[damage]
[damage_type]
alternative_type=cold
[/damage]
[/damage_type]
[chance_to_hit]
value=100
[/chance_to_hit]
@ -244,9 +278,9 @@
[damage]
value=12
[/damage]
[damage]
[damage_type]
alternative_type=fire
[/damage]
[/damage_type]
[chance_to_hit]
value=100
[/chance_to_hit]

View file

@ -134,7 +134,7 @@ void unit_attack::pre_show(window& window)
defw_type_second = ", " + string_table["type_" + defw_type_second];
}
const std::set<std::string> checking_tags_other = {"disable", "berserk", "drains", "heal_on_hit", "plague", "slow", "petrifies", "firststrike", "poison"};
const std::set<std::string> checking_tags_other = {"damage_type", "disable", "berserk", "drains", "heal_on_hit", "plague", "slow", "petrifies", "firststrike", "poison"};
std::string attw_specials = attacker_weapon.weapon_specials();
std::string attw_specials_dmg = attacker_weapon.weapon_specials_value({"leadership", "damage"});
std::string attw_specials_atk = attacker_weapon.weapon_specials_value({"attacks", "swarm"});

View file

@ -1197,7 +1197,7 @@ static std::vector<std::string> damage_type_list(const unit_ability_list& abil_l
*/
std::pair<std::string, std::string> attack_type::damage_type() const
{
unit_ability_list abil_list = get_specials_and_abilities("damage");
unit_ability_list abil_list = get_specials_and_abilities("damage_type");
if(abil_list.empty()){
return {type(), ""};
}
@ -1207,6 +1207,9 @@ std::pair<std::string, std::string> attack_type::damage_type() const
std::string type_damage, sec_type_damage;
type_damage = !type_list.empty() ? type_list.front() : type();
sec_type_damage = !added_type_list.empty() ? added_type_list.front() : "";
if(!sec_type_damage.empty()){
sec_type_damage = type_damage != sec_type_damage ? sec_type_damage: "";
}
return {type_damage, sec_type_damage};
}
@ -1273,11 +1276,12 @@ namespace { // Helpers for attack_type::special_active()
* (normally a [filter_*] child) of the provided filter.
* @param[in] u A unit to filter.
* @param[in] u2 Another unit to filter.
* @param[in] loc The presumed location of @a un_it.
* @param[in] loc The presumed location of @a unit.
* @param[in] weapon The attack_type to filter.
* @param[in] filter The filter containing the child filter to use.
* @param[in] for_listing
* @param[in] child_tag The tag of the child filter to use.
* @param[in] tag_name Parameter used for don't have infinite recusion for some filter attribute.
*/
static bool special_unit_matches(unit_const_ptr & u,
unit_const_ptr & u2,
@ -1285,7 +1289,7 @@ namespace { // Helpers for attack_type::special_active()
const_attack_ptr weapon,
const config & filter,
const bool for_listing,
const std::string & child_tag)
const std::string & child_tag, const std::string& tag_name)
{
if (for_listing && !loc.valid())
// The special's context was set to ignore this unit, so assume we pass.
@ -1312,7 +1316,7 @@ namespace { // Helpers for attack_type::special_active()
// Check for a weapon match.
if (auto filter_weapon = filter_child->optional_child("filter_weapon") ) {
if ( !weapon || !weapon->matches_filter(*filter_weapon) )
if ( !weapon || !weapon->matches_filter(*filter_weapon, tag_name) )
return false;
}
@ -1810,13 +1814,20 @@ bool attack_type::special_active_impl(
const config& special_backstab = special["backstab"].to_bool() ? cfg : special;
// Filter the units involved.
if (!special_unit_matches(self, other, self_loc, self_attack, special, is_for_listing, filter_self))
//If filter concerns the unit on which special is applied,
//then the type of special must be entered to avoid calling
//the function of this special in matches_filter()
std::string self_tag_name = whom_is_self ? tag_name : "";
if (!special_unit_matches(self, other, self_loc, self_attack, special, is_for_listing, filter_self, self_tag_name))
return false;
if (!special_unit_matches(other, self, other_loc, other_attack, special_backstab, is_for_listing, "filter_opponent"))
std::string opp_tag_name = !whom_is_self ? tag_name : "";
if (!special_unit_matches(other, self, other_loc, other_attack, special_backstab, is_for_listing, "filter_opponent", opp_tag_name))
return false;
if (!special_unit_matches(att, def, att_loc, att_weapon, special, is_for_listing, "filter_attacker"))
std::string att_tag_name = is_attacker ? tag_name : "";
if (!special_unit_matches(att, def, att_loc, att_weapon, special, is_for_listing, "filter_attacker", att_tag_name))
return false;
if (!special_unit_matches(def, att, def_loc, def_weapon, special, is_for_listing, "filter_defender"))
std::string def_tag_name = !is_attacker ? tag_name : "";
if (!special_unit_matches(def, att, def_loc, def_weapon, special, is_for_listing, "filter_defender", def_tag_name))
return false;
const auto adjacent = get_adjacent_tiles(self_loc);

View file

@ -101,19 +101,17 @@ std::string attack_type::accuracy_parry_description() const
* Returns whether or not *this matches the given @a filter, ignoring the
* complexities introduced by [and], [or], and [not].
*/
static bool matches_simple_filter(const attack_type & attack, const config & filter)
static bool matches_simple_filter(const attack_type & attack, const config & filter, const std::string& tag_name)
{
const std::vector<std::string>& filter_range = utils::split(filter["range"]);
const std::set<std::string> filter_range = utils::split_set(filter["range"].str());
const std::string& filter_damage = filter["damage"];
const std::string& filter_attacks = filter["number"];
const std::string& filter_accuracy = filter["accuracy"];
const std::string& filter_parry = filter["parry"];
const std::string& filter_movement = filter["movement_used"];
const std::string& filter_attacks_used = filter["attacks_used"];
const std::vector<std::string> filter_name = utils::split(filter["name"]);
const std::vector<std::string> filter_type = utils::split(filter["type"]);
const std::vector<std::string> filter_replacement_type = utils::split(filter["replacement_type"]);
const std::vector<std::string> filter_alternative_type = utils::split(filter["alternative_type"]);
const std::set<std::string> filter_name = utils::split_set(filter["name"].str());
const std::set<std::string> filter_type = utils::split_set(filter["type"].str());
const std::vector<std::string> filter_special = utils::split(filter["special"]);
const std::vector<std::string> filter_special_id = utils::split(filter["special_id"]);
const std::vector<std::string> filter_special_type = utils::split(filter["special_type"]);
@ -122,7 +120,7 @@ static bool matches_simple_filter(const attack_type & attack, const config & fil
const std::vector<std::string> filter_special_type_active = utils::split(filter["special_type_active"]);
const std::string filter_formula = filter["formula"];
if ( !filter_range.empty() && std::find(filter_range.begin(), filter_range.end(), attack.range()) == filter_range.end() )
if ( !filter_range.empty() && filter_range.count(attack.range()) == 0 )
return false;
if ( !filter_damage.empty() && !in_ranges(attack.damage(), utils::parse_ranges_unsigned(filter_damage)) )
@ -143,21 +141,19 @@ static bool matches_simple_filter(const attack_type & attack, const config & fil
if (!filter_attacks_used.empty() && !in_ranges(attack.attacks_used(), utils::parse_ranges_unsigned(filter_attacks_used)))
return false;
if ( !filter_name.empty() && std::find(filter_name.begin(), filter_name.end(), attack.id()) == filter_name.end() )
if ( !filter_name.empty() && filter_name.count(attack.id()) == 0)
return false;
if ( !filter_type.empty() && std::find(filter_type.begin(), filter_type.end(), attack.type()) == filter_type.end() )
return false;
if(!filter_alternative_type.empty() || !filter_replacement_type.empty()){
std::pair<std::string, std::string> damage_type = attack.damage_type();
if(!filter_replacement_type.empty()){
if ( std::find(filter_replacement_type.begin(), filter_replacement_type.end(), damage_type.first) == filter_replacement_type.end() ){
if (!filter_type.empty()){
//if special is type "damage_type" then check attack.type() only for don't have infinite recursion by calling damage_type() below.
if(tag_name == "damage_type"){
if (filter_type.count(attack.type()) == 0){
return false;
}
}
if(!filter_alternative_type.empty()){
if ( std::find(filter_alternative_type.begin(), filter_alternative_type.end(), damage_type.second) == filter_alternative_type.end() ){
} else {
//if the type is different from "damage_type" then damage_type() can be called for safe checking.
std::pair<std::string, std::string> damage_type = attack.damage_type();
if (filter_type.count(damage_type.first) == 0 && filter_type.count(damage_type.second) == 0){
return false;
}
}
@ -261,25 +257,25 @@ static bool matches_simple_filter(const attack_type & attack, const config & fil
/**
* Returns whether or not *this matches the given @a filter.
*/
bool attack_type::matches_filter(const config& filter) const
bool attack_type::matches_filter(const config& filter, const std::string& tag_name) const
{
// Handle the basic filter.
bool matches = matches_simple_filter(*this, filter);
bool matches = matches_simple_filter(*this, filter, tag_name);
// 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 && matches_filter(condition.cfg);
matches = matches && matches_filter(condition.cfg, tag_name);
// Handle [or]
else if ( condition.key == "or" )
matches = matches || matches_filter(condition.cfg);
matches = matches || matches_filter(condition.cfg, tag_name);
// Handle [not]
else if ( condition.key == "not" )
matches = matches && !matches_filter(condition.cfg);
matches = matches && !matches_filter(condition.cfg, tag_name);
}
return matches;

View file

@ -124,7 +124,7 @@ public:
// In unit_types.cpp:
bool matches_filter(const config& filter) const;
bool matches_filter(const config& filter, const std::string& tag_name = "") const;
bool apply_modification(const config& cfg);
bool describe_modification(const config& cfg,std::string* description);

View file

@ -1460,6 +1460,17 @@ static bool matches_ability_filter(const config & cfg, const std::string& tag_na
if(!string_matches_if_present(filter, cfg, "active_on", "both"))
return false;
//for damage only
if(!string_matches_if_present(filter, cfg, "replacement_type", ""))
return false;
if(!string_matches_if_present(filter, cfg, "alternative_type", ""))
return false;
//for plague only
if(!string_matches_if_present(filter, cfg, "type", ""))
return false;
if(!filter["value"].empty()){
bool has_other_key = (!cfg["add"].empty() || !cfg["sub"].empty() || !cfg["multiply"].empty() || !cfg["divide"].empty());
if(!has_other_key){

View file

@ -1803,7 +1803,7 @@ public:
private:
const std::set<std::string> checking_tags_{"attacks", "damage", "chance_to_hit", "berserk", "swarm", "drains", "heal_on_hit", "plague", "slow", "petrifies", "firststrike", "poison"};
const std::set<std::string> checking_tags_{"attacks", "damage", "chance_to_hit", "berserk", "swarm", "drains", "heal_on_hit", "plague", "slow", "petrifies", "firststrike", "poison", "damage_type"};
/**
* Check if an ability is active.
* @param ability The type (tag name) of the ability

View file

@ -155,6 +155,7 @@
0 test_ability_id_not_active
0 event_test_filter_attack
0 event_test_filter_attack_no_defense
0 event_test_filter_attack_type
0 event_test_filter_attack_specials
0 event_test_filter_attack_on_moveto
0 event_test_filter_attack_opponent_weapon_condition
@ -335,6 +336,7 @@
0 test_remove_ability_by_filter
0 test_overwrite_specials_filter
0 damage_type_test
0 damage_type_with_filter_test
0 damage_secondary_type_test
0 swarms_filter_student_by_type
0 swarms_effects_not_checkable