Implement certain weapon specials in the engine

Support ['specials'] in abilities except plagues, heal_on_hit and swarm, which emulate the weapon specials with [affect_adjacent] in option and the possibility to affect all weapons filtered (used with 'Initiative' in HttH or 'Formation' in UtBS). (Don't put filter_weapon in 'filter_self/opponent' like in  true weapons specials.)

Closes #3630
Closes #3620
This commit is contained in:
newfrenchy83 2018-10-18 10:34:53 +02:00 committed by Gregory A Lundberg
parent 1f9a614dbc
commit 7f7217cf15
11 changed files with 237 additions and 276 deletions

View file

@ -28,6 +28,10 @@
* Support [filter_second_weapon] in leadership and resistance abilities,
which activates the ability only when the affected unit's opponent
is using a matching weapon.
* Support ['specials'] in abilities except plagues, heal_on_hit and swarm , which emulate the weapon
specials with [affect_adjacent] in option and the possibility to affect all weapons filtered(used with
'Initiative' in httt or 'Formation' in utbs). (don't put filter_weapon in 'filter_self/opponent' like
in true weapons specials).
* Support WFL and percentages in [random_placement]num_items=
* Support [or] in [filter_wml] and similar places
* Support globbing in [filter_wml] via glob_on_* keys

View file

@ -95,7 +95,6 @@
{SOUND:HIT_AND_MISS {SOUND_LIST:SWORD_SWISH} {SOUND_LIST:MISS} -100}
[/attack_anim]
{WEAPON_SPECIAL_INITIATIVE_EVENTS}
[variation]
variation_id=sceptre
hide_help=yes

View file

@ -55,7 +55,6 @@
{SOUND:HIT_AND_MISS {SOUND_LIST:SWORD_SWISH} {SOUND_LIST:MISS} -75}
[/attack_anim]
{WEAPON_SPECIAL_INITIATIVE_EVENTS}
[variation]
variation_id=sceptre
hide_help=yes

View file

@ -1,139 +1,18 @@
#textdomain wesnoth-httt
# NOTE: This code is tailored for Li'sar in this campaign specifically, and will
# not work correctly elsewhere without modification.
#define ABILITY_INITIATIVE
[dummy]
[firststrike]
id=initiative
name= _ "initiative"
description= _ "All adjacent friendly units will strike first in melee combat, even when defending."
[/dummy]
affect_self=no
[filter_second_weapon]
range=melee
[/filter_second_weapon]
[affect_adjacent]
[/affect_adjacent]
[/firststrike]
#enddef
#define SPECIAL_NOTES_INITIATIVE
_" This unit's grasp of melee tactics allows adjacent allies to strike the first blow even when defending."#enddef
#define INITIATIVE_OBJECT FILTER
[object]
silent=yes
[filter]
{FILTER}
[/filter]
[effect]
apply_to=attack
range=melee
[set_specials]
mode=append
[firststrike]
id=initiative
name=""
description=""
[filter_self]
[filter_adjacent]
id="Li'sar"
[/filter_adjacent]
[/filter_self]
[/firststrike]
[/set_specials]
[/effect]
[/object]
#enddef
#define LACKS_INITIATIVE_FILTER
side=1,2
[filter_side]
[allied_with]
[has_unit]
id="Li'sar"
[/has_unit]
[/allied_with]
[/filter_side]
[filter_wml]
[attack]
range=melee
[/attack]
[/filter_wml]
[not]
[filter_wml]
[attack]
range=melee
[specials]
[firststrike]
[/firststrike]
[/specials]
[/attack]
[/filter_wml]
[/not]
#enddef
#define WEAPON_SPECIAL_INITIATIVE_EVENTS
[event]
id=initiative_1
name=prerecruit,prerecall
first_time_only=no
[filter]
{LACKS_INITIATIVE_FILTER}
[/filter]
[allow_undo]
[/allow_undo]
[on_undo]
[modify_unit]
[filter]
id=$unit.id
[/filter]
[effect]
apply_to=attack
range=melee
remove_specials=initiative
[/effect]
[/modify_unit]
[/on_undo]
{INITIATIVE_OBJECT x,y=$x1,$y1}
[/event]
[event]
id=initiative_2
name=side 1 turn, side 2 turn
first_time_only=no
[store_unit]
[filter]
{LACKS_INITIATIVE_FILTER}
[/filter]
kill=no
variable=units_lacking_initiative
[/store_unit]
[foreach]
array=units_lacking_initiative
[do]
{INITIATIVE_OBJECT x,y=$this_item.x,$this_item.y}
[/do]
[/foreach]
{CLEAR_VARIABLE units_lacking_initiative}
[/event]
[event]
id=initiative_3
name=attack
first_time_only=no
[filter_second]
{LACKS_INITIATIVE_FILTER}
[/filter_second]
{INITIATIVE_OBJECT x,y=$x2,$y2}
[/event]
#enddef

View file

@ -170,7 +170,6 @@
# Add global events
[+campaign]
{WEAPON_SPECIAL_DAZE_EVENTS}
{ABILITY_FORMATION_EVENTS}
{ABILITY_DISENGAGE_EVENTS}
{ABILITY_SUPPORT_EVENTS}
# On recruit, this switches the higher-recruit-cost units to the normal

View file

@ -94,13 +94,43 @@ _" This unit is capable of healing those around it, slowing dehydration, and cur
[/attacks]
#enddef
#define FORMATION ID GREATER_THAN COUNT
[chance_to_hit]
id={ID}
apply_to=opponent
sub=10
[filter_base_value]
greater_than_equal_to={GREATER_THAN}
[/filter_base_value]
[filter_self]
[filter_adjacent]
is_enemy=no
count={COUNT}
ability=formation
[/filter_adjacent]
[/filter_self]
[/chance_to_hit]
#enddef
#define ABILITY_FORMATION
[dummy]
id=formation
name= _ "formation"
female_name= _ "female^formation"
description= _ "This unit gains a +10% bonus to defense when another unit with the same ability is adjacent to it. However, this cannot raise the units defense above 70%."
[filter]
[filter_adjacent]
ability=formation
is_enemy=no
count=1-5
[/filter_adjacent]
[/filter]
[/dummy]
{FORMATION formation1 40 1-5}
{FORMATION formation2 50 2-5}
{FORMATION formation3 60 3-5}
{FORMATION formation4 70 4-5}
{FORMATION formation5 80 5}
#enddef
#define ABILITY_DISENGAGE
@ -531,139 +561,6 @@ _"This unit is capable of distracting opponents, allowing allied units to trespa
[/event]
#enddef
#define ABILITY_FORMATION_EVENTS
[event]
id=ability_formation_event
name=unit placed
first_time_only=no
[filter]
[not]
side=1
[/not]
[not]
[filter_wml]
[attack]
[specials]
[chance_to_hit]
id=formation_enemy_1
[/chance_to_hit]
[/specials]
[/attack]
[/filter_wml]
[/not]
[/filter]
[object]
silent=yes
[filter]
id=$unit.id
[/filter]
[effect]
apply_to=attack
[set_specials]
mode=append
[chance_to_hit]
id=formation_enemy_1
name=""
description=""
sub=10
[filter_base_value]
greater_than_equal_to=40
[/filter_base_value]
[filter_opponent]
ability=formation
[filter_adjacent]
ability=formation
is_enemy=no
count=1-5
[/filter_adjacent]
[/filter_opponent]
[/chance_to_hit]
[chance_to_hit]
id=formation_enemy_2
name=""
description=""
sub=10
[filter_base_value]
greater_than_equal_to=50
[/filter_base_value]
[filter_opponent]
ability=formation
[filter_adjacent]
ability=formation
is_enemy=no
count=2-5
[/filter_adjacent]
[/filter_opponent]
[/chance_to_hit]
[chance_to_hit]
id=formation_enemy_3
name=""
description=""
sub=10
[filter_base_value]
greater_than_equal_to=60
[/filter_base_value]
[filter_opponent]
ability=formation
[filter_adjacent]
ability=formation
is_enemy=no
count=3-5
[/filter_adjacent]
[/filter_opponent]
[/chance_to_hit]
[chance_to_hit]
id=formation_enemy_4
name=""
description=""
sub=10
[filter_base_value]
greater_than_equal_to=70
[/filter_base_value]
[filter_opponent]
ability=formation
[filter_adjacent]
ability=formation
is_enemy=no
count=4-5
[/filter_adjacent]
[/filter_opponent]
[/chance_to_hit]
[chance_to_hit]
id=formation_enemy_5
name=""
description=""
sub=10
[filter_base_value]
greater_than_equal_to=80
[/filter_base_value]
[filter_opponent]
ability=formation
[filter_adjacent]
ability=formation
is_enemy=no
count=5
[/filter_adjacent]
[/filter_opponent]
[/chance_to_hit]
[/set_specials]
[/effect]
[/object]
[/event]
#enddef
#define ABILITY_SUPPORT_EVENTS
[event]
name=unit placed

View file

@ -43,6 +43,7 @@
#include "units/abilities.hpp"
#include "units/animation_component.hpp"
#include "units/helper.hpp"
#include "units/filter.hpp"
#include "units/map.hpp"
#include "units/udisplay.hpp"
#include "units/unit.hpp"
@ -135,13 +136,16 @@ battle_context_unit_stats::battle_context_unit_stats(const unit& u,
opp_ctx.emplace(opp_weapon->specials_context(&opp, &u, opp_loc, u_loc, !attacking, weapon));
}
slows = weapon->get_special_bool("slow");
drains = !opp.get_state("undrainable") && weapon->get_special_bool("drains");
petrifies = weapon->get_special_bool("petrifies");
poisons = !opp.get_state("unpoisonable") && weapon->get_special_bool("poison") && !opp.get_state(unit::STATE_POISONED);
slows = weapon->bool_ability("slow");
drains = !opp.get_state("undrainable") && weapon->bool_ability("drains");
petrifies = weapon->bool_ability("petrifies");
poisons = !opp.get_state("unpoisonable") && weapon->bool_ability("poison") && !opp.get_state(unit::STATE_POISONED);
backstab_pos = is_attacker && backstab_check(u_loc, opp_loc, units, resources::gameboard->teams());
rounds = weapon->get_specials("berserk").highest("value", 1).first;
firststrike = weapon->get_special_bool("firststrike");
if(weapon->combat_ability("berserk", 1).second) {
rounds = weapon->combat_ability("berserk", 1).first;
}
firststrike = weapon->bool_ability("firststrike");
{
const int distance = distance_between(u_loc, opp_loc);
@ -172,6 +176,10 @@ battle_context_unit_stats::battle_context_unit_stats(const unit& u,
unit_abilities::effect cth_effects(cth_specials, cth, backstab_pos);
cth = cth_effects.get_composite_value();
cth = utils::clamp(cth, 0, 100);
cth = weapon->combat_ability("chance_to_hit", cth, backstab_pos).first;
if(opp.get_state("invulnerable")) {
cth = 0;
}
@ -207,11 +215,15 @@ battle_context_unit_stats::battle_context_unit_stats(const unit& u,
// Compute drain amounts only if draining is possible.
if(drains) {
unit_ability_list drain_specials = weapon->get_specials("drains");
// Compute the drain percent (with 50% as the base for backward compatibility)
unit_abilities::effect drain_percent_effects(drain_specials, 50, backstab_pos);
drain_percent = drain_percent_effects.get_composite_value();
if (weapon->get_special_bool("drains")) {
unit_ability_list drain_specials = weapon->get_specials("drains");
// Compute the drain percent (with 50% as the base for backward compatibility)
unit_abilities::effect drain_percent_effects(drain_specials, 50, backstab_pos);
drain_percent = drain_percent_effects.get_composite_value();
}
if (weapon->combat_ability("drains", 50, backstab_pos).second) {
drain_percent = weapon->combat_ability("drains", 50, backstab_pos).first;
}
}
// Add heal_on_hit (the drain constant)
@ -1563,6 +1575,159 @@ std::pair<int, map_location> under_leadership(const unit_map& units, const map_l
return abil.highest("value");
}
//begin of weapon emulates function.
bool unit::abilities_filter_matches(const config& cfg, bool attacker, int res) const
{
if(!(cfg["active_on"].empty() || (attacker && cfg["active_on"] == "offense") || (!attacker && cfg["active_on"] == "defense"))) {
return false;
}
if(!unit_abilities::filter_base_matches(cfg, res)) {
return false;
}
return true;
}
//functions for emulate weapon specials.
//filter opponent and affect self/opponent/both option.
bool unit::ability_filter_opponent(const std::string& ability,const config& cfg,const map_location& loc) const
{
const config &filter = cfg.child("filter_opponent");
if(!filter) {
return true;
}
return unit_filter(vconfig(filter)).set_use_flat_tod(ability == "illuminates").matches(*this, loc);
}
bool leadership_affects_self(const std::string& ability,const unit_map& units, const map_location& loc, const_attack_ptr weapon,const_attack_ptr opp_weapon)
{
const unit_map::const_iterator un = units.find(loc);
if(un == units.end()) {
return false;
}
unit_ability_list abil = un->get_abilities(ability, weapon, opp_weapon);
for(unit_ability_list::iterator i = abil.begin(); i != abil.end();) {
const std::string& apply_to = (*i->first)["apply_to"];
if(apply_to.empty()||apply_to == "both"||apply_to == "under") {
return true;
}
++i;
}
return false;
}
bool leadership_affects_opponent(const std::string& ability,const unit_map& units, const map_location& loc, const_attack_ptr weapon,const_attack_ptr opp_weapon)
{
const unit_map::const_iterator un = units.find(loc);
if(un == units.end()) {
return false;
}
unit_ability_list abil = un->get_abilities(ability, weapon, opp_weapon);
for(unit_ability_list::iterator i = abil.begin(); i != abil.end();) {
const std::string& apply_to = (*i->first)["apply_to"];
if(apply_to == "both"||apply_to == "opponent") {
return true;
}
++i;
}
return false;
}
//sub function for emulate chance_to_hit,damage drains and attacks special.
std::pair<int, bool> ability_leadership(const std::string& ability,const unit_map& units, const map_location& loc, const map_location& opp_loc, bool attacker, int abil_value, bool backstab_pos, const_attack_ptr weapon, const_attack_ptr opp_weapon)
{
const unit_map::const_iterator un = units.find(loc);
const unit_map::const_iterator up = units.find(opp_loc);
if(un == units.end()) {
return {abil_value, false};
}
unit_ability_list abil = un->get_abilities(ability, weapon, opp_weapon);
for(unit_ability_list::iterator i = abil.begin(); i != abil.end();) {
const config &filter = (*i->first).child("filter_opponent");
bool show_result = false;
if(up == units.end() && !filter) {
show_result = un->abilities_filter_matches(*i->first, attacker, abil_value);
} else if(up == units.end() && filter) {
return {abil_value, false};
} else {
show_result = !(!un->abilities_filter_matches(*i->first, attacker, abil_value) || !up->ability_filter_opponent(ability, *i->first, opp_loc));
}
if(!show_result) {
i = abil.erase(i);
} else {
++i;
}
}
if(!abil.empty()) {
unit_abilities::effect leader_effect(abil, abil_value, backstab_pos);
return {leader_effect.get_composite_value(), true};
}
return {abil_value, false};
}
//sub function for wmulate boolean special(slow, poison...)
bool bool_leadership(const std::string& ability,const unit_map& units, const map_location& loc, const map_location& opp_loc, bool attacker, const_attack_ptr weapon, const_attack_ptr opp_weapon)
{
const unit_map::const_iterator un = units.find(loc);
const unit_map::const_iterator up = units.find(opp_loc);
if(un == units.end() || up == units.end()) {
return false;
}
unit_ability_list abil = un->get_abilities(ability, weapon, opp_weapon);
for(unit_ability_list::iterator i = abil.begin(); i != abil.end();) {
const std::string& active_on = (*i->first)["active_on"];
if(!(active_on.empty() || (attacker && active_on == "offense") || (!attacker && active_on == "defense")) || !up->ability_filter_opponent(ability, *i->first, opp_loc)) {
i = abil.erase(i);
} else {
++i;
}
}
if(!abil.empty()) {
return true;
}
return false;
}
//emulate boolean special for self/adjacent and/or opponent.
bool attack_type::bool_ability(const std::string& ability) const
{
bool abil_bool= get_special_bool(ability);
const unit_map& units = display::get_singleton()->get_units();
if(leadership_affects_self(ability, units, self_loc_, shared_from_this(), other_attack_)) {
abil_bool = bool_leadership(ability, units, self_loc_, other_loc_, is_attacker_, shared_from_this(), other_attack_);
}
if(leadership_affects_opponent(ability, units, other_loc_, other_attack_, shared_from_this())) {
abil_bool = bool_leadership(ability, units, other_loc_, self_loc_, !is_attacker_, other_attack_, shared_from_this());
}
return abil_bool;
}
//emulate numerical special for self/adjacent and/or opponent.
std::pair<int, bool> attack_type::combat_ability(const std::string& ability, int abil_value, bool backstab_pos) const
{
const unit_map& units = display::get_singleton()->get_units();
if(leadership_affects_self(ability, units, self_loc_, shared_from_this(), other_attack_)) {
return ability_leadership(ability, units, self_loc_, other_loc_, is_attacker_, abil_value, backstab_pos, shared_from_this(), other_attack_);
}
if(leadership_affects_opponent(ability, units, other_loc_, other_attack_, shared_from_this())) {
return ability_leadership(ability, units, other_loc_,self_loc_, !is_attacker_, abil_value, backstab_pos, other_attack_, shared_from_this());
}
return {abil_value, false};
}
//end of emulate weapon special functions.
int combat_modifier(const unit_map& units,
const gamemap& map,
const map_location& loc,

View file

@ -279,6 +279,10 @@ void attack_unit_and_advance(const map_location& attacker,
* or 0 and map_location::null_location() otherwise.
*/
std::pair<int, map_location> under_leadership(const unit_map& units, const map_location& loc, const_attack_ptr weapon, const_attack_ptr opp_weapon = nullptr);
bool leadership_affects_self(const std::string& ability,const unit_map& units, const map_location& loc, const_attack_ptr weapon=nullptr,const_attack_ptr opp_weapon=nullptr);
bool leadership_affects_opponent(const std::string& ability,const unit_map& units, const map_location& loc, const_attack_ptr weapon=nullptr,const_attack_ptr opp_weapon=nullptr);
std::pair<int, bool> ability_leadership(const std::string& ability, const unit_map& units, const map_location& loc, const map_location& opp_loc, bool attacker=true, int abil_value=0, bool backstab_pos=false, const_attack_ptr weapon=nullptr, const_attack_ptr opp_weapon=nullptr);
bool bool_leadership(const std::string& ability,const unit_map& units, const map_location& loc, const map_location& opp_loc, bool attacker=true, const_attack_ptr weapon=nullptr, const_attack_ptr opp_weapon=nullptr);
/**
* Returns the amount that a unit's damage should be multiplied by

View file

@ -824,6 +824,10 @@ void attack_type::modified_attacks(bool is_backstab, unsigned & min_attacks,
unit_abilities::effect attacks_effect(get_specials("attacks"),
num_attacks(), is_backstab);
int attacks_value = attacks_effect.get_composite_value();
if ( combat_ability("attacks", attacks_value, is_backstab).second ) {
attacks_value = combat_ability("attacks", attacks_value, is_backstab).first;
}
if ( attacks_value < 0 ) {
attacks_value = num_attacks();
ERR_NG << "negative number of strikes after applying weapon specials" << std::endl;
@ -846,7 +850,11 @@ void attack_type::modified_attacks(bool is_backstab, unsigned & min_attacks,
int attack_type::modified_damage(bool is_backstab) const
{
unit_abilities::effect dmg_effect(get_specials("damage"), damage(), is_backstab);
return dmg_effect.get_composite_value();
int damage_value = dmg_effect.get_composite_value();
if ( combat_ability("damage", damage_value, is_backstab).second ) {
damage_value = combat_ability("damage", damage_value, is_backstab).first;
}
return damage_value;
}

View file

@ -66,6 +66,10 @@ public:
void set_defense_weight(double value) { defense_weight_ = value; }
void set_specials(config value) { specials_ = value; }
// In action/attack.cpp
std::pair<int, bool> combat_ability(const std::string& ability, int abil_value = 0, bool backstab_pos = false) const;
bool bool_ability(const std::string& ability) const;
// In unit_abilities.cpp:

View file

@ -1537,6 +1537,9 @@ public:
*/
void remove_ability_by_id(const std::string& ability);
bool abilities_filter_matches(const config& cfg, bool attacker, int res) const;
bool ability_filter_opponent(const std::string& ability,const config& cfg,const map_location& loc) const;
private:
/**
* Check if an ability is active.