wesnoth/src/actions/attack.cpp
Charles Dang 45f871067f Use std::size_t everywhere instead of plain size_t
Excludes:
* spirit_po/
* xBRZ/

(cherry-picked from commit fc2a58f693)
2018-10-07 03:17:59 +00:00

1690 lines
50 KiB
C++

/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
/**
* @file
* Fighting.
*/
#include "actions/attack.hpp"
#include "actions/advancement.hpp"
#include "actions/vision.hpp"
#include "ai/lua/aspect_advancements.hpp"
#include "game_config.hpp"
#include "game_data.hpp"
#include "game_events/pump.hpp"
#include "gettext.hpp"
#include "log.hpp"
#include "map/map.hpp"
#include "mouse_handler_base.hpp"
#include "play_controller.hpp"
#include "preferences/game.hpp"
#include "random.hpp"
#include "replay.hpp"
#include "resources.hpp"
#include "statistics.hpp"
#include "synced_checkup.hpp"
#include "synced_user_choice.hpp"
#include "team.hpp"
#include "tod_manager.hpp"
#include "units/abilities.hpp"
#include "units/animation_component.hpp"
#include "units/helper.hpp"
#include "units/map.hpp"
#include "units/udisplay.hpp"
#include "units/unit.hpp"
#include "whiteboard/manager.hpp"
#include "wml_exception.hpp"
static lg::log_domain log_engine("engine");
#define DBG_NG LOG_STREAM(debug, log_engine)
#define LOG_NG LOG_STREAM(info, log_engine)
#define WRN_NG LOG_STREAM(err, log_engine)
#define ERR_NG LOG_STREAM(err, log_engine)
static lg::log_domain log_config("config");
#define LOG_CF LOG_STREAM(info, log_config)
// ==================================================================================
// BATTLE CONTEXT UNIT STATS
// ==================================================================================
battle_context_unit_stats::battle_context_unit_stats(const unit& u,
const map_location& u_loc,
int u_attack_num,
bool attacking,
const unit& opp,
const map_location& opp_loc,
const_attack_ptr opp_weapon,
const unit_map& units)
: weapon(nullptr)
, attack_num(u_attack_num)
, is_attacker(attacking)
, is_poisoned(u.get_state(unit::STATE_POISONED))
, is_slowed(u.get_state(unit::STATE_SLOWED))
, slows(false)
, drains(false)
, petrifies(false)
, plagues(false)
, poisons(false)
, backstab_pos(false)
, swarm(false)
, firststrike(false)
, disable(false)
, experience(u.experience())
, max_experience(u.max_experience())
, level(u.level())
, rounds(1)
, hp(0)
, max_hp(u.max_hitpoints())
, chance_to_hit(0)
, damage(0)
, slow_damage(0)
, drain_percent(0)
, drain_constant(0)
, num_blows(0)
, swarm_min(0)
, swarm_max(0)
, plague_type()
{
// Get the current state of the unit.
if(attack_num >= 0) {
weapon = u.attacks()[attack_num].shared_from_this();
}
if(u.hitpoints() < 0) {
LOG_CF << "Unit with " << u.hitpoints() << " hitpoints found, set to 0 for damage calculations\n";
hp = 0;
} else if(u.hitpoints() > u.max_hitpoints()) {
// If a unit has more hp than its maximum, the engine will fail with an
// assertion failure due to accessing the prob_matrix out of bounds.
hp = u.max_hitpoints();
} else {
hp = u.hitpoints();
}
// Exit if no weapon.
if(!weapon) {
return;
}
// Get the weapon characteristics as appropriate.
auto ctx = weapon->specials_context(&u, &opp, u_loc, opp_loc, attacking, opp_weapon);
boost::optional<decltype(ctx)> opp_ctx;
if(opp_weapon) {
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);
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");
{
const int distance = distance_between(u_loc, opp_loc);
const bool out_of_range = distance > weapon->max_range() || distance < weapon->min_range();
disable = weapon->get_special_bool("disable") || out_of_range;
}
// Handle plague.
unit_ability_list plague_specials = weapon->get_specials("plague");
plagues = !opp.get_state("unplagueable") && !plague_specials.empty() &&
opp.undead_variation() != "null" && !resources::gameboard->map().is_village(opp_loc);
if(plagues) {
plague_type = (*plague_specials.front().first)["type"].str();
if(plague_type.empty()) {
plague_type = u.type().base_id();
}
}
// Compute chance to hit.
chance_to_hit = opp.defense_modifier(resources::gameboard->map().get_terrain(opp_loc)) + weapon->accuracy()
- (opp_weapon ? opp_weapon->parry() : 0);
if(chance_to_hit > 100) {
chance_to_hit = 100;
}
unit_ability_list cth_specials = weapon->get_specials("chance_to_hit");
unit_abilities::effect cth_effects(cth_specials, chance_to_hit, backstab_pos);
chance_to_hit = cth_effects.get_composite_value();
if(opp.get_state("invulnerable")) {
chance_to_hit = 0;
}
// Compute base damage done with the weapon.
int base_damage = weapon->modified_damage(backstab_pos);
// Get the damage multiplier applied to the base damage of the weapon.
int damage_multiplier = 100;
// Time of day bonus.
damage_multiplier += combat_modifier(
resources::gameboard->units(), resources::gameboard->map(), u_loc, u.alignment(), u.is_fearless());
// Leadership bonus.
int leader_bonus = under_leadership(units, u_loc).first;
if(leader_bonus != 0) {
damage_multiplier += leader_bonus;
}
// Resistance modifier.
damage_multiplier *= opp.damage_from(*weapon, !attacking, opp_loc);
// Compute both the normal and slowed damage.
damage = round_damage(base_damage, damage_multiplier, 10000);
slow_damage = round_damage(base_damage, damage_multiplier, 20000);
if(is_slowed) {
damage = slow_damage;
}
// 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();
}
// Add heal_on_hit (the drain constant)
unit_ability_list heal_on_hit_specials = weapon->get_specials("heal_on_hit");
unit_abilities::effect heal_on_hit_effects(heal_on_hit_specials, 0, backstab_pos);
drain_constant += heal_on_hit_effects.get_composite_value();
drains = drain_constant || drain_percent;
// Compute the number of blows and handle swarm.
weapon->modified_attacks(backstab_pos, swarm_min, swarm_max);
swarm = swarm_min != swarm_max;
num_blows = calc_blows(hp);
}
battle_context_unit_stats::battle_context_unit_stats(const unit_type* u_type,
const_attack_ptr att_weapon,
bool attacking,
const unit_type* opp_type,
const_attack_ptr opp_weapon,
unsigned int opp_terrain_defense,
int lawful_bonus)
: weapon(att_weapon)
, attack_num(-2) // This is and stays invalid. Always use weapon when using this constructor.
, is_attacker(attacking)
, is_poisoned(false)
, is_slowed(false)
, slows(false)
, drains(false)
, petrifies(false)
, plagues(false)
, poisons(false)
, backstab_pos(false)
, swarm(false)
, firststrike(false)
, disable(false)
, experience(0)
, max_experience(0)
, level(0)
, rounds(1)
, hp(0)
, max_hp(0)
, chance_to_hit(0)
, damage(0)
, slow_damage(0)
, drain_percent(0)
, drain_constant(0)
, num_blows(0)
, swarm_min(0)
, swarm_max(0)
, plague_type()
{
if(!u_type || !opp_type) {
return;
}
// Get the current state of the unit.
if(u_type->hitpoints() < 0) {
hp = 0;
} else {
hp = u_type->hitpoints();
}
max_experience = u_type->experience_needed();
level = (u_type->level());
max_hp = (u_type->hitpoints());
// Exit if no weapon.
if(!weapon) {
return;
}
// Get the weapon characteristics as appropriate.
auto ctx = weapon->specials_context(*u_type, map_location::null_location(), attacking);
boost::optional<decltype(ctx)> opp_ctx;
if(opp_weapon) {
opp_ctx.emplace(opp_weapon->specials_context(*opp_type, map_location::null_location(), !attacking));
}
slows = weapon->get_special_bool("slow");
drains = !opp_type->musthave_status("undrainable") && weapon->get_special_bool("drains");
petrifies = weapon->get_special_bool("petrifies");
poisons = !opp_type->musthave_status("unpoisonable") && weapon->get_special_bool("poison");
rounds = weapon->get_specials("berserk").highest("value", 1).first;
firststrike = weapon->get_special_bool("firststrike");
disable = weapon->get_special_bool("disable");
unit_ability_list plague_specials = weapon->get_specials("plague");
plagues = !opp_type->musthave_status("unplagueable") && !plague_specials.empty() &&
opp_type->undead_variation() != "null";
if(plagues) {
plague_type = (*plague_specials.front().first)["type"].str();
if(plague_type.empty()) {
plague_type = u_type->base_id();
}
}
signed int cth = 100 - opp_terrain_defense + weapon->accuracy() - (opp_weapon ? opp_weapon->parry() : 0);
cth = std::min(100, cth);
cth = std::max(0, cth);
chance_to_hit = cth;
unit_ability_list cth_specials = weapon->get_specials("chance_to_hit");
unit_abilities::effect cth_effects(cth_specials, chance_to_hit, backstab_pos);
chance_to_hit = cth_effects.get_composite_value();
int base_damage = weapon->modified_damage(backstab_pos);
int damage_multiplier = 100;
damage_multiplier
+= generic_combat_modifier(lawful_bonus, u_type->alignment(), u_type->musthave_status("fearless"));
damage_multiplier *= opp_type->resistance_against(weapon->type(), !attacking);
damage = round_damage(base_damage, damage_multiplier, 10000);
slow_damage = round_damage(base_damage, damage_multiplier, 20000);
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();
}
// Add heal_on_hit (the drain constant)
unit_ability_list heal_on_hit_specials = weapon->get_specials("heal_on_hit");
unit_abilities::effect heal_on_hit_effects(heal_on_hit_specials, 0, backstab_pos);
drain_constant += heal_on_hit_effects.get_composite_value();
drains = drain_constant || drain_percent;
// Compute the number of blows and handle swarm.
weapon->modified_attacks(backstab_pos, swarm_min, swarm_max);
swarm = swarm_min != swarm_max;
num_blows = calc_blows(hp);
}
// ==================================================================================
// BATTLE CONTEXT
// ==================================================================================
battle_context::battle_context(const unit_map& units,
const map_location& attacker_loc,
const map_location& defender_loc,
int attacker_weapon,
int defender_weapon,
double aggression,
const combatant* prev_def,
const unit* attacker_ptr)
: attacker_stats_(nullptr)
, defender_stats_(nullptr)
, attacker_combatant_(nullptr)
, defender_combatant_(nullptr)
{
const unit& attacker = attacker_ptr ? *attacker_ptr : *units.find(attacker_loc);
const unit& defender = *units.find(defender_loc);
const double harm_weight = 1.0 - aggression;
if(attacker_weapon == -1 &&
attacker.attacks().size() == 1 &&
attacker.attacks()[0].attack_weight() > 0 &&
!attacker.attacks()[0].get_special_bool("disable", true)
) {
attacker_weapon = 0;
}
if(attacker_weapon == -1) {
attacker_weapon = choose_attacker_weapon(
attacker, defender, units, attacker_loc, defender_loc, harm_weight, &defender_weapon, prev_def
);
} else if(defender_weapon == -1) {
defender_weapon = choose_defender_weapon(
attacker, defender, attacker_weapon, units, attacker_loc, defender_loc, prev_def
);
}
// If those didn't have to generate statistics, do so now.
if(!attacker_stats_) {
const_attack_ptr adef = nullptr;
const_attack_ptr ddef = nullptr;
if(attacker_weapon >= 0) {
VALIDATE(attacker_weapon < static_cast<int>(attacker.attacks().size()),
_("An invalid attacker weapon got selected."));
adef = attacker.attacks()[attacker_weapon].shared_from_this();
}
if(defender_weapon >= 0) {
VALIDATE(defender_weapon < static_cast<int>(defender.attacks().size()),
_("An invalid defender weapon got selected."));
ddef = defender.attacks()[defender_weapon].shared_from_this();
}
assert(!defender_stats_ && !attacker_combatant_ && !defender_combatant_);
attacker_stats_.reset(new battle_context_unit_stats(
attacker, attacker_loc, attacker_weapon, true, defender, defender_loc, ddef, units));
defender_stats_.reset(new battle_context_unit_stats(
defender, defender_loc, defender_weapon, false, attacker, attacker_loc, adef, units));
}
// There have been various bugs where only one of these was set
assert(attacker_stats_);
assert(defender_stats_);
}
battle_context::battle_context(const battle_context_unit_stats& att, const battle_context_unit_stats& def)
: attacker_stats_(new battle_context_unit_stats(att))
, defender_stats_(new battle_context_unit_stats(def))
, attacker_combatant_(nullptr)
, defender_combatant_(nullptr)
{
}
battle_context::battle_context(const battle_context& other)
: attacker_stats_(nullptr)
, defender_stats_(nullptr)
, attacker_combatant_(nullptr)
, defender_combatant_(nullptr)
{
*this = other;
}
battle_context& battle_context::operator=(const battle_context& other)
{
if(&other != this) {
attacker_stats_.reset(new battle_context_unit_stats(*other.attacker_stats_));
defender_stats_.reset(new battle_context_unit_stats(*other.defender_stats_));
attacker_combatant_.reset(other.attacker_combatant_
? new combatant(*other.attacker_combatant_, *attacker_stats_) : nullptr);
defender_combatant_.reset(other.defender_combatant_
? new combatant(*other.defender_combatant_, *defender_stats_) : nullptr);
}
return *this;
}
/** @todo FIXME: better to initialize combatant initially (move into
battle_context_unit_stats?), just do fight() when required. */
const combatant& battle_context::get_attacker_combatant(const combatant* prev_def)
{
// We calculate this lazily, since AI doesn't always need it.
if(!attacker_combatant_) {
assert(!defender_combatant_);
attacker_combatant_.reset(new combatant(*attacker_stats_));
defender_combatant_.reset(new combatant(*defender_stats_, prev_def));
attacker_combatant_->fight(*defender_combatant_);
}
return *attacker_combatant_;
}
const combatant& battle_context::get_defender_combatant(const combatant* prev_def)
{
// We calculate this lazily, since AI doesn't always need it.
if(!defender_combatant_) {
assert(!attacker_combatant_);
attacker_combatant_.reset(new combatant(*attacker_stats_));
defender_combatant_.reset(new combatant(*defender_stats_, prev_def));
attacker_combatant_->fight(*defender_combatant_);
}
return *defender_combatant_;
}
// Given this harm_weight, are we better than this other context?
bool battle_context::better_attack(class battle_context& that, double harm_weight)
{
return better_combat(
get_attacker_combatant(),
get_defender_combatant(),
that.get_attacker_combatant(),
that.get_defender_combatant(),
harm_weight
);
}
// Does combat A give us a better result than combat B?
bool battle_context::better_combat(const combatant& us_a,
const combatant& them_a,
const combatant& us_b,
const combatant& them_b,
double harm_weight)
{
double a, b;
// Compare: P(we kill them) - P(they kill us).
a = them_a.hp_dist[0] - us_a.hp_dist[0] * harm_weight;
b = them_b.hp_dist[0] - us_b.hp_dist[0] * harm_weight;
if(a - b < -0.01) {
return false;
}
if(a - b > 0.01) {
return true;
}
// Add poison to calculations
double poison_a_us = (us_a.poisoned) * game_config::poison_amount;
double poison_a_them = (them_a.poisoned) * game_config::poison_amount;
double poison_b_us = (us_b.poisoned) * game_config::poison_amount;
double poison_b_them = (them_b.poisoned) * game_config::poison_amount;
// Compare: damage to them - damage to us (average_hp replaces -damage)
a = (us_a.average_hp() - poison_a_us) * harm_weight - (them_a.average_hp() - poison_a_them);
b = (us_b.average_hp() - poison_b_us) * harm_weight - (them_b.average_hp() - poison_b_them);
if(a - b < -0.01) {
return false;
}
if(a - b > 0.01) {
return true;
}
// All else equal: go for most damage.
return them_a.average_hp() < them_b.average_hp();
}
int battle_context::choose_attacker_weapon(const unit& attacker,
const unit& defender,
const unit_map& units,
const map_location& attacker_loc,
const map_location& defender_loc,
double harm_weight,
int* defender_weapon,
const combatant* prev_def)
{
std::vector<unsigned int> choices;
// What options does attacker have?
unsigned int i;
for(i = 0; i < attacker.attacks().size(); ++i) {
const attack_type& att = attacker.attacks()[i];
if(att.attack_weight() > 0) {
choices.push_back(i);
}
}
if(choices.empty()) {
return -1;
}
if(choices.size() == 1) {
*defender_weapon
= choose_defender_weapon(attacker, defender, choices[0], units, attacker_loc, defender_loc, prev_def);
const_attack_ptr def_weapon
= *defender_weapon >= 0 ? defender.attacks()[*defender_weapon].shared_from_this() : nullptr;
attacker_stats_.reset(new battle_context_unit_stats(
attacker, attacker_loc, choices[0], true, defender, defender_loc, def_weapon, units));
if(attacker_stats_->disable) {
return -1;
}
const attack_type& att = attacker.attacks()[choices[0]];
defender_stats_.reset(new battle_context_unit_stats(
defender, defender_loc, *defender_weapon, false, attacker, attacker_loc, att.shared_from_this(), units));
return choices[0];
}
// Multiple options: simulate them, save best.
std::unique_ptr<battle_context_unit_stats> best_att_stats(nullptr);
std::unique_ptr<battle_context_unit_stats> best_def_stats(nullptr);
std::unique_ptr<combatant> best_att_comb(nullptr);
std::unique_ptr<combatant> best_def_comb(nullptr);
for(i = 0; i < choices.size(); ++i) {
const attack_type& att = attacker.attacks()[choices[i]];
int def_weapon =
choose_defender_weapon(attacker, defender, choices[i], units, attacker_loc, defender_loc, prev_def);
// If that didn't simulate, do so now.
if(!attacker_combatant_) {
const_attack_ptr def = nullptr;
if(def_weapon >= 0) {
def = defender.attacks()[def_weapon].shared_from_this();
}
attacker_stats_.reset(new battle_context_unit_stats(
attacker, attacker_loc, choices[i], true, defender, defender_loc, def, units));
if(attacker_stats_->disable) {
continue;
}
defender_stats_.reset(new battle_context_unit_stats(
defender, defender_loc, def_weapon, false, attacker, attacker_loc, att.shared_from_this(), units));
attacker_combatant_.reset(new combatant(*attacker_stats_));
defender_combatant_.reset(new combatant(*defender_stats_, prev_def));
attacker_combatant_->fight(*defender_combatant_);
} else {
if(attacker_stats_ != nullptr && attacker_stats_->disable) {
continue;
}
}
if(!best_att_comb ||
better_combat(*attacker_combatant_, *defender_combatant_, *best_att_comb, *best_def_comb, harm_weight)
) {
best_att_comb = std::move(attacker_combatant_);
best_def_comb = std::move(defender_combatant_);
best_att_stats = std::move(attacker_stats_);
best_def_stats = std::move(defender_stats_);
}
attacker_combatant_.reset();
defender_combatant_.reset();
attacker_stats_.reset();
defender_stats_.reset();
}
attacker_combatant_ = std::move(best_att_comb);
defender_combatant_ = std::move(best_def_comb);
attacker_stats_ = std::move(best_att_stats);
defender_stats_ = std::move(best_def_stats);
// These currently mean the same thing, but assumptions like that have been broken before
if(!defender_stats_ || !attacker_stats_) {
return -1;
}
*defender_weapon = defender_stats_->attack_num;
return attacker_stats_->attack_num;
}
/** @todo FIXME: Hand previous defender unit in here. */
int battle_context::choose_defender_weapon(const unit& attacker,
const unit& defender,
unsigned attacker_weapon,
const unit_map& units,
const map_location& attacker_loc,
const map_location& defender_loc,
const combatant* prev_def)
{
VALIDATE(attacker_weapon < attacker.attacks().size(), _("An invalid attacker weapon got selected."));
const attack_type& att = attacker.attacks()[attacker_weapon];
std::vector<unsigned int> choices;
// What options does defender have?
unsigned int i;
for(i = 0; i < defender.attacks().size(); ++i) {
const attack_type& def = defender.attacks()[i];
if(def.range() == att.range() && def.defense_weight() > 0) {
choices.push_back(i);
}
}
if(choices.empty()) {
return -1;
}
if(choices.size() == 1) {
const battle_context_unit_stats def_stats(
defender, defender_loc, choices[0], false, attacker, attacker_loc, att.shared_from_this(), units);
return (def_stats.disable) ? -1 : choices[0];
}
// Multiple options:
// First pass : get the best weight and the minimum simple rating for this weight.
// simple rating = number of blows * damage per blows (resistance taken in account) * cth * weight
// Eligible attacks for defense should have a simple rating greater or equal to this weight.
int min_rating = 0;
{
double max_weight = 0.0;
for(i = 0; i < choices.size(); ++i) {
const attack_type& def = defender.attacks()[choices[i]];
if(def.defense_weight() >= max_weight) {
const battle_context_unit_stats def_stats(defender, defender_loc, choices[i], false, attacker,
attacker_loc, att.shared_from_this(), units);
if(def_stats.disable) {
continue;
}
max_weight = def.defense_weight();
int rating = static_cast<int>(
def_stats.num_blows * def_stats.damage * def_stats.chance_to_hit * def.defense_weight());
if(def.defense_weight() > max_weight || rating < min_rating) {
min_rating = rating;
}
}
}
}
// Multiple options: simulate them, save best.
for(i = 0; i < choices.size(); ++i) {
const attack_type& def = defender.attacks()[choices[i]];
std::unique_ptr<battle_context_unit_stats> att_stats(new battle_context_unit_stats(
attacker, attacker_loc, attacker_weapon, true, defender, defender_loc, def.shared_from_this(), units));
std::unique_ptr<battle_context_unit_stats> def_stats(new battle_context_unit_stats(
defender, defender_loc, choices[i], false, attacker, attacker_loc, att.shared_from_this(), units));
if(def_stats->disable) {
continue;
}
std::unique_ptr<combatant> att_comb(new combatant(*att_stats));
std::unique_ptr<combatant> def_comb(new combatant(*def_stats, prev_def));
att_comb->fight(*def_comb);
int simple_rating = static_cast<int>(
def_stats->num_blows * def_stats->damage * def_stats->chance_to_hit * def.defense_weight());
if(simple_rating >= min_rating &&
(!attacker_combatant_ || better_combat(*def_comb, *att_comb, *defender_combatant_, *attacker_combatant_, 1.0))
) {
attacker_combatant_ = std::move(att_comb);
defender_combatant_ = std::move(def_comb);
attacker_stats_ = std::move(att_stats);
defender_stats_ = std::move(def_stats);
}
}
return defender_stats_ ? defender_stats_->attack_num : -1;
}
// ==================================================================================
// HELPERS
// ==================================================================================
namespace
{
void refresh_weapon_index(int& weap_index, const std::string& weap_id, attack_itors attacks)
{
// No attacks to choose from.
if(attacks.empty()) {
weap_index = -1;
return;
}
// The currently selected attack fits.
if(weap_index >= 0 && weap_index < static_cast<int>(attacks.size()) && attacks[weap_index].id() == weap_id) {
return;
}
// Look up the weapon by id.
if(!weap_id.empty()) {
for(int i = 0; i < static_cast<int>(attacks.size()); ++i) {
if(attacks[i].id() == weap_id) {
weap_index = i;
return;
}
}
}
// Lookup has failed.
weap_index = -1;
return;
}
/** Helper class for performing an attack. */
class attack
{
public:
attack(const map_location& attacker,
const map_location& defender,
int attack_with,
int defend_with,
bool update_display = true);
void perform();
private:
class attack_end_exception
{
};
bool perform_hit(bool, statistics::attack_context&);
void fire_event(const std::string& n);
void refresh_bc();
/** Structure holding unit info used in the attack action. */
struct unit_info
{
const map_location loc_;
int weapon_;
unit_map& units_;
std::size_t id_; /**< unit.underlying_id() */
std::string weap_id_;
int orig_attacks_;
int n_attacks_; /**< Number of attacks left. */
int cth_;
int damage_;
int xp_;
unit_info(const map_location& loc, int weapon, unit_map& units);
unit& get_unit();
bool valid();
std::string dump();
};
/**
* Used in perform_hit to confirm a replay is in sync.
* Check OOS_error_ after this method, true if error detected.
*/
void check_replay_attack_result(bool&, int, int&, config, unit_info&);
void unit_killed(
unit_info&, unit_info&, const battle_context_unit_stats*&, const battle_context_unit_stats*&, bool);
std::unique_ptr<battle_context> bc_;
const battle_context_unit_stats* a_stats_;
const battle_context_unit_stats* d_stats_;
int abs_n_attack_, abs_n_defend_;
// update_att_fog_ is not used, other than making some code simpler.
bool update_att_fog_, update_def_fog_, update_minimap_;
unit_info a_, d_;
unit_map& units_;
std::ostringstream errbuf_;
bool update_display_;
bool OOS_error_;
};
attack::unit_info::unit_info(const map_location& loc, int weapon, unit_map& units)
: loc_(loc)
, weapon_(weapon)
, units_(units)
, id_()
, weap_id_()
, orig_attacks_(0)
, n_attacks_(0)
, cth_(0)
, damage_(0)
, xp_(0)
{
unit_map::iterator i = units_.find(loc_);
if(!i.valid()) {
return;
}
id_ = i->underlying_id();
}
unit& attack::unit_info::get_unit()
{
unit_map::iterator i = units_.find(loc_);
assert(i.valid() && i->underlying_id() == id_);
return *i;
}
bool attack::unit_info::valid()
{
unit_map::iterator i = units_.find(loc_);
return i.valid() && i->underlying_id() == id_;
}
std::string attack::unit_info::dump()
{
std::stringstream s;
s << get_unit().type_id() << " (" << loc_.wml_x() << ',' << loc_.wml_y() << ')';
return s.str();
}
attack::attack(const map_location& attacker,
const map_location& defender,
int attack_with,
int defend_with,
bool update_display)
: bc_(nullptr)
, a_stats_(nullptr)
, d_stats_(nullptr)
, abs_n_attack_(0)
, abs_n_defend_(0)
, update_att_fog_(false)
, update_def_fog_(false)
, update_minimap_(false)
, a_(attacker, attack_with, resources::gameboard->units())
, d_(defender, defend_with, resources::gameboard->units())
, units_(resources::gameboard->units())
, errbuf_()
, update_display_(update_display)
, OOS_error_(false)
{
}
void attack::fire_event(const std::string& n)
{
LOG_NG << "firing " << n << " event\n";
// prepare the event data for weapon filtering
config ev_data;
config& a_weapon_cfg = ev_data.add_child("first");
config& d_weapon_cfg = ev_data.add_child("second");
// Need these to ensure weapon filters work correctly
boost::optional<attack_type::specials_context_t> a_ctx, d_ctx;
if(a_stats_->weapon != nullptr && a_.valid()) {
if(d_stats_->weapon != nullptr && d_.valid()) {
a_ctx.emplace(a_stats_->weapon->specials_context(nullptr, nullptr, a_.loc_, d_.loc_, true, d_stats_->weapon));
} else {
a_ctx.emplace(a_stats_->weapon->specials_context(nullptr, a_.loc_, true));
}
a_stats_->weapon->write(a_weapon_cfg);
}
if(d_stats_->weapon != nullptr && d_.valid()) {
if(a_stats_->weapon != nullptr && a_.valid()) {
d_ctx.emplace(d_stats_->weapon->specials_context(nullptr, nullptr, d_.loc_, a_.loc_, false, a_stats_->weapon));
} else {
d_ctx.emplace(d_stats_->weapon->specials_context(nullptr, d_.loc_, false));
}
d_stats_->weapon->write(d_weapon_cfg);
}
if(a_weapon_cfg["name"].empty()) {
a_weapon_cfg["name"] = "none";
}
if(d_weapon_cfg["name"].empty()) {
d_weapon_cfg["name"] = "none";
}
if(n == "attack_end") {
// We want to fire attack_end event in any case! Even if one of units was removed by WML.
resources::game_events->pump().fire(n, a_.loc_, d_.loc_, ev_data);
return;
}
// damage_inflicted is set in these two events.
// TODO: should we set this value from unit_info::damage, or continue using the WML variable?
if(n == "attacker_hits" || n == "defender_hits") {
ev_data["damage_inflicted"] = resources::gamedata->get_variable("damage_inflicted");
}
const int defender_side = d_.get_unit().side();
bool wml_aborted;
std::tie(std::ignore, wml_aborted) = resources::game_events->pump().fire(n,
game_events::entity_location(a_.loc_, a_.id_),
game_events::entity_location(d_.loc_, d_.id_), ev_data);
// The event could have killed either the attacker or
// defender, so we have to make sure they still exist.
refresh_bc();
if(wml_aborted || !a_.valid() || !d_.valid()
|| !resources::gameboard->get_team(a_.get_unit().side()).is_enemy(d_.get_unit().side())
) {
actions::recalculate_fog(defender_side);
if(update_display_) {
display::get_singleton()->redraw_minimap();
}
fire_event("attack_end");
throw attack_end_exception();
}
}
void attack::refresh_bc()
{
// Fix index of weapons.
if(a_.valid()) {
refresh_weapon_index(a_.weapon_, a_.weap_id_, a_.get_unit().attacks());
}
if(d_.valid()) {
refresh_weapon_index(d_.weapon_, d_.weap_id_, d_.get_unit().attacks());
}
if(!a_.valid() || !d_.valid()) {
// Fix pointer to weapons.
const_cast<battle_context_unit_stats*>(a_stats_)->weapon
= a_.valid() && a_.weapon_ >= 0 ? a_.get_unit().attacks()[a_.weapon_].shared_from_this() : nullptr;
const_cast<battle_context_unit_stats*>(d_stats_)->weapon
= d_.valid() && d_.weapon_ >= 0 ? d_.get_unit().attacks()[d_.weapon_].shared_from_this() : nullptr;
return;
}
bc_.reset(new battle_context(units_, a_.loc_, d_.loc_, a_.weapon_, d_.weapon_));
a_stats_ = &bc_->get_attacker_stats();
d_stats_ = &bc_->get_defender_stats();
a_.cth_ = a_stats_->chance_to_hit;
d_.cth_ = d_stats_->chance_to_hit;
a_.damage_ = a_stats_->damage;
d_.damage_ = d_stats_->damage;
}
bool attack::perform_hit(bool attacker_turn, statistics::attack_context& stats)
{
unit_info& attacker = attacker_turn ? a_ : d_;
unit_info& defender = attacker_turn ? d_ : a_;
// NOTE: we need to use a reference-to-pointer here so a_stats_ and d_stats_ can be
// modified without. Using a pointer directly would render them invalid when that happened.
const battle_context_unit_stats*& attacker_stats = attacker_turn ? a_stats_ : d_stats_;
const battle_context_unit_stats*& defender_stats = attacker_turn ? d_stats_ : a_stats_;
int& abs_n = attacker_turn ? abs_n_attack_ : abs_n_defend_;
bool& update_fog = attacker_turn ? update_def_fog_ : update_att_fog_;
int ran_num = randomness::generator->get_random_int(0, 99);
bool hits = (ran_num < attacker.cth_);
int damage = 0;
if(hits) {
damage = attacker.damage_;
resources::gamedata->get_variable("damage_inflicted") = damage;
}
// Make sure that if we're serializing a game here,
// we got the same results as the game did originally.
const config local_results {"chance", attacker.cth_, "hits", hits, "damage", damage};
config replay_results;
bool equals_replay = checkup_instance->local_checkup(local_results, replay_results);
if(!equals_replay) {
check_replay_attack_result(hits, ran_num, damage, replay_results, attacker);
}
// can do no more damage than the defender has hitpoints
int damage_done = std::min<int>(defender.get_unit().hitpoints(), attacker.damage_);
// expected damage = damage potential * chance to hit (as a percentage)
double expected_damage = damage_done * attacker.cth_ * 0.01;
if(attacker_turn) {
stats.attack_expected_damage(expected_damage, 0);
} else {
stats.attack_expected_damage(0, expected_damage);
}
int drains_damage = 0;
if(hits && attacker_stats->drains) {
drains_damage = damage_done * attacker_stats->drain_percent / 100 + attacker_stats->drain_constant;
// don't drain so much that the attacker gets more than his maximum hitpoints
drains_damage =
std::min<int>(drains_damage, attacker.get_unit().max_hitpoints() - attacker.get_unit().hitpoints());
// if drain is negative, don't allow drain to kill the attacker
drains_damage = std::max<int>(drains_damage, 1 - attacker.get_unit().hitpoints());
}
if(update_display_) {
std::ostringstream float_text;
std::vector<std::string> extra_hit_sounds;
if(hits) {
const unit& defender_unit = defender.get_unit();
if(attacker_stats->poisons && !defender_unit.get_state(unit::STATE_POISONED)) {
float_text << (defender_unit.gender() == unit_race::FEMALE ? _("female^poisoned") : _("poisoned"))
<< '\n';
extra_hit_sounds.push_back(game_config::sounds::status::poisoned);
}
if(attacker_stats->slows && !defender_unit.get_state(unit::STATE_SLOWED)) {
float_text << (defender_unit.gender() == unit_race::FEMALE ? _("female^slowed") : _("slowed")) << '\n';
extra_hit_sounds.push_back(game_config::sounds::status::slowed);
}
if(attacker_stats->petrifies) {
float_text << (defender_unit.gender() == unit_race::FEMALE ? _("female^petrified") : _("petrified"))
<< '\n';
extra_hit_sounds.push_back(game_config::sounds::status::petrified);
}
}
unit_display::unit_attack(
game_display::get_singleton(),
*resources::gameboard,
attacker.loc_, defender.loc_,
damage,
*attacker_stats->weapon, defender_stats->weapon,
abs_n, float_text.str(), drains_damage, "",
&extra_hit_sounds
);
}
bool dies = defender.get_unit().take_hit(damage);
LOG_NG << "defender took " << damage << (dies ? " and died\n" : "\n");
if(attacker_turn) {
stats.attack_result(hits
? (dies
? statistics::attack_context::KILLS
: statistics::attack_context::HITS)
: statistics::attack_context::MISSES, damage_done, drains_damage
);
} else {
stats.defend_result(hits
? (dies
? statistics::attack_context::KILLS
: statistics::attack_context::HITS)
: statistics::attack_context::MISSES, damage_done, drains_damage
);
}
replay_results.clear();
// There was also a attribute cfg["unit_hit"] which was never used so i deleted.
equals_replay = checkup_instance->local_checkup(config{"dies", dies}, replay_results);
if(!equals_replay) {
bool results_dies = replay_results["dies"].to_bool();
errbuf_ << "SYNC: In attack " << a_.dump() << " vs " << d_.dump() << ": the data source says the "
<< (attacker_turn ? "defender" : "attacker") << ' ' << (results_dies ? "perished" : "survived")
<< " while in-game calculations show it " << (dies ? "perished" : "survived")
<< " (over-riding game calculations with data source results)\n";
dies = results_dies;
// Set hitpoints to 0 so later checks don't invalidate the death.
if(results_dies) {
defender.get_unit().set_hitpoints(0);
}
OOS_error_ = true;
}
if(hits) {
try {
fire_event(attacker_turn ? "attacker_hits" : "defender_hits");
} catch(attack_end_exception) {
refresh_bc();
return false;
}
} else {
try {
fire_event(attacker_turn ? "attacker_misses" : "defender_misses");
} catch(attack_end_exception) {
refresh_bc();
return false;
}
}
refresh_bc();
bool attacker_dies = false;
if(drains_damage > 0) {
attacker.get_unit().heal(drains_damage);
} else if(drains_damage < 0) {
attacker_dies = attacker.get_unit().take_hit(-drains_damage);
}
if(dies) {
unit_killed(attacker, defender, attacker_stats, defender_stats, false);
update_fog = true;
}
if(attacker_dies) {
unit_killed(defender, attacker, defender_stats, attacker_stats, true);
(attacker_turn ? update_att_fog_ : update_def_fog_) = true;
}
if(dies) {
update_minimap_ = true;
return false;
}
if(hits) {
unit& defender_unit = defender.get_unit();
if(attacker_stats->poisons && !defender_unit.get_state(unit::STATE_POISONED)) {
defender_unit.set_state(unit::STATE_POISONED, true);
LOG_NG << "defender poisoned\n";
}
if(attacker_stats->slows && !defender_unit.get_state(unit::STATE_SLOWED)) {
defender_unit.set_state(unit::STATE_SLOWED, true);
update_fog = true;
defender.damage_ = defender_stats->slow_damage;
LOG_NG << "defender slowed\n";
}
// If the defender is petrified, the fight stops immediately
if(attacker_stats->petrifies) {
defender_unit.set_state(unit::STATE_PETRIFIED, true);
update_fog = true;
attacker.n_attacks_ = 0;
defender.n_attacks_ = -1; // Petrified.
resources::game_events->pump().fire("petrified", defender.loc_, attacker.loc_);
refresh_bc();
}
}
// Delay until here so that poison and slow go through
if(attacker_dies) {
update_minimap_ = true;
return false;
}
--attacker.n_attacks_;
return true;
}
void attack::unit_killed(unit_info& attacker,
unit_info& defender,
const battle_context_unit_stats*& attacker_stats,
const battle_context_unit_stats*& defender_stats,
bool drain_killed)
{
attacker.xp_ = game_config::kill_xp(defender.get_unit().level());
defender.xp_ = 0;
display::get_singleton()->invalidate(attacker.loc_);
game_events::entity_location death_loc(defender.loc_, defender.id_);
game_events::entity_location attacker_loc(attacker.loc_, attacker.id_);
std::string undead_variation = defender.get_unit().undead_variation();
fire_event("attack_end");
refresh_bc();
// Get weapon info for last_breath and die events.
config dat;
config a_weapon_cfg = attacker_stats->weapon && attacker.valid() ? attacker_stats->weapon->to_config() : config();
config d_weapon_cfg = defender_stats->weapon && defender.valid() ? defender_stats->weapon->to_config() : config();
if(a_weapon_cfg["name"].empty()) {
a_weapon_cfg["name"] = "none";
}
if(d_weapon_cfg["name"].empty()) {
d_weapon_cfg["name"] = "none";
}
dat.add_child("first", d_weapon_cfg);
dat.add_child("second", a_weapon_cfg);
resources::game_events->pump().fire("last_breath", death_loc, attacker_loc, dat);
refresh_bc();
// WML has invalidated the dying unit, abort.
if(!defender.valid() || defender.get_unit().hitpoints() > 0) {
return;
}
if(!attacker.valid()) {
unit_display::unit_die(
defender.loc_,
defender.get_unit(),
nullptr,
defender_stats->weapon
);
} else {
unit_display::unit_die(
defender.loc_,
defender.get_unit(),
attacker_stats->weapon,
defender_stats->weapon,
attacker.loc_,
&attacker.get_unit()
);
}
resources::game_events->pump().fire("die", death_loc, attacker_loc, dat);
refresh_bc();
if(!defender.valid() || defender.get_unit().hitpoints() > 0) {
// WML has invalidated the dying unit, abort
return;
}
units_.erase(defender.loc_);
resources::whiteboard->on_kill_unit();
// Plague units make new units on the target hex.
if(attacker.valid() && attacker_stats->plagues && !drain_killed) {
LOG_NG << "trying to reanimate " << attacker_stats->plague_type << '\n';
if(const unit_type* reanimator = unit_types.find(attacker_stats->plague_type)) {
LOG_NG << "found unit type:" << reanimator->id() << '\n';
unit_ptr newunit = unit::create(*reanimator, attacker.get_unit().side(), true, unit_race::MALE);
newunit->set_attacks(0);
newunit->set_movement(0, true);
newunit->set_facing(map_location::get_opposite_dir(attacker.get_unit().facing()));
// Apply variation
if(undead_variation != "null") {
config mod;
config& variation = mod.add_child("effect");
variation["apply_to"] = "variation";
variation["name"] = undead_variation;
newunit->add_modification("variation", mod);
newunit->heal_fully();
}
newunit->set_location(death_loc);
units_.insert(newunit);
game_events::entity_location reanim_loc(defender.loc_, newunit->underlying_id());
resources::game_events->pump().fire("unit_placed", reanim_loc);
preferences::encountered_units().insert(newunit->type_id());
if(update_display_) {
display::get_singleton()->invalidate(death_loc);
}
}
} else {
LOG_NG << "unit not reanimated\n";
}
}
void attack::perform()
{
// Stop the user from issuing any commands while the units are fighting.
const events::command_disabler disable_commands;
if(!a_.valid() || !d_.valid()) {
return;
}
// no attack weapon => stop here and don't attack
if(a_.weapon_ < 0) {
a_.get_unit().set_attacks(a_.get_unit().attacks_left() - 1);
a_.get_unit().set_movement(-1, true);
return;
}
a_.get_unit().set_facing(a_.loc_.get_relative_dir(d_.loc_));
d_.get_unit().set_facing(d_.loc_.get_relative_dir(a_.loc_));
a_.get_unit().set_attacks(a_.get_unit().attacks_left() - 1);
VALIDATE(a_.weapon_ < static_cast<int>(a_.get_unit().attacks().size()),
_("An invalid attacker weapon got selected."));
a_.get_unit().set_movement(a_.get_unit().movement_left() - a_.get_unit().attacks()[a_.weapon_].movement_used(), true);
a_.get_unit().set_state(unit::STATE_NOT_MOVED, false);
a_.get_unit().set_resting(false);
d_.get_unit().set_resting(false);
// If the attacker was invisible, she isn't anymore!
a_.get_unit().set_state(unit::STATE_UNCOVERED, true);
bc_.reset(new battle_context(units_, a_.loc_, d_.loc_, a_.weapon_, d_.weapon_));
a_stats_ = &bc_->get_attacker_stats();
d_stats_ = &bc_->get_defender_stats();
if(a_stats_->weapon) {
a_.weap_id_ = a_stats_->weapon->id();
}
if(d_stats_->weapon) {
d_.weap_id_ = d_stats_->weapon->id();
}
try {
fire_event("attack");
} catch(attack_end_exception) {
return;
}
refresh_bc();
DBG_NG << "getting attack statistics\n";
statistics::attack_context attack_stats(
a_.get_unit(), d_.get_unit(), a_stats_->chance_to_hit, d_stats_->chance_to_hit);
a_.orig_attacks_ = a_stats_->num_blows;
d_.orig_attacks_ = d_stats_->num_blows;
a_.n_attacks_ = a_.orig_attacks_;
d_.n_attacks_ = d_.orig_attacks_;
a_.xp_ = d_.get_unit().level();
d_.xp_ = a_.get_unit().level();
bool defender_strikes_first = (d_stats_->firststrike && !a_stats_->firststrike);
unsigned int rounds = std::max<unsigned int>(a_stats_->rounds, d_stats_->rounds) - 1;
const int defender_side = d_.get_unit().side();
LOG_NG << "Fight: (" << a_.loc_ << ") vs (" << d_.loc_ << ") ATT: " << a_stats_->weapon->name() << " "
<< a_stats_->damage << "-" << a_stats_->num_blows << "(" << a_stats_->chance_to_hit
<< "%) vs DEF: " << (d_stats_->weapon ? d_stats_->weapon->name() : "none") << " " << d_stats_->damage << "-"
<< d_stats_->num_blows << "(" << d_stats_->chance_to_hit << "%)"
<< (defender_strikes_first ? " defender first-strike" : "") << "\n";
// Play the pre-fight animation
unit_display::unit_draw_weapon(a_.loc_, a_.get_unit(), a_stats_->weapon, d_stats_->weapon, d_.loc_, &d_.get_unit());
for(;;) {
DBG_NG << "start of attack loop...\n";
++abs_n_attack_;
if(a_.n_attacks_ > 0 && !defender_strikes_first) {
if(!perform_hit(true, attack_stats)) {
DBG_NG << "broke from attack loop on attacker turn\n";
break;
}
}
// If the defender got to strike first, they use it up here.
defender_strikes_first = false;
++abs_n_defend_;
if(d_.n_attacks_ > 0) {
if(!perform_hit(false, attack_stats)) {
DBG_NG << "broke from attack loop on defender turn\n";
break;
}
}
// Continue the fight to death; if one of the units got petrified,
// either n_attacks or n_defends is -1
if(rounds > 0 && d_.n_attacks_ == 0 && a_.n_attacks_ == 0) {
a_.n_attacks_ = a_.orig_attacks_;
d_.n_attacks_ = d_.orig_attacks_;
--rounds;
defender_strikes_first = (d_stats_->firststrike && !a_stats_->firststrike);
}
if(a_.n_attacks_ <= 0 && d_.n_attacks_ <= 0) {
fire_event("attack_end");
refresh_bc();
break;
}
}
// Set by attacker_hits and defender_hits events.
resources::gamedata->clear_variable("damage_inflicted");
if(update_def_fog_) {
actions::recalculate_fog(defender_side);
}
// TODO: if we knew the viewing team, we could skip this display update
if(update_minimap_ && update_display_) {
display::get_singleton()->redraw_minimap();
}
if(a_.valid()) {
unit& u = a_.get_unit();
u.anim_comp().set_standing();
u.set_experience(u.experience() + a_.xp_);
}
if(d_.valid()) {
unit& u = d_.get_unit();
u.anim_comp().set_standing();
u.set_experience(u.experience() + d_.xp_);
}
unit_display::unit_sheath_weapon(a_.loc_, a_.valid() ? &a_.get_unit() : nullptr, a_stats_->weapon, d_stats_->weapon,
d_.loc_, d_.valid() ? &d_.get_unit() : nullptr);
if(update_display_) {
game_display::get_singleton()->invalidate_unit();
display::get_singleton()->invalidate(a_.loc_);
display::get_singleton()->invalidate(d_.loc_);
}
if(OOS_error_) {
replay::process_error(errbuf_.str());
}
}
void attack::check_replay_attack_result(
bool& hits, int ran_num, int& damage, config replay_results, unit_info& attacker)
{
int results_chance = replay_results["chance"];
bool results_hits = replay_results["hits"].to_bool();
int results_damage = replay_results["damage"];
#if 0
errbuf_ << "SYNC: In attack " << a_.dump() << " vs " << d_.dump()
<< " replay data differs from local calculated data:"
<< " chance to hit in data source: " << results_chance
<< " chance to hit in calculated: " << attacker.cth_
<< " chance to hit in data source: " << results_chance
<< " chance to hit in calculated: " << attacker.cth_
;
attacker.cth_ = results_chance;
hits = results_hits;
damage = results_damage;
OOS_error_ = true;
#endif
if(results_chance != attacker.cth_) {
errbuf_ << "SYNC: In attack " << a_.dump() << " vs " << d_.dump()
<< ": chance to hit is inconsistent. Data source: " << results_chance
<< "; Calculation: " << attacker.cth_ << " (over-riding game calculations with data source results)\n";
attacker.cth_ = results_chance;
OOS_error_ = true;
}
if(results_hits != hits) {
errbuf_ << "SYNC: In attack " << a_.dump() << " vs " << d_.dump() << ": the data source says the hit was "
<< (results_hits ? "successful" : "unsuccessful") << ", while in-game calculations say the hit was "
<< (hits ? "successful" : "unsuccessful") << " random number: " << ran_num << " = " << (ran_num % 100)
<< "/" << results_chance << " (over-riding game calculations with data source results)\n";
hits = results_hits;
OOS_error_ = true;
}
if(results_damage != damage) {
errbuf_ << "SYNC: In attack " << a_.dump() << " vs " << d_.dump() << ": the data source says the hit did "
<< results_damage << " damage, while in-game calculations show the hit doing " << damage
<< " damage (over-riding game calculations with data source results)\n";
damage = results_damage;
OOS_error_ = true;
}
}
} // end anonymous namespace
// ==================================================================================
// FREE-STANDING FUNCTIONS
// ==================================================================================
void attack_unit(const map_location& attacker,
const map_location& defender,
int attack_with,
int defend_with,
bool update_display)
{
attack dummy(attacker, defender, attack_with, defend_with, update_display);
dummy.perform();
}
void attack_unit_and_advance(const map_location& attacker,
const map_location& defender,
int attack_with,
int defend_with,
bool update_display,
const ai::unit_advancements_aspect& ai_advancement)
{
attack_unit(attacker, defender, attack_with, defend_with, update_display);
unit_map::const_iterator atku = resources::gameboard->units().find(attacker);
if(atku != resources::gameboard->units().end()) {
advance_unit_at(advance_unit_params(attacker).ai_advancements(ai_advancement));
}
unit_map::const_iterator defu = resources::gameboard->units().find(defender);
if(defu != resources::gameboard->units().end()) {
advance_unit_at(advance_unit_params(defender).ai_advancements(ai_advancement));
}
}
std::pair<int, map_location> under_leadership(const unit_map& units, const map_location& loc)
{
const unit_map::const_iterator un = units.find(loc);
if(un == units.end()) {
return {0, map_location::null_location()};
}
unit_ability_list abil = un->get_abilities("leadership");
return abil.highest("value");
}
int combat_modifier(const unit_map& units,
const gamemap& map,
const map_location& loc,
unit_type::ALIGNMENT alignment,
bool is_fearless)
{
const tod_manager& tod_m = *resources::tod_manager;
int lawful_bonus = tod_m.get_illuminated_time_of_day(units, map, loc).lawful_bonus;
return generic_combat_modifier(lawful_bonus, alignment, is_fearless);
}
int generic_combat_modifier(int lawful_bonus, unit_type::ALIGNMENT alignment, bool is_fearless)
{
int bonus;
switch(alignment.v) {
case unit_type::ALIGNMENT::LAWFUL:
bonus = lawful_bonus;
break;
case unit_type::ALIGNMENT::NEUTRAL:
bonus = 0;
break;
case unit_type::ALIGNMENT::CHAOTIC:
bonus = -lawful_bonus;
break;
case unit_type::ALIGNMENT::LIMINAL:
bonus = -std::abs(lawful_bonus);
break;
default:
bonus = 0;
}
if(is_fearless) {
bonus = std::max<int>(bonus, 0);
}
return bonus;
}
bool backstab_check(const map_location& attacker_loc,
const map_location& defender_loc,
const unit_map& units,
const std::vector<team>& teams)
{
const unit_map::const_iterator defender = units.find(defender_loc);
if(defender == units.end()) {
return false; // No defender
}
adjacent_loc_array_t adj;
get_adjacent_tiles(defender_loc, adj.data());
unsigned i;
for(i = 0; i < adj.size(); ++i) {
if(adj[i] == attacker_loc) {
break;
}
}
if(i >= 6) {
return false; // Attack not from adjacent location
}
const unit_map::const_iterator opp = units.find(adj[(i + 3) % 6]);
// No opposite unit.
if(opp == units.end()) {
return false;
}
if(opp->incapacitated()) {
return false;
}
// If sides aren't valid teams, then they are enemies.
if(std::size_t(defender->side() - 1) >= teams.size() || std::size_t(opp->side() - 1) >= teams.size()) {
return true;
}
// Defender and opposite are enemies.
if(teams[defender->side() - 1].is_enemy(opp->side())) {
return true;
}
// Defender and opposite are friends.
return false;
}