Have [filter_attack/weapon]type= pick the type which does more damage (#8936)

resolve [filter_attack/weapon]type= can check both type when it wait only effective type.

* add 'base_type' filter for detect original type of attack reagrdless of [damage_type] modifications

if for some reason the original type must be filtered instead of effective type, this attribute is here for that.

* add effective_type in formulas
This commit is contained in:
newfrenchy83 2025-02-01 17:36:55 +01:00 committed by GitHub
parent 2c0a1cdaa7
commit 0dce4e4731
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 269 additions and 118 deletions

View File

@ -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}

View File

@ -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

View File

@ -74,12 +74,8 @@ class unit_adapter {
*/
int damage_from(const attack_type& attack) const {
if(unit_type_ != nullptr) {
std::pair<std::string, std::string> 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());

View File

@ -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");

View File

@ -206,12 +206,7 @@ void attack_predictions::set_data(const combatant_data& attacker, const combatan
}
}
std::pair<std::string, std::string> 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());

View File

@ -109,24 +109,16 @@ void unit_attack::pre_show()
attacker_itor_->get_location(), false, attacker.weapon
);
std::pair<std::string, std::string> 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<std::string, std::string> 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<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();
@ -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, "%")

View File

@ -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<std::string> alt_types = at.alternative_damage_types();
std::pair<std::string, std::set<std::string>> all_damage_types = at.damage_types();
std::string type = all_damage_types.first;
std::set<std::string> 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];

View File

@ -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<std::string, unsigned int> 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<std::string> 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<std::string, int> attack_type::select_alternative_type(const unit_ability_list& damage_type_list, const unit_ability_list& resistance_list) const
{
std::map<std::string, int> type_res;
int max_res = std::numeric_limits<int>::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<std::string> 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<std::string, std::string> attack_type::damage_type() const
std::pair<std::string, int> 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<std::string, int> 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<std::string> 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<std::string, std::set<std::string>> attack_type::damage_types() const
{
unit_ability_list damage_type_list = get_specials_and_abilities("damage_type");
std::set<std::string> alternative_damage_types;
if(damage_type_list.empty()){
return {};
return {type(), alternative_damage_types};
}
std::set<std::string> 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.
*/

View File

@ -116,6 +116,7 @@ static bool matches_simple_filter(const attack_type & attack, const config & fil
const std::set<std::string> filter_alignment = utils::split_set(filter["alignment"].str());
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::set<std::string> filter_base_type = utils::split_set(filter["base_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"]);
@ -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<std::string, std::string> 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;

View File

@ -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<std::string, std::string> damage_type() const;
/** @return A list of alternative_type damage types. */
std::set<std::string> 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<std::string, std::set<std::string>> damage_types() const;
/** @return The type of attack used and the resistance value that does the most damage. */
std::pair<std::string, int> 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<std::string, int> 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.

View File

@ -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);
}

View File

@ -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