Add an 'alignment' attribute to attacks

This attribute, when set to a valid value of lawful|neutral|chaotic|liminal, will assign to the weapon a different alignment than the unit alignment used by default. This alignment is then used when attacking with this weapon.

The attribute is not accessible from lua so far since the fallback to unit alignment does not work.
This commit is contained in:
newfrenchy83 2024-09-23 17:11:46 +02:00 committed by GitHub
parent 363241faf9
commit feef53d4ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 324 additions and 5 deletions

View file

@ -0,0 +1,2 @@
### WML Engine
* Add alignment in [attack] to specify the alignment of an attack independent of the unit's alignment.

View file

@ -3,6 +3,7 @@
name="$filter_weapon"
max=0
{SIMPLE_KEY range string_list}
{SIMPLE_KEY alignment alignment}
{SIMPLE_KEY name string_list}
{SIMPLE_KEY type string_list}
{SIMPLE_KEY special string_list}

View file

@ -27,7 +27,8 @@
{SIMPLE_KEY set_type string}
{SIMPLE_KEY set_icon string}
{SIMPLE_KEY set_range string}
{SIMPLE_KEY set_alignment string}
{SIMPLE_KEY set_damage s_int}
{SIMPLE_KEY set_attacks s_int}
{SIMPLE_KEY set_parry s_int}

View file

@ -50,6 +50,7 @@
{SIMPLE_KEY icon string}
{SIMPLE_KEY type string}
{SIMPLE_KEY range string}
{SIMPLE_KEY alignment alignment}
{SIMPLE_KEY damage int}
{SIMPLE_KEY number int}
{SIMPLE_KEY defense_weight real}

View file

@ -0,0 +1,245 @@
#textdomain wesnoth-test
#####
# API(s) being tested: [attack]alignment=
##
# Actions:
# Give a lawful time of day.
# Give the leaders an alignment of lawful.
# Have the leaders fight each other.
##
# Expected end state:
# The attacker inflicted 25% more damage because the weapon alignment is lawful at a lawful time of day.
#####
{COMMON_KEEP_A_B_UNIT_TEST "attack_alignment_test" (
[event]
name=start
[modify_unit]
[filter]
[/filter]
[effect]
apply_to=attack
set_alignment=lawful
[/effect]
[/modify_unit]
{ATTACK_AND_VALIDATE 125}
{SUCCEED}
[/event]
) TIME=MORNING}
#####
# API(s) being tested: [attack]alignment=
##
# Actions:
# Give an illumination ability to leaders.
# Give the leaders an alignment of lawful.
# Have the leaders fight each other.
##
# Expected end state:
# The attacker inflicted 25% more damage because the weapon alignment is lawful and illumination provides a lawful time of day.
#####
{COMMON_KEEP_A_B_UNIT_TEST "attack_alignment_test_with_illuminates" (
[event]
name=start
[modify_unit]
[filter]
[/filter]
[effect]
apply_to=new_ability
[abilities]
{ABILITY_ILLUMINATES}
[/abilities]
[/effect]
[effect]
apply_to=attack
set_alignment=lawful
[/effect]
[/modify_unit]
{ATTACK_AND_VALIDATE 125}
{SUCCEED}
[/event]
)}
#define FILTER_ATTACK_ALIGNMENT_TEST
[event]
name=start
[modify_unit]
[filter]
[/filter]
[status]
invulnerable=yes
[/status]
[/modify_unit]
[object]
silent=yes
[effect]
apply_to=attack
set_alignment=lawful
[/effect]
[filter]
id=alice
[/filter]
[/object]
{VARIABLE triggers_on_attack 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_attack]alignment=
##
# Actions:
# Give Alice alignment=lawful.
# Define events that use filter_attack matching Alice's alignment.
# Have Alice attack bob.
##
# Expected end state:
# An event triggers when Alice attacks during side 1's turn.
#####
{GENERIC_UNIT_TEST event_test_filter_attack_attack_alignment (
{FILTER_ATTACK_ALIGNMENT_TEST}
# Event when Alice attacks
[event]
name=attack
first_time_only=no
[filter_attack]
alignment=lawful
[/filter_attack]
{ASSERT ({VARIABLE_CONDITIONAL side_number equals 1})}
{VARIABLE_OP triggers_on_attack add 1}
[/event]
[event]
name=turn 2
{ASSERT ({VARIABLE_CONDITIONAL triggers_on_attack equals 1})}
{SUCCEED}
[/event]
)}
# API(s) being tested: [event][filter_attack]alignment=
##
# Actions:
# Give Alice alignment=lawful.
# Define events that use filter_attack not matching Alice's alignment.
# Have Alice attack bob.
##
# Expected end state:
# No event triggers when Alice attacks during side 1's turn.
#####
{GENERIC_UNIT_TEST event_test_filter_attack_attack_alignment_no_match (
{FILTER_ATTACK_ALIGNMENT_TEST}
# Event when Alice attacks
[event]
name=attack
first_time_only=no
[filter_attack]
alignment=neutral
[/filter_attack]
{ASSERT ({VARIABLE_CONDITIONAL side_number equals 1})}
{VARIABLE_OP triggers_on_attack add 1}
[/event]
[event]
name=turn 2
{ASSERT ({VARIABLE_CONDITIONAL triggers_on_attack equals 0})}
{SUCCEED}
[/event]
)}
# API(s) being tested: [event][filter_attack]formula="alignment="
##
# Actions:
# Give Alice alignment=lawful.
# Define events that use filter_attack matching Alice's alignment.
# Have Alice attack bob.
##
# Expected end state:
# An event triggers when Alice attacks during side 1's turn.
#####
{GENERIC_UNIT_TEST event_test_filter_attack_formula_attack_alignment (
{FILTER_ATTACK_ALIGNMENT_TEST}
# Event when Alice attacks
[event]
name=attack
first_time_only=no
[filter_attack]
formula="alignment='lawful'"
[/filter_attack]
{ASSERT ({VARIABLE_CONDITIONAL side_number equals 1})}
{VARIABLE_OP triggers_on_attack add 1}
[/event]
[event]
name=turn 2
{ASSERT ({VARIABLE_CONDITIONAL triggers_on_attack equals 1})}
{SUCCEED}
[/event]
)}
# API(s) being tested: [event][filter_attack]formula="alignment="
##
# Actions:
# Give Alice alignment=lawful.
# Define events that use filter_attack no matching Alice's alignment.
# Have Alice attack bob.
##
# Expected end state:
# No event triggers when Alice attacks during side 1's turn.
#####
{GENERIC_UNIT_TEST event_test_filter_attack_formula_attack_alignment_no_match (
{FILTER_ATTACK_ALIGNMENT_TEST}
# Event when Alice attacks
[event]
name=attack
first_time_only=no
[filter_attack]
formula="alignment='neutral'"
[/filter_attack]
{ASSERT ({VARIABLE_CONDITIONAL side_number equals 1})}
{VARIABLE_OP triggers_on_attack add 1}
[/event]
[event]
name=turn 2
{ASSERT ({VARIABLE_CONDITIONAL triggers_on_attack equals 0})}
{SUCCEED}
[/event]
)}
#undef FILTER_ATTACK_ALIGNMENT_TEST

View file

@ -188,7 +188,7 @@ battle_context_unit_stats::battle_context_unit_stats(nonempty_unit_const_ptr up,
// Time of day bonus.
damage_multiplier += combat_modifier(
resources::gameboard->units(), resources::gameboard->map(), u_loc, u.alignment(), u.is_fearless());
resources::gameboard->units(), resources::gameboard->map(), u_loc, weapon->alignment(), u.is_fearless());
// Leadership bonus.
int leader_bonus = under_leadership(u, u_loc, weapon, opp_weapon);
@ -317,7 +317,7 @@ battle_context_unit_stats::battle_context_unit_stats(const unit_type* u_type,
int base_damage = weapon->modified_damage();
int damage_multiplier = 100;
damage_multiplier
+= generic_combat_modifier(lawful_bonus, u_type->alignment(), u_type->musthave_status("fearless"), 0);
+= generic_combat_modifier(lawful_bonus, weapon->alignment(), u_type->musthave_status("fearless"), 0);
damage_multiplier *= opp_type->resistance_against(weapon->type(), !attacking);
damage = round_damage(base_damage, damage_multiplier, 10000);

View file

@ -93,6 +93,8 @@ variant attack_type_callable::get_value(const std::string& key) const
return variant(att_->icon());
} else if(key == "range") {
return variant(att_->range());
} else if(key == "alignment") {
return variant(att_->alignment_str());
} else if(key == "damage") {
return variant(att_->damage());
} else if(key == "number_of_attacks" || key == "number" || key == "num_attacks" || key == "attacks") {
@ -134,6 +136,7 @@ void attack_type_callable::get_inputs(formula_input_vector& inputs) const
add_input(inputs, "description");
add_input(inputs, "icon");
add_input(inputs, "range");
add_input(inputs, "alignment");
add_input(inputs, "damage");
add_input(inputs, "number");
add_input(inputs, "accuracy");
@ -174,6 +177,10 @@ int attack_type_callable::do_compare(const formula_callable* callable) const
return att_->range().compare(att_callable->att_->range());
}
if(att_->alignment_str() != att_callable->att_->alignment_str()) {
return att_->alignment_str().compare(att_callable->att_->alignment_str());
}
const auto self_specials = att_->specials().all_children_range();
const auto other_specials = att_callable->att_->specials().all_children_range();
if(self_specials.size() != other_specials.size()) {

View file

@ -225,7 +225,7 @@ void attack_predictions::set_data(window& window, const combatant_data& attacker
const unit& u = *attacker.unit_;
const int tod_modifier = combat_modifier(resources::gameboard->units(), resources::gameboard->map(),
u.get_location(), u.alignment(), u.is_fearless());
u.get_location(), weapon->alignment(), u.is_fearless());
if(tod_modifier != 0) {
set_label_helper("tod_modifier", utils::signed_percent(tod_modifier));

View file

@ -786,7 +786,8 @@ static int attack_info(const reports::context& rc, const attack_type &at, config
int specials_damage = at.modified_damage();
int damage_multiplier = 100;
const_attack_ptr weapon = at.shared_from_this();
int tod_bonus = combat_modifier(get_visible_time_of_day_at(rc, hex), u.alignment(), u.is_fearless());
unit_alignments::type attack_alignment = weapon->alignment();
int tod_bonus = combat_modifier(get_visible_time_of_day_at(rc, hex), attack_alignment, u.is_fearless());
damage_multiplier += tod_bonus;
int leader_bonus = under_leadership(u, hex, weapon);
if (leader_bonus != 0)
@ -958,6 +959,24 @@ static int attack_info(const reports::context& rc, const attack_type &at, config
add_text(res, damage_and_num_attacks.str, damage_and_num_attacks.tooltip);
add_text(res, damage_versus.str, damage_versus.tooltip); // This string is usually empty
if(attack_alignment != u.alignment()){
const std::string align = unit_type::alignment_description(attack_alignment, u.gender());
const std::string align_id = unit_alignments::get_string(attack_alignment);
color_t color = font::weapon_color;
if(tod_bonus != 0) {
color = (tod_bonus > 0) ? font::good_dmg_color : font::bad_dmg_color;
}
str << " " << align << " (" << span_color(color) << utils::signed_percent(tod_bonus)
<< naps << ")" << "\n";
tooltip << _("Alignment: ") << "<b>" << align << "</b>\n"
<< string_table[align_id + "_description" ] + "\n";
add_text(res, flush(str), flush(tooltip));
}
const std::string &accuracy_parry = at.accuracy_parry_description();
if (!accuracy_parry.empty())
{

View file

@ -188,6 +188,7 @@ static int impl_unit_attacks_set(lua_State* L)
if(iter == end) {
atk = u.add_attack(end, cfg);
} else {
auto ctx = atk->specials_context(u.shared_from_this(), map_location::null_location(), true);
iter.base()->reset(new attack_type(cfg));
atk = *iter.base();
}
@ -257,6 +258,7 @@ static int impl_unit_attack_get(lua_State *L)
return_string_attrib("type", attack.type());
return_string_attrib("icon", attack.icon());
return_string_attrib("range", attack.range());
return_string_attrib("alignment", attack.alignment_str());
return_int_attrib("damage", attack.damage());
return_int_attrib("number", attack.num_attacks());
return_float_attrib("attack_weight", attack.attack_weight());
@ -292,6 +294,7 @@ static int impl_unit_attack_set(lua_State *L)
modify_string_attrib("type", attack.set_type(value));
modify_string_attrib("icon", attack.set_icon(value));
modify_string_attrib("range", attack.set_range(value));
modify_string_attrib("alignment", attack.set_range(value));
modify_int_attrib("damage", attack.set_damage(value));
modify_int_attrib("number", attack.set_num_attacks(value));
modify_int_attrib("attack_weight", attack.set_attack_weight(value));

View file

@ -19,6 +19,7 @@
*/
#include "units/attack_type.hpp"
#include "units/unit.hpp"
#include "formula/callable_objects.hpp"
#include "formula/formula.hpp"
#include "formula/string_utils.hpp"
@ -79,6 +80,7 @@ attack_type::attack_type(const config& cfg) :
range_(cfg["range"]),
min_range_(cfg["min_range"].to_int(1)),
max_range_(cfg["max_range"].to_int(1)),
alignment_str_(),
damage_(cfg["damage"]),
num_attacks_(cfg["number"]),
attack_weight_(cfg["attack_weight"].to_double(1.0)),
@ -99,6 +101,17 @@ attack_type::attack_type(const config& cfg) :
else
icon_ = "attacks/blank-attack.png";
}
if(cfg.has_attribute("alignment") && (cfg["alignment"] == "neutral" || cfg["alignment"] == "lawful" || cfg["alignment"] == "chaotic" || cfg["alignment"] == "liminal")){
alignment_str_ = cfg["alignment"].str();
} else if(self_){
alignment_str_ =unit_alignments::get_string(self_->alignment());
}
}
unit_alignments::type attack_type::alignment() const
{
// pick attack alignment or fall back to unit alignment
return (unit_alignments::get_enum(alignment_str_).value_or(self_ ? self_->alignment() : unit_alignments::type::neutral));
}
std::string attack_type::accuracy_parry_description() const
@ -170,6 +183,7 @@ bool matches_simple_filter(const attack_type& attack, const config& filter, cons
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::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::vector<std::string> filter_special = utils::split(filter["special"]);
@ -207,6 +221,9 @@ bool matches_simple_filter(const attack_type& attack, const config& filter, cons
if (!filter_attacks_used.empty() && !in_ranges(attack.attacks_used(), utils::parse_ranges_unsigned(filter_attacks_used)))
return false;
if(!filter_alignment.empty() && filter_alignment.count(attack.alignment_str()) == 0)
return false;
if ( !filter_name.empty() && filter_name.count(attack.id()) == 0)
return false;
@ -367,6 +384,7 @@ bool attack_type::apply_modification(const config& cfg)
const t_string& set_desc = cfg["set_description"];
const std::string& set_type = cfg["set_type"];
const std::string& set_range = cfg["set_range"];
const std::string& set_attack_alignment = cfg["set_alignment"];
const std::string& set_icon = cfg["set_icon"];
const std::string& del_specials = cfg["remove_specials"];
auto set_specials = cfg.optional_child("set_specials");
@ -407,6 +425,10 @@ bool attack_type::apply_modification(const config& cfg)
range_ = set_range;
}
if(set_attack_alignment.empty() == false) {
alignment_str_ = set_attack_alignment;
}
if(set_icon.empty() == false) {
icon_ = set_icon;
}
@ -748,6 +770,7 @@ void attack_type::write(config& cfg) const
cfg["range"] = range_;
cfg["min_range"] = min_range_;
cfg["max_range"] = max_range_;
cfg["alignment"] = alignment_str_;
cfg["damage"] = damage_;
cfg["number"] = num_attacks_;
cfg["attack_weight"] = attack_weight_;

View file

@ -25,6 +25,7 @@
#include <boost/dynamic_bitset_fwd.hpp>
#include "units/ptr.hpp" // for attack_ptr
#include "units/unit_alignments.hpp"
class unit_ability_list;
class unit_type;
@ -61,6 +62,7 @@ public:
void set_range(const std::string& value) { range_ = value; set_changed(true); }
void set_min_range(int value) { min_range_ = value; set_changed(true); }
void set_max_range(int value) { max_range_ = value; set_changed(true); }
void set_attack_alignment(const std::string& value) { alignment_str_ = value; set_changed(true); }
void set_accuracy(int value) { accuracy_ = value; set_changed(true); }
void set_parry(int value) { parry_ = value; set_changed(true); }
void set_damage(int value) { damage_ = value; set_changed(true); }
@ -85,6 +87,13 @@ public:
std::string weapon_specials() const;
std::string weapon_specials_value(const std::set<std::string> checking_tags) const;
/** Returns alignment specified by alignment_str_ variable If empty or not valid returns the unit's alignment or neutral if self_ variable empty.
*/
unit_alignments::type alignment() const;
/** Returns alignment specified by alignment() for filtering.
*/
std::string alignment_str() const {return unit_alignments::get_string(alignment());}
/** Calculates the number of attacks this weapon has, considering specials. */
void modified_attacks(unsigned & min_attacks,
unsigned & max_attacks) const;
@ -406,6 +415,7 @@ private:
std::string icon_;
std::string range_;
int min_range_, max_range_;
std::string alignment_str_;
int damage_;
int num_attacks_;
double attack_weight_;

View file

@ -8,6 +8,7 @@
---@field type string
---@field icon string
---@field range integer
---@field alignment string
---@field number integer
---@field movement_used integer
---@field attacks_used integer

View file

@ -365,6 +365,12 @@
0 replace_special_with_filter_in_attack_event_active
0 replace_special_with_filter_in_attack_event_inactive
0 swarm_disables_upgrades
0 attack_alignment_test
0 attack_alignment_test_with_illuminates
0 event_test_filter_attack_attack_alignment
0 event_test_filter_attack_attack_alignment_no_match
0 event_test_filter_attack_formula_attack_alignment
0 event_test_filter_attack_formula_attack_alignment_no_match
0 poison_opponent
0 unslowable_status_test
0 unpetrifiable_status_test