wesnoth/src/actions.cpp
Nils Kneuper c588445335 commit map border change for mordante
now only wmllint --maplift has to be run on data/, could you do this esr?
2007-10-26 16:02:04 +00:00

2253 lines
73 KiB
C++

/* $Id$ */
/*
Copyright (C) 2003 - 2007 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 version 2
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 actions.cpp
//! Recruiting, Fighting.
#include "actions.hpp"
#include "attack_prediction.hpp"
#include "game_config.hpp"
#include "game_errors.hpp"
#include "game_preferences.hpp"
#include "game_events.hpp"
#include "gettext.hpp"
#include "halo.hpp"
#include "hotkeys.hpp"
#include "menu_events.hpp"
#include "mouse_events.hpp"
#include "pathfind.hpp"
#include "random.hpp"
#include "replay.hpp"
#include "sound.hpp"
#include "statistics.hpp"
#include "unit_abilities.hpp"
#include "unit_display.hpp"
#include "wassert.hpp"
#include "wml_separators.hpp"
#include "serialization/binary_wml.hpp"
#include "serialization/parser.hpp"
#define LOG_NG LOG_STREAM(info, engine)
#define ERR_NW LOG_STREAM(err, network)
struct castle_cost_calculator : cost_calculator
{
castle_cost_calculator(const gamemap& map) : map_(map)
{}
virtual double cost(const gamemap::location&, const gamemap::location& loc, const double, const bool) const
{
if(!map_.is_castle(loc))
return 10000;
return 1;
}
private:
const gamemap& map_;
};
// Conditions placed on victory must be accessible from the global function
// check_victory, but shouldn't be passed to that function as parameters,
// since it is called from a variety of places.
namespace victory_conditions
{
static bool when_enemies_defeated = true;
void set_victory_when_enemies_defeated(bool on)
{
when_enemies_defeated = on;
}
static bool victory_when_enemies_defeated()
{
return when_enemies_defeated;
}
}
bool can_recruit_on(const gamemap& map, const gamemap::location& leader, const gamemap::location loc)
{
if(!map.on_board(loc))
return false;
if(!map.is_castle(loc))
return false;
castle_cost_calculator calc(map);
const paths::route& rt = a_star_search(leader, loc, 100.0, &calc, map.w(), map.h());
if(rt.steps.empty())
return false;
return true;
}
std::string recruit_unit(const gamemap& map, int side,
unit_map& units, unit new_unit,
gamemap::location& recruit_location, bool show, bool need_castle, bool full_movement)
{
const events::command_disabler disable_commands;
LOG_NG << "recruiting unit for side " << side << "\n";
// Find the unit that can recruit
unit_map::const_iterator u = units.begin();
for(; u != units.end(); ++u) {
if(u->second.can_recruit() &&
static_cast<int>(u->second.side()) == side) {
break;
}
}
if(u == units.end() && (need_castle || !map.on_board(recruit_location))) {
return _("You don't have a leader to recruit with.");
}
wassert(u != units.end() || !need_castle);
if(need_castle && map.is_keep(u->first) == false) {
LOG_NG << "Leader not on start: leader is on " << u->first << '\n';
return _("You must have your leader on a keep to recruit or recall units.");
}
if(need_castle) {
if (units.find(recruit_location) != units.end() ||
!can_recruit_on(map, u->first, recruit_location)) {
recruit_location = gamemap::location();
}
}
if(!map.on_board(recruit_location)) {
recruit_location = find_vacant_tile(map,units,u->first,
need_castle ? VACANT_CASTLE : VACANT_ANY);
} else if(units.count(recruit_location) == 1) {
recruit_location = find_vacant_tile(map,units,recruit_location,VACANT_ANY);
}
if(!map.on_board(recruit_location)) {
return _("There are no vacant castle tiles in which to recruit a unit.");
}
if(full_movement) {
new_unit.set_movement(new_unit.total_movement());
} else {
new_unit.set_movement(0);
new_unit.set_attacks(0);
}
new_unit.heal_all();
units.add(new std::pair<gamemap::location,unit>(recruit_location,new_unit));
LOG_NG << "firing prerecruit event\n";
game_events::fire("prerecruit",recruit_location);
if(show)unit_display::unit_recruited(recruit_location);
LOG_NG << "firing recruit event\n";
game_events::fire("recruit",recruit_location);
const std::string checksum = get_checksum(new_unit, true);
const config* ran_results = get_random_results();
if(ran_results != NULL) {
const std::string rc = (*ran_results)["checksum"];
if(rc != checksum) {
ERR_NW << "SYNC: In recruit " << new_unit.id() <<
": has checksum " << checksum <<
" while datasource has checksum " <<
rc << "\n";
config cfg_unit1;
new_unit.write(cfg_unit1);
::write(std::cerr, cfg_unit1);
//! @todo FIXME enabling this exception will trigger an assertion failure
// in display.cpp:1010 when loading a map (the cause of that OOS
// will be fixed soon). (Only tested in SP for bug #9728)
// if (!game_config::ignore_replay_errors) {
// throw replay::error("OOS while recruiting.");
// }
}
} else {
config cfg;
cfg["checksum"] = checksum;
set_random_results(cfg);
}
// If an OOS happens, this option allows to write
// the debug info about the recruited unit.
if(!lg::info.dont_log(lg::engine)) {
config cfg_unit;
new_unit.write(cfg_unit);
LOG_NG << "recruit unit\nChecksum = "
<< checksum << "\n-----[start data]-----\n";
::write(lg::info(lg::engine), cfg_unit);
LOG_NG << "\n----[end data]-----\n";
}
return std::string();
}
gamemap::location under_leadership(const unit_map& units,
const gamemap::location& loc, int* bonus)
{
const unit_map::const_iterator un = units.find(loc);
if(un == units.end()) {
return gamemap::location::null_location;
}
unit_ability_list abil = un->second.get_abilities("leadership",loc);
int best_bonus = abil.highest("value").first;
if(bonus) {
*bonus = best_bonus;
}
return abil.highest("value").second;
}
battle_context::battle_context(const gamemap& map, const std::vector<team>& teams, const unit_map& units,
const gamestatus& status, const game_data& gamedata,
const gamemap::location& attacker_loc, const gamemap::location& defender_loc,
int attacker_weapon, int defender_weapon, double aggression, const combatant *prev_def)
: attacker_stats_(NULL), defender_stats_(NULL), attacker_combatant_(NULL), defender_combatant_(NULL)
{
const unit& attacker = units.find(attacker_loc)->second;
const unit& defender = units.find(defender_loc)->second;
const double harm_weight = 1.0 - aggression;
// A Python AI can send an invalid weapon and crash Wesnoth.
// Haven't found a way for the Python API to prevent this problem.
// So instead of segfaulting it sends an assertion failure.
wassert(attacker_weapon < static_cast<int>(attacker.attacks().size()));
if (attacker_weapon == -1 && attacker.attacks().size() == 1 && attacker.attacks()[0].attack_weight() > 0 )
attacker_weapon = 0;
if (attacker_weapon == -1) {
wassert(defender_weapon == -1);
attacker_weapon = choose_attacker_weapon(attacker, defender, map, teams, units,
status, gamedata, attacker_loc, defender_loc,
harm_weight, &defender_weapon, prev_def);
} else if (defender_weapon == -1) {
defender_weapon = choose_defender_weapon(attacker, defender, attacker_weapon, map, teams,
units, status, gamedata, attacker_loc, defender_loc, prev_def);
}
// If those didn't have to generate statistics, do so now.
if (!attacker_stats_) {
const attack_type *adef = NULL;
const attack_type *ddef = NULL;
if (attacker_weapon >= 0) {
wassert(attacker_weapon < static_cast<int>(attacker.attacks().size()));
adef = &attacker.attacks()[attacker_weapon];
}
if (defender_weapon >= 0) {
wassert(defender_weapon < static_cast<int>(defender.attacks().size()));
ddef = &defender.attacks()[defender_weapon];
}
wassert(!defender_stats_ && !attacker_combatant_ && !defender_combatant_);
attacker_stats_ = new unit_stats(attacker, attacker_loc, attacker_weapon,
true, defender, defender_loc, ddef,
units, teams, status, map, gamedata);
defender_stats_ = new unit_stats(defender, defender_loc, defender_weapon, false,
attacker, attacker_loc, adef,
units, teams, status, map, gamedata);
}
}
battle_context::battle_context(const battle_context &other)
: attacker_stats_(NULL), defender_stats_(NULL), attacker_combatant_(NULL), defender_combatant_(NULL)
{
*this = other;
}
battle_context::battle_context(const unit_stats &att, const unit_stats &def)
{
attacker_stats_ = new unit_stats(att);
defender_stats_ = new unit_stats(def);
attacker_combatant_ = NULL;
defender_combatant_ = NULL;
}
battle_context::~battle_context()
{
delete attacker_stats_;
delete defender_stats_;
delete attacker_combatant_;
delete defender_combatant_;
}
battle_context& battle_context::operator=(const battle_context &other)
{
if (&other != this) {
delete attacker_stats_;
delete defender_stats_;
delete attacker_combatant_;
delete defender_combatant_;
attacker_stats_ = new unit_stats(*other.attacker_stats_);
defender_stats_ = new unit_stats(*other.defender_stats_);
attacker_combatant_ = other.attacker_combatant_ ? new combatant(*other.attacker_combatant_, *attacker_stats_) : NULL;
defender_combatant_ = other.defender_combatant_ ? new combatant(*other.defender_combatant_, *defender_stats_) : NULL;
}
return *this;
}
//! @todo FIXME: Hand previous defender unit in here.
int battle_context::choose_defender_weapon(const unit &attacker, const unit &defender, unsigned attacker_weapon,
const gamemap& map, const std::vector<team>& teams, const unit_map& units,
const gamestatus& status, const game_data& gamedata,
const gamemap::location& attacker_loc, const gamemap::location& defender_loc,
const combatant *prev_def)
{
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.size() == 0)
return -1;
if (choices.size() == 1)
return 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
// Elligible attacks for defense should have a simple rating greater or equal to this weight.
double max_weight = 0.0;
int min_rating = 0;
for (i = 0; i < choices.size(); i++) {
const attack_type &def = defender.attacks()[choices[i]];
if (def.defense_weight() > max_weight) {
max_weight = def.defense_weight();
unit_stats *def_stats = new unit_stats(defender, defender_loc, choices[i], false,
attacker, attacker_loc, &att,
units, teams, status, map, gamedata);
min_rating = static_cast<int>(def_stats->num_blows * def_stats->damage *
def_stats->chance_to_hit * def.defense_weight());
delete def_stats;
}
else if (def.defense_weight() == max_weight) {
unit_stats *def_stats = new unit_stats(defender, defender_loc, choices[i], false,
attacker, attacker_loc, &att,
units, teams, status, map, gamedata);
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 )
min_rating = simple_rating;
delete def_stats;
}
}
// Multiple options: simulate them, save best.
for (i = 0; i < choices.size(); i++) {
const attack_type &def = defender.attacks()[choices[i]];
unit_stats *att_stats = new unit_stats(attacker, attacker_loc, attacker_weapon,
true, defender, defender_loc, &def,
units, teams, status, map, gamedata);
unit_stats *def_stats = new unit_stats(defender, defender_loc, choices[i], false,
attacker, attacker_loc, &att,
units, teams, status, map, gamedata);
combatant *att_comb = new combatant(*att_stats);
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) )
) {
delete attacker_combatant_;
delete defender_combatant_;
delete attacker_stats_;
delete defender_stats_;
attacker_combatant_ = att_comb;
defender_combatant_ = def_comb;
attacker_stats_ = att_stats;
defender_stats_ = def_stats;
} else {
delete att_comb;
delete def_comb;
delete att_stats;
delete def_stats;
}
}
return defender_stats_->attack_num;
}
int battle_context::choose_attacker_weapon(const unit &attacker, const unit &defender,
const gamemap& map, const std::vector<team>& teams, const unit_map& units,
const gamestatus& status, const game_data& gamedata,
const gamemap::location& attacker_loc, const gamemap::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.size() == 0)
return -1;
if (choices.size() == 1) {
*defender_weapon = choose_defender_weapon(attacker, defender, choices[0], map, teams, units,
status, gamedata, attacker_loc, defender_loc, prev_def);
return choices[0];
}
// Multiple options: simulate them, save best.
unit_stats *best_att_stats = NULL, *best_def_stats = NULL;
combatant *best_att_comb = NULL, *best_def_comb = NULL;
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], map, teams, units,
status, gamedata, attacker_loc, defender_loc, prev_def);
// If that didn't simulate, do so now.
if (!attacker_combatant_) {
const attack_type *def = NULL;
if (def_weapon >= 0) {
def = &defender.attacks()[def_weapon];
}
attacker_stats_ = new unit_stats(attacker, attacker_loc, choices[i],
true, defender, defender_loc, def,
units, teams, status, map, gamedata);
defender_stats_ = new unit_stats(defender, defender_loc, def_weapon, false,
attacker, attacker_loc, &att,
units, teams, status, map, gamedata);
attacker_combatant_ = new combatant(*attacker_stats_);
defender_combatant_ = new combatant(*defender_stats_, prev_def);
attacker_combatant_->fight(*defender_combatant_);
}
if (!best_att_comb || better_combat(*attacker_combatant_, *defender_combatant_,
*best_att_comb, *best_def_comb, harm_weight)) {
delete best_att_comb;
delete best_def_comb;
delete best_att_stats;
delete best_def_stats;
best_att_comb = attacker_combatant_;
best_def_comb = defender_combatant_;
best_att_stats = attacker_stats_;
best_def_stats = defender_stats_;
} else {
delete attacker_combatant_;
delete defender_combatant_;
delete attacker_stats_;
delete defender_stats_;
}
attacker_combatant_ = NULL;
defender_combatant_ = NULL;
attacker_stats_ = NULL;
defender_stats_ = NULL;
}
attacker_combatant_ = best_att_comb;
defender_combatant_ = best_def_comb;
attacker_stats_ = best_att_stats;
defender_stats_ = best_def_stats;
*defender_weapon = defender_stats_->attack_num;
return attacker_stats_->attack_num;
}
// 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;
// Compare: damage to them - damage to us (average_hp replaces -damage)
a = us_a.average_hp()*harm_weight - them_a.average_hp();
b = us_b.average_hp()*harm_weight - them_b.average_hp();
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();
}
// Get the simulation results.
//! @todo FIXME: better to initialize combatant initially (move into 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_) {
wassert(!defender_combatant_);
attacker_combatant_ = new combatant(*attacker_stats_);
defender_combatant_ = 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_) {
wassert(!attacker_combatant_);
attacker_combatant_ = new combatant(*attacker_stats_);
defender_combatant_ = new combatant(*defender_stats_, prev_def);
attacker_combatant_->fight(*defender_combatant_);
}
return *defender_combatant_;
}
battle_context::unit_stats::unit_stats(const unit &u, const gamemap::location& u_loc,
int u_attack_num, bool attacking,
const unit &opp, const gamemap::location& opp_loc,
const attack_type *opp_weapon,
const unit_map& units,
const std::vector<team>& teams,
const gamestatus& status,
const gamemap& map,
const game_data& gamedata)
{
// Get the current state of the unit.
attack_num = u_attack_num;
if (attack_num >= 0) {
weapon = &u.attacks()[attack_num];
} else {
weapon = NULL;
}
is_attacker = attacking;
is_poisoned = utils::string_bool(u.get_state("poisoned"));
is_slowed = utils::string_bool(u.get_state("slowed"));
if(u.hitpoints() < 0) {
//! @todo FIXME enable after 1.3.2 and find out why this happens -- Mordante
// LOG_STREAM(err, config) << "Unit with " << u.hitpoints() << " hitpoints found, set to 0 for damage calculations\n";
hp = 0;
} else {
hp = u.hitpoints();
}
max_hp = u.max_hitpoints();
// Get the weapon characteristics, if any.
if (weapon) {
weapon->set_specials_context(u_loc, opp_loc, &gamedata, &units, &map, &status, &teams, attacking, opp_weapon);
if (opp_weapon)
opp_weapon->set_specials_context(u_loc, opp_loc, &gamedata, &units, &map, &status, &teams, !attacking, weapon);
slows = weapon->get_special_bool("slow");
drains = weapon->get_special_bool("drains") && !utils::string_bool(opp.get_state("not_living"));
stones = weapon->get_special_bool("stones");
poisons = weapon->get_special_bool("poison") && opp.get_state("not_living") != "yes" && opp.get_state("poisoned") != "yes";
backstab_pos = is_attacker && backstab_check(u_loc, opp_loc, units, teams);
rounds = weapon->get_specials("berserk").highest("value", 1).first;
firststrike = weapon->get_special_bool("firststrike");
// Handle plague.
unit_ability_list plague_specials = weapon->get_specials("plague");
plagues = !plague_specials.empty() && opp.get_state("not_living") != "yes" &&
strcmp(opp.undead_variation().c_str(), "null") && !map.is_village(opp_loc);
if (!plague_specials.empty()) {
if((*plague_specials.cfgs.front().first)["type"] == "")
plague_type = u.id();
else
plague_type = (*plague_specials.cfgs.front().first)["type"];
}
// Compute chance to hit.
chance_to_hit = opp.defense_modifier(map.get_terrain(opp_loc));
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();
// Compute base damage done with the weapon.
int base_damage = weapon->damage();
unit_ability_list dmg_specials = weapon->get_specials("damage");
unit_abilities::effect dmg_effect(dmg_specials, base_damage, backstab_pos);
base_damage = dmg_effect.get_composite_value();
// Get the damage multiplier applied to the base damage of the weapon.
int damage_multiplier = 100;
// Time of day bonus.
damage_multiplier += combat_modifier(status, units, u_loc, u.alignment(), u.is_fearless(), map);
// Leadership bonus.
int leader_bonus = 0;
if (under_leadership(units, u_loc, &leader_bonus).valid())
damage_multiplier += leader_bonus;
// Resistance modifier.
damage_multiplier *= opp.damage_from(*weapon, !attacking, opp_loc);
// Compute both the normal and slowed damage. For the record,
// drain = normal damage / 2 and slow_drain = slow_damage / 2.
damage = round_damage(base_damage, damage_multiplier, 10000);
slow_damage = round_damage(base_damage, damage_multiplier, 20000);
if (is_slowed)
damage = slow_damage;
// Compute the number of blows and handle swarm.
unit_ability_list swarm_specials = weapon->get_specials("attacks");
if (!swarm_specials.empty()) {
swarm = true;
swarm_min = swarm_specials.highest("attacks_min").first;
swarm_max = swarm_specials.highest("attacks_max", weapon->num_attacks()).first;
num_blows = swarm_min + (swarm_max - swarm_min) * hp / max_hp;
} else {
swarm = false;
num_blows = weapon->num_attacks();
swarm_min = num_blows;
swarm_max = num_blows;
}
} else {
slows = false;
drains = false;
stones = false;
plagues = false;
poisons = false;
backstab_pos = false;
swarm = false;
rounds = 1;
firststrike = false;
chance_to_hit = 0;
damage = 0;
slow_damage = 0;
num_blows = 0;
swarm_min = 0;
swarm_max = 0;
}
}
battle_context::unit_stats::~unit_stats()
{
}
void battle_context::unit_stats::dump() const
{
printf("==================================\n");
printf("is_attacker: %d\n", static_cast<int>(is_attacker));
printf("is_poisoned: %d\n", static_cast<int>(is_poisoned));
printf("is_slowed: %d\n", static_cast<int>(is_slowed));
printf("slows: %d\n", static_cast<int>(slows));
printf("drains: %d\n", static_cast<int>(drains));
printf("stones: %d\n", static_cast<int>(stones));
printf("poisons: %d\n", static_cast<int>(poisons));
printf("backstab_pos: %d\n", static_cast<int>(backstab_pos));
printf("swarm: %d\n", static_cast<int>(swarm));
printf("rounds: %d\n", static_cast<int>(rounds));
printf("firststrike: %d\n", static_cast<int>(firststrike));
printf("\n");
printf("hp: %d\n", hp);
printf("max_hp: %d\n", max_hp);
printf("chance_to_hit: %d\n", chance_to_hit);
printf("damage: %d\n", damage);
printf("slow_damage: %d\n", slow_damage);
printf("num_blows: %d\n", num_blows);
printf("swarm_min: %d\n", swarm_min);
printf("swarm_max: %d\n", swarm_max);
printf("\n");
}
// 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);
}
static std::string unit_dump(std::pair< gamemap::location, unit > const &u)
{
std::stringstream s;
s << u.second.id() << " (" << u.first.x + 1 << ',' << u.first.y + 1 << ')';
return s.str();
}
void attack::fire_event(const std::string& n)
{
LOG_NG << "firing " << n << " event\n";
if(n == "attack_end") {
config dat;
dat.add_child("first");
dat.add_child("second");
if(a_ != units_.end()) {
(*(dat.child("first")))["weapon"]=a_stats_->weapon->id();
}
if(d_ != units_.end()) {
config *tempcfg = dat.child("second");
t_string d_weap = "none";
if(d_stats_->weapon != NULL) {
if(a_ != units_.end()) {
d_weap = d_stats_->weapon->id();
} else {
// The weapon choice will be invalid since the attacker was removed
d_weap = "invalid";
}
}
std::pair<std::string,t_string> to_insert("weapon", d_weap);
tempcfg->values.insert(to_insert);
}
game_events::fire(n,attacker_,defender_,dat);
a_ = units_.find(attacker_);
d_ = units_.find(defender_);
return;
}
const int defender_side = d_->second.side();
const int attacker_side = a_->second.side();
config dat;
dat.add_child("first");
dat.add_child("second");
(*(dat.child("first")))["weapon"]=a_stats_->weapon->id();
(*(dat.child("second")))["weapon"]=d_stats_->weapon != NULL ? d_stats_->weapon->id() : "none";
game_events::fire(n,attacker_,defender_,dat);
// The event could have killed either the attacker or
// defender, so we have to make sure they still exist
a_ = units_.find(attacker_);
d_ = units_.find(defender_);
//! @todo FIXME: If the event removes this attack, we should stop attacking.
// The previous code checked if 'attack_with' and 'defend_with'
// were still within the bounds of the attack arrays,
// or -1, but it was incorrect.
// The attack used could be removed and '*_with' variables
// could still be in bounds but point to a different attack.
if(a_ == units_.end() || d_ == units_.end()) {
if (update_display_){
recalculate_fog(map_,state_,info_,units_,teams_,attacker_side-1);
recalculate_fog(map_,state_,info_,units_,teams_,defender_side-1);
gui_.recalculate_minimap();
gui_.update_display();
}
fire_event("attack_end");
throw attack_end_exception();
}
}
void attack::refresh_bc()
{
a_ = units_.find(attacker_);
d_ = units_.find(defender_);
if(a_ == units_.end() || d_ == units_.end()) {
return;
}
*bc_ = battle_context(map_, teams_, units_, state_, info_, attacker_, defender_, attack_with_, defend_with_);
a_stats_ = &bc_->get_attacker_stats();
d_stats_ = &bc_->get_defender_stats();
attacker_cth_ = a_stats_->chance_to_hit;
defender_cth_ = d_stats_->chance_to_hit;
attacker_damage_ = a_stats_->damage;
defender_damage_ = d_stats_->damage;
}
attack::~attack()
{
delete bc_;
}
attack::attack(game_display& gui, const gamemap& map,
std::vector<team>& teams,
gamemap::location attacker,
gamemap::location defender,
int attack_with,
int defend_with,
unit_map& units,
const gamestatus& state,
const game_data& info,
bool update_display) : gui_(gui),map_(map),teams_(teams),
attacker_(attacker),defender_(defender),
attack_with_(attack_with),defend_with_(defend_with),
units_(units),state_(state),info_(info),
update_display_(update_display),OOS_error_(false),bc_(NULL)
{
// Stop the user from issuing any commands while the units are fighting
const events::command_disabler disable_commands;
a_ = units_.find(attacker);
d_ = units_.find(defender);
if(a_ == units_.end() || d_ == units_.end()) {
return;
}
// no attack weapon => stop here and don't attack
if (attack_with < 0) {
a_->second.set_attacks(a_->second.attacks_left()-1);
a_->second.set_movement(-1);
return;
}
a_->second.set_attacks(a_->second.attacks_left()-1);
a_->second.set_movement(a_->second.movement_left()-a_->second.attacks()[attack_with].movement_used());
a_->second.set_state("not_moved","");
d_->second.set_resting(false);
// If the attacker was invisible, she isn't anymore!
a_->second.set_state("hides","");
bc_ = new battle_context(map_, teams_, units_, state_, info_, attacker_, defender_, attack_with_, defend_with_);
a_stats_ = &bc_->get_attacker_stats();
d_stats_ = &bc_->get_defender_stats();
try {
fire_event("attack");
} catch (attack_end_exception) {
return;
}
refresh_bc();
LOG_NG << "getting attack statistics\n";
statistics::attack_context attack_stats(a_->second, d_->second, a_stats_->chance_to_hit, d_stats_->chance_to_hit);
orig_attacks_ = a_stats_->num_blows;
orig_defends_ = d_stats_->num_blows;
n_attacks_ = orig_attacks_;
n_defends_ = orig_defends_;
attackerxp_ = d_->second.level();
defenderxp_ = a_->second.level();
bool defender_strikes_first = (d_stats_->firststrike && ! a_stats_->firststrike);
unsigned int rounds = maximum<unsigned int>(a_stats_->rounds, d_stats_->rounds) - 1;
int abs_n_attack_ = 0;
int abs_n_defend_ = 0;
static const std::string poison_string("poison");
LOG_NG << "Fight: (" << attacker << ") vs (" << defender << ") 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";
while(n_attacks_ > 0 || n_defends_ > 0) {
LOG_NG << "start of attack loop...\n";
abs_n_attack_++;
if(n_attacks_ > 0 && defender_strikes_first == false) {
const int ran_num = get_random();
bool hits = (ran_num%100) < attacker_cth_;
int damage_defender_takes;
if(hits) {
damage_defender_takes = attacker_damage_;
} else {
damage_defender_takes = 0;
}
// Make sure that if we're serializing a game here,
// we got the same results as the game did originally.
const config* ran_results = get_random_results();
if(ran_results != NULL) {
const int results_chance = atoi((*ran_results)["chance"].c_str());
const bool results_hits = (*ran_results)["hits"] == "yes";
const int results_damage = atoi((*ran_results)["damage"].c_str());
if(results_chance != attacker_cth_) {
errbuf_ << "SYNC: In attack " << unit_dump(*a_) << " vs " << unit_dump(*d_)
<< ": chance to hit defender 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(hits != results_hits) {
errbuf_ << "SYNC: In attack " << unit_dump(*a_) << " vs " << unit_dump(*d_)
<< ": 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_defender_takes) {
errbuf_ << "SYNC: In attack " << unit_dump(*a_) << " vs " << unit_dump(*d_)
<< ": the data source says the hit did " << results_damage
<< " damage, while in-game calculations show the hit doing "
<< damage_defender_takes
<< " damage (over-riding game calculations with data source results)\n";
damage_defender_takes = results_damage;
OOS_error_ = true;
}
}
if(update_display_) {
std::string float_text = "";
if(hits) {
if (a_stats_->poisons &&
!utils::string_bool(d_->second.get_state("poisoned"))) {
float_text = float_text + _("poisoned") + "\n";
}
if(a_stats_->slows && !utils::string_bool(d_->second.get_state("slowed"))) {
float_text = float_text + _("slowed") + "\n";
}
// If the defender is turned to stone, the fight stops immediately
static const std::string stone_string("stone");
if (a_stats_->stones) {
float_text = float_text + _("stone") + "\n";
}
}
unit_display::unit_attack(attacker_,defender_,
damage_defender_takes,
*a_stats_->weapon,d_stats_->weapon,
abs_n_attack_,float_text);
}
bool dies = d_->second.take_hit(damage_defender_takes);
LOG_NG << "defender took " << damage_defender_takes << (dies ? " and died" : "") << "\n";
if(dies) {
unit_display::unit_die(defender_,d_->second,a_stats_->weapon,d_stats_->weapon, &(a_->second));
}
attack_stats.attack_result(hits ? (dies ? statistics::attack_context::KILLS : statistics::attack_context::HITS)
: statistics::attack_context::MISSES, attacker_damage_);
if(ran_results == NULL) {
log_scope2(engine, "setting random results");
config cfg;
cfg["hits"] = (hits ? "yes" : "no");
cfg["dies"] = (dies ? "yes" : "no");
cfg["damage"] = lexical_cast<std::string>(damage_defender_takes);
cfg["chance"] = lexical_cast<std::string>(attacker_cth_);
set_random_results(cfg);
} else {
const bool results_dies = (*ran_results)["dies"] == "yes";
if(results_dies != dies) {
errbuf_ << "SYNC: In attack " << unit_dump(*a_) << " vs " << unit_dump(*d_)
<< ": the data source the unit "
<< (results_dies ? "perished" : "survived")
<< " while in-game calculations show the unit "
<< (dies ? "perished" : "survived")
<< " (over-riding game calculations with data source results)\n";
dies = results_dies;
OOS_error_ = true;
}
}
if(hits) {
try {
fire_event("attacker_hits");
} catch (attack_end_exception) {
break;
}
refresh_bc();
} else {
try {
fire_event("attacker_misses");
} catch (attack_end_exception) {
break;
}
refresh_bc();
}
LOG_NG << "done attacking\n";
if(dies || hits) {
int amount_drained = a_stats_->drains ? attacker_damage_ / 2 : 0;
if(amount_drained > 0) {
char buf[50];
snprintf(buf,sizeof(buf),"%d",amount_drained);
if (update_display_){
gui_.float_label(a_->first,buf,0,255,0);
}
a_->second.heal(amount_drained);
}
}
if(dies) { // attacker kills defender
attackerxp_ = game_config::kill_experience*d_->second.level();
if(d_->second.level() == 0)
attackerxp_ = game_config::kill_experience/2;
a_->second.get_experience(attackerxp_);
attackerxp_ = defenderxp_ = 0;
gui_.invalidate(a_->first);
game_events::entity_location death_loc(d_);
game_events::entity_location attacker_loc(a_);
std::string undead_variation = d_->second.undead_variation();
const int defender_side = d_->second.side();
fire_event("attack_end");
game_events::fire("die",death_loc,attacker_loc);
d_ = units_.find(death_loc);
a_ = units_.find(attacker_loc);
if(d_ == units_.end() || !death_loc.matches_unit(d_->second)) {
// WML has invalidated the dying unit, abort
break;
} else if(d_->second.hitpoints() <= 0) {
units_.erase(d_);
d_ = units_.end();
}
if(a_ == units_.end() || !attacker_loc.matches_unit(a_->second)) {
// WML has invalidated the killing unit, abort
break;
}
refresh_bc();
if(a_stats_->plagues) {
// plague units make new units on the target hex
game_data::unit_type_map::const_iterator reanimitor;
LOG_NG<<"trying to reanimate "<<a_stats_->plague_type<<std::endl;
reanimitor = info_.unit_types.find(a_stats_->plague_type);
LOG_NG<<"found unit type:"<<reanimitor->second.id()<<std::endl;
if(reanimitor != info_.unit_types.end()) {
unit newunit=unit(&info_,&units_,&map_,&state_,&teams_,&reanimitor->second,a_->second.side(),true,true);
newunit.set_attacks(0);
// Apply variation
if(strcmp(undead_variation.c_str(),"null")) {
config mod;
config& variation=mod.add_child("effect");
variation["apply_to"]="variation";
variation["name"]=undead_variation;
newunit.add_modification("variation",mod);
newunit.heal_all();
}
units_.add(new std::pair<gamemap::location,unit>(death_loc,newunit));
preferences::encountered_units().insert(newunit.id());
if (update_display_){
gui_.invalidate(death_loc);
}
}
} else {
LOG_NG<<"unit not reanimated"<<std::endl;
}
if (update_display_){
recalculate_fog(map_,state_,info_,units_,teams_,defender_side-1);
gui_.invalidate_all();
gui_.recalculate_minimap();
gui_.draw();
}
break;
} else if(hits) {
if (a_stats_->poisons &&
!utils::string_bool(d_->second.get_state("poisoned"))) {
d_->second.set_state("poisoned","yes");
LOG_NG << "defender poisoned\n";
}
if(a_stats_->slows && !utils::string_bool(d_->second.get_state("slowed"))) {
d_->second.set_state("slowed","yes");
defender_damage_ = d_stats_->slow_damage;
LOG_NG << "defender slowed\n";
}
// If the defender is turned to stone, the fight stops immediately
static const std::string stone_string("stone");
if (a_stats_->stones) {
d_->second.set_state("stoned","yes");
n_defends_ = 0;
n_attacks_ = 0;
game_events::fire(stone_string,d_->first,a_->first);
}
}
--n_attacks_;
}
// If the defender got to strike first, they use it up here.
defender_strikes_first = false;
abs_n_defend_++;
if(n_defends_ > 0) {
LOG_NG << "doing defender attack...\n";
const int ran_num = get_random();
bool hits = (ran_num%100) < defender_cth_;
int damage_attacker_takes;
if(hits) {
damage_attacker_takes = defender_damage_;
} else {
damage_attacker_takes = 0;
}
// Make sure that if we're serializing a game here,
// we got the same results as the game did originally.
const config* ran_results = get_random_results();
if(ran_results != NULL) {
const int results_chance = atoi((*ran_results)["chance"].c_str());
const bool results_hits = (*ran_results)["hits"] == "yes";
const int results_damage = atoi((*ran_results)["damage"].c_str());
if(results_chance != defender_cth_) {
errbuf_ << "SYNC: In defend " << unit_dump(*a_) << " vs " << unit_dump(*d_)
<< ": chance to hit attacker is inconsistent. Data source: "
<< results_chance << "; Calculation: " << defender_cth_
<< " (over-riding game calculations with data source results)\n";
defender_cth_ = results_chance;
OOS_error_ = true;
}
if(hits != results_hits) {
errbuf_ << "SYNC: In defend " << unit_dump(*a_) << " vs " << unit_dump(*d_)
<< ": 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_attacker_takes) {
errbuf_ << "SYNC: In defend " << unit_dump(*a_) << " vs " << unit_dump(*d_)
<< ": the data source says the hit did " << results_damage
<< " damage, while in-game calculations show the hit doing "
<< damage_attacker_takes
<< " damage (over-riding game calculations with data source results)\n";
damage_attacker_takes = results_damage;
OOS_error_ = true;
}
}
if(update_display_) {
std::string float_text = "";
if(hits) {
if (d_stats_->poisons &&
!utils::string_bool(a_->second.get_state("poisoned"))) {
float_text = float_text + _("poisoned") + "\n";
}
if(d_stats_->slows && !utils::string_bool(a_->second.get_state("slowed"))) {
float_text = float_text + _("slowed") + "\n";
}
// If the defender is turned to stone, the fight stops immediately
static const std::string stone_string("stone");
if (d_stats_->stones) {
float_text = float_text + _("stone") + "\n";
}
}
unit_display::unit_attack(defender_,attacker_,
damage_attacker_takes,
*d_stats_->weapon,a_stats_->weapon,
abs_n_defend_,float_text);
}
bool dies = a_->second.take_hit(damage_attacker_takes);
LOG_NG << "attacker took " << damage_attacker_takes << (dies ? " and died" : "") << "\n";
if(dies) {
unit_display::unit_die(attacker_,a_->second,a_stats_->weapon,d_stats_->weapon, &(d_->second));
}
if(ran_results == NULL) {
config cfg;
cfg["hits"] = (hits ? "yes" : "no");
cfg["dies"] = (dies ? "yes" : "no");
cfg["damage"] = lexical_cast<std::string>(damage_attacker_takes);
cfg["chance"] = lexical_cast<std::string>(defender_cth_);
set_random_results(cfg);
} else {
const bool results_dies = (*ran_results)["dies"] == "yes";
if(results_dies != dies) {
errbuf_ << "SYNC: In defend " << unit_dump(*a_) << " vs " << unit_dump(*d_)
<< ": the data source the unit "
<< (results_dies ? "perished" : "survived")
<< " while in-game calculations show the unit "
<< (dies ? "perished" : "survived")
<< " (over-riding game calculations with data source results)\n";
dies = results_dies;
OOS_error_ = true;
}
}
if(hits) {
try {
fire_event("defender_hits");
} catch (attack_end_exception) {
break;
}
refresh_bc();
} else {
try {
fire_event("defender_misses");
} catch (attack_end_exception) {
break;
}
refresh_bc();
}
attack_stats.defend_result(hits ? (dies ? statistics::attack_context::KILLS : statistics::attack_context::HITS)
: statistics::attack_context::MISSES, defender_damage_);
if(hits || dies){
int amount_drained = d_stats_->drains ? defender_damage_ / 2 : 0;
if(amount_drained > 0) {
char buf[50];
snprintf(buf,sizeof(buf),"%d",amount_drained);
if (update_display_){
gui_.float_label(d_->first,buf,0,255,0);
}
d_->second.heal(amount_drained);
}
}
if(dies) { // defender kills attacker
defenderxp_ = game_config::kill_experience*a_->second.level();
if(a_->second.level() == 0)
defenderxp_ = game_config::kill_experience/2;
d_->second.get_experience(defenderxp_);
attackerxp_ = defenderxp_ = 0;
gui_.invalidate(d_->first);
std::string undead_variation = a_->second.undead_variation();
game_events::entity_location death_loc(a_);
game_events::entity_location defender_loc(d_);
const int attacker_side = a_->second.side();
fire_event("attack_end");
game_events::fire("die",death_loc,defender_loc);
refresh_bc();
if(a_ == units_.end() || !death_loc.matches_unit(a_->second)) {
// WML has invalidated the dying unit, abort
break;
} else if(a_->second.hitpoints() <= 0) {
units_.erase(a_);
a_ = units_.end();
}
if(d_ == units_.end() || !defender_loc.matches_unit(d_->second)) {
// WML has invalidated the killing unit, abort
break;
} else if(d_stats_->plagues) {
// plague units make new units on the target hex.
game_data::unit_type_map::const_iterator reanimitor;
LOG_NG<<"trying to reanimate "<<d_stats_->plague_type<<std::endl;
reanimitor = info_.unit_types.find(d_stats_->plague_type);
LOG_NG<<"found unit type:"<<reanimitor->second.id()<<std::endl;
if(reanimitor != info_.unit_types.end()) {
unit newunit=unit(&info_,&units_,&map_,&state_,&teams_,&reanimitor->second,d_->second.side(),true,true);
// Apply variation
if(strcmp(undead_variation.c_str(),"null")){
config mod;
config& variation=mod.add_child("effect");
variation["apply_to"]="variation";
variation["name"]=undead_variation;
newunit.add_modification("variation",mod);
}
units_.add(new std::pair<gamemap::location,unit>(death_loc,newunit));
if (update_display_){
gui_.invalidate(death_loc);
}
}
} else {
LOG_NG<<"unit not reanimated"<<std::endl;
}
if (update_display_){
recalculate_fog(map_,state_,info_,units_,teams_,attacker_side-1);
gui_.invalidate_all();
gui_.recalculate_minimap();
gui_.draw();
}
break;
} else if(hits) {
if (d_stats_->poisons &&
!utils::string_bool(a_->second.get_state("poisoned"))) {
a_->second.set_state("poisoned","yes");
LOG_NG << "attacker poisoned\n";
}
if(d_stats_->slows && !utils::string_bool(a_->second.get_state("slowed"))) {
a_->second.set_state("slowed","yes");
attacker_damage_ = a_stats_->slow_damage;
LOG_NG << "attacker slowed\n";
}
// If the attacker is turned to stone, the fight stops immediately
static const std::string stone_string("stone");
if (d_stats_->stones) {
a_->second.set_state("stoned","yes");
n_defends_ = 0;
n_attacks_ = 0;
game_events::fire(stone_string,a_->first,d_->first);
}
}
--n_defends_;
}
// Continue the fight to death; if one of the units got stoned,
// either n_attacks or n_defends is -1
if(rounds > 0 && n_defends_ == 0 && n_attacks_ == 0) {
n_attacks_ = orig_attacks_;
n_defends_ = orig_defends_;
--rounds;
defender_strikes_first = (d_stats_->firststrike && ! a_stats_->firststrike);
}
if(n_attacks_ <= 0 && n_defends_ <= 0) {
fire_event("attack_end");
}
}
if(a_ != units.end()) {
a_->second.set_standing(gui_,a_->first);
if(attackerxp_)
a_->second.get_experience(attackerxp_);
}
if(d_ != units.end()) {
d_->second.set_standing(gui_,d_->first);
if(defenderxp_)
d_->second.get_experience(defenderxp_);
}
if (update_display_){
gui_.invalidate_unit();
gui_.invalidate(attacker);
gui_.invalidate(defender);
gui_.draw(true,true);
}
if(OOS_error_) {
replay::throw_error(errbuf_.str());
}
}
int village_owner(const gamemap::location& loc, const std::vector<team>& teams)
{
for(size_t i = 0; i != teams.size(); ++i) {
if(teams[i].owns_village(loc))
return i;
}
return -1;
}
bool get_village(const gamemap::location& loc, game_display& disp,
std::vector<team>& teams, size_t team_num,
const unit_map& units, int *action_timebonus)
{
if(team_num < teams.size() && teams[team_num].owns_village(loc)) {
return false;
}
const bool has_leader = find_leader(units,int(team_num+1)) != units.end();
bool grants_timebonus = false;
// We strip the village off all other sides, unless it is held by an ally
// and we don't have a leader (and thus can't occupy it)
for(std::vector<team>::iterator i = teams.begin(); i != teams.end(); ++i) {
const unsigned int side = i - teams.begin() + 1;
if(team_num >= teams.size() || has_leader || teams[team_num].is_enemy(side)) {
i->lose_village(loc);
if(team_num + 1 != side && action_timebonus) {
grants_timebonus = true;
}
}
}
if(grants_timebonus) {
teams[team_num].set_action_bonus_count(1 + teams[team_num].action_bonus_count());
*action_timebonus = 1;
}
if(team_num >= teams.size()) {
return false;
}
if(has_leader) {
disp.invalidate(loc);
return teams[team_num].get_village(loc);
}
return false;
}
unit_map::iterator find_leader(unit_map& units, int side)
{
for(unit_map::iterator i = units.begin(); i != units.end(); ++i) {
if(static_cast<int>(i->second.side()) == side && i->second.can_recruit())
return i;
}
return units.end();
}
unit_map::const_iterator find_leader(const unit_map& units, int side)
{
for(unit_map::const_iterator i = units.begin(); i != units.end(); ++i) {
if(static_cast<int>(i->second.side()) == side && i->second.can_recruit())
return i;
}
return units.end();
}
// Simple algorithm: no maximum number of patients per healer.
void reset_resting(unit_map& units, unsigned int side)
{
for (unit_map::iterator i = units.begin(); i != units.end(); ++i) {
if (i->second.side() == side)
i->second.set_resting(true);
}
}
void calculate_healing(game_display& disp, const gamemap& map,
unit_map& units, unsigned int side,
const std::vector<team>& teams, bool update_display)
{
// We look for all allied units, then we see if our healer is near them.
for (unit_map::iterator i = units.begin(); i != units.end(); ++i) {
if (!utils::string_bool(i->second.get_state("healable"),true))
continue;
unit_map::iterator curer = units.end();
std::vector<unit_map::iterator> healers;
int healing = 0;
std::string curing;
unit_ability_list heal = i->second.get_abilities("heals",i->first);
const bool is_poisoned = utils::string_bool(i->second.get_state("poisoned"));
if(is_poisoned) {
// Remove the enemies' healers to determine if poison is slowed or cured
for(std::vector<std::pair<config*,gamemap::location> >::iterator h_it = heal.cfgs.begin(); h_it != heal.cfgs.end();) {
unit_map::iterator potential_healer = units.find(h_it->second);
wassert(potential_healer != units.end());
if(teams[potential_healer->second.side()-1].is_enemy(side)) {
h_it = heal.cfgs.erase(h_it);
} else {
++h_it;
}
}
for(std::vector<std::pair<config*,gamemap::location> >::const_iterator heal_it = heal.cfgs.begin(); heal_it != heal.cfgs.end(); ++heal_it) {
if((*heal_it->first)["poison"] == "cured") {
curer = units.find(heal_it->second);
// Full curing only occurs on the healer turn (may be changed)
if(curer->second.side() == side) {
curing = "cured";
} else if(curing != "cured") {
curing = "slowed";
}
} else if(curing != "cured" && (*heal_it->first)["poison"] == "slowed") {
curer = units.find(heal_it->second);
curing = "slowed";
}
}
}
// For heal amounts, only consider healers on side which is starting now.
// Remove all healers not on this side.
for(std::vector<std::pair<config*,gamemap::location> >::iterator h_it = heal.cfgs.begin(); h_it != heal.cfgs.end();) {
unit_map::iterator potential_healer = units.find(h_it->second);
wassert(potential_healer != units.end());
if(potential_healer->second.side() != side) {
h_it = heal.cfgs.erase(h_it);
} else {
++h_it;
}
}
unit_abilities::effect heal_effect(heal,0,false);
healing = heal_effect.get_composite_value();
for(std::vector<unit_abilities::individual_effect>::const_iterator heal_loc = heal_effect.begin(); heal_loc != heal_effect.end(); ++heal_loc) {
healers.push_back(units.find(heal_loc->loc));
}
if(i->second.side() == side) {
unit_ability_list regen = i->second.get_abilities("regenerate",i->first);
unit_abilities::effect regen_effect(regen,0,false);
if(regen_effect.get_composite_value() > healing) {
healing = regen_effect.get_composite_value();
healers.clear();
}
if(regen.cfgs.size()) {
for(std::vector<std::pair<config*,gamemap::location> >::const_iterator regen_it = regen.cfgs.begin(); regen_it != regen.cfgs.end(); ++regen_it) {
if((*regen_it->first)["poison"] == "cured") {
curer = units.end();
curing = "cured";
} else if(curing != "cured" && (*regen_it->first)["poison"] == "slowed") {
curer = units.end();
curing = "slowed";
}
}
}
if (map.gives_healing(i->first)) {
if(map.gives_healing(i->first) > healing) {
healing = map.gives_healing(i->first);
healers.clear();
}
//! @todo FIXME
curing = "cured";
curer = units.end();
}
if(i->second.resting()) {
healing += game_config::rest_heal_amount;
}
}
if(is_poisoned) {
if(curing == "cured") {
i->second.set_state("poisoned","");
healing = 0;
healers.clear();
healers.push_back(curer);
} else if(curing == "slowed") {
healing = 0;
healers.clear();
healers.push_back(curer);
} else {
healers.clear();
healing = 0;
if(i->second.side() == side) {
healing = -game_config::poison_amount;
}
}
}
if (curing == "" && healing==0) {
continue;
}
int pos_max = i->second.max_hitpoints() - i->second.hitpoints();
int neg_max = -(i->second.hitpoints() - 1);
if(healing > 0 && pos_max <= 0) {
// Do not try to "heal" if HP >= max HP
continue;
}
if(healing > pos_max) {
healing = pos_max;
} else if(healing < neg_max) {
healing = neg_max;
}
if ( !recorder.is_skipping()
&& update_display
&& !(i->second.invisible(i->first,units,teams) &&
teams[disp.viewing_team()].is_enemy(side))) {
unit_display::unit_healing(i->second,i->first,healers,healing);
}
if (healing > 0)
i->second.heal(healing);
else if (healing < 0)
i->second.take_hit(-healing);
}
}
unit get_advanced_unit(const game_data& info,
unit_map& units,
const gamemap::location& loc, const std::string& advance_to)
{
const std::map<std::string,unit_type>::const_iterator new_type = info.unit_types.find(advance_to);
const unit_map::iterator un = units.find(loc);
if(new_type != info.unit_types.end() && un != units.end()) {
unit new_unit(un->second);
new_unit.get_experience(-new_unit.max_experience());
new_unit.advance_to(&(new_type->second));
return new_unit;
} else {
throw game::game_error("Could not find the unit being advanced"
" to: " + advance_to);
}
}
void advance_unit(const game_data& info,
unit_map& units,
gamemap::location loc, const std::string& advance_to)
{
if(units.count(loc) == 0) {
return;
}
const unit& new_unit = get_advanced_unit(info,units,loc,advance_to);
LOG_NG << "firing advance event\n";
game_events::fire("advance",loc);
statistics::advance_unit(new_unit);
preferences::encountered_units().insert(new_unit.id());
LOG_STREAM(info, config) << "Added '" << new_unit.id() << "' to encountered units\n";
units.replace(new std::pair<gamemap::location,unit>(loc,new_unit));
LOG_NG << "firing post_advance event\n";
game_events::fire("post_advance",loc);
}
void check_victory(unit_map& units, std::vector<team>& teams)
{
std::vector<int> seen_leaders;
for(unit_map::const_iterator i = units.begin();
i != units.end(); ++i) {
if(i->second.can_recruit()) {
LOG_NG << "seen leader for side " << i->second.side() << "\n";
seen_leaders.push_back(i->second.side());
}
}
// Clear villages for teams that have no leader
for(std::vector<team>::iterator tm = teams.begin(); tm != teams.end(); ++tm) {
if(std::find(seen_leaders.begin(),seen_leaders.end(),tm-teams.begin() + 1) == seen_leaders.end()) {
tm->clear_villages();
}
}
bool found_enemies = false;
bool found_player = false;
for(size_t n = 0; n != seen_leaders.size(); ++n) {
const size_t side = seen_leaders[n]-1;
wassert(side < teams.size());
for(size_t m = n+1; m != seen_leaders.size(); ++m) {
if(side < teams.size() && teams[side].is_enemy(seen_leaders[m])) {
found_enemies = true;
}
}
if (teams[side].is_human() || teams[side].is_persistent()) {
found_player = true;
}
}
if(found_enemies == false) {
if (found_player) {
game_events::fire("enemies defeated");
}
if (victory_conditions::victory_when_enemies_defeated() == false
&& (found_player || is_observer())){
// This level has asked not to be ended by this condition.
return;
}
if(non_interactive()) {
std::cout << "winner: ";
for(std::vector<int>::const_iterator i = seen_leaders.begin(); i != seen_leaders.end(); ++i) {
std::string ai = teams[*i - 1].ai_algorithm();
if (ai == "") ai = "default ai";
std::cout << *i << " (using " << ai << ") ";
}
std::cout << "\n";
}
LOG_NG << "throwing end level exception...\n";
throw end_level_exception(found_player ? VICTORY : DEFEAT);
}
}
time_of_day timeofday_at(const gamestatus& status,const unit_map& units,const gamemap::location& loc, const gamemap& map)
{
int lighten = maximum<int>(map.get_terrain_info(map.get_terrain(loc)).light_modification() , 0);
int darken = minimum<int>(map.get_terrain_info(map.get_terrain(loc)).light_modification() , 0);
time_of_day tod = status.get_time_of_day(lighten + darken,loc);
if(loc.valid()) {
gamemap::location locs[7];
locs[0] = loc;
get_adjacent_tiles(loc,locs+1);
for(int i = 0; i != 7; ++i) {
const unit_map::const_iterator itor = units.find(locs[i]);
if(itor != units.end() &&
itor->second.get_ability_bool("illuminates",itor->first) && !itor->second.incapacitated()) {
unit_ability_list illum = itor->second.get_abilities("illuminates",itor->first);
unit_abilities::effect illum_effect(illum,lighten,false);
int mod = illum_effect.get_composite_value();
if(mod + tod.lawful_bonus > illum.highest("max_value").first) {
mod = illum.highest("max_value").first - tod.lawful_bonus;
}
lighten = maximum<int>(mod, lighten);
darken = minimum<int>(mod, darken);
}
}
}
tod = status.get_time_of_day(lighten + darken,loc);
return tod;
}
int combat_modifier(const gamestatus& status,
const unit_map& units,
const gamemap::location& loc,
unit_type::ALIGNMENT alignment,
bool is_fearless,
const gamemap& map)
{
const time_of_day& tod = timeofday_at(status,units,loc,map);
int bonus = tod.lawful_bonus;
if(alignment == unit_type::NEUTRAL)
bonus = 0;
else if(alignment == unit_type::CHAOTIC)
bonus = -bonus;
if(is_fearless)
bonus = maximum<int>(bonus, 0);
return bonus;
}
namespace {
bool clear_shroud_loc(const gamemap& map, team& tm,
const gamemap::location& loc,
std::vector<gamemap::location>* cleared)
{
bool result = false;
gamemap::location adj[7];
get_adjacent_tiles(loc,adj);
adj[6] = loc;
for(int i = 0; i != 7; ++i) {
// We clear one past the edge of the board, so that the half-hexes
// at the edge can also be cleared of fog/shroud.
if(map.on_board(adj[i]) || map.on_board(loc)) {
const bool res = tm.clear_shroud(adj[i].x,adj[i].y) ||
tm.clear_fog(adj[i].x,adj[i].y);
if(res && cleared != NULL) {
cleared->push_back(adj[i]);
}
result |= res;
}
}
return result;
}
//! Returns true iff some shroud is cleared.
//! seen_units will return new units that have been seen by this unit.
//! If known_units is NULL, seen_units can be NULL and will not be changed.
bool clear_shroud_unit(const gamemap& map,
const gamestatus& status,
const game_data& gamedata,
const unit_map& units, const gamemap::location& loc,
std::vector<team>& teams, int team,
const std::set<gamemap::location>* known_units,
std::set<gamemap::location>* seen_units)
{
std::vector<gamemap::location> cleared_locations;
const unit_map::const_iterator u = units.find(loc);
if(u == units.end()) {
return false;
}
unit_map temp_units(u->first, u->second);
paths p(map,status,gamedata,temp_units,loc,teams,true,false,teams[team]);
for(paths::routes_map::const_iterator i = p.routes.begin();
i != p.routes.end(); ++i) {
clear_shroud_loc(map,teams[team],i->first,&cleared_locations);
}
// Clear the location the unit is at
clear_shroud_loc(map,teams[team],loc,&cleared_locations);
// Remove all redundant location.
// If a unit is on this location, the sighed event is called twice.
std::unique(cleared_locations.begin(),cleared_locations.end());
for(std::vector<gamemap::location>::const_iterator it =
cleared_locations.begin(); it != cleared_locations.end(); ++it) {
const unit_map::const_iterator sighted = units.find(*it);
if(sighted != units.end() &&
(sighted->second.invisible(*it,units,teams) == false
|| teams[team].is_enemy(sighted->second.side()) == false)) {
if(!(seen_units == NULL || known_units == NULL) && known_units->count(*it) == 0) {
if (!utils::string_bool(sighted->second.get_state("stoned")))
{
seen_units->insert(*it);
}
if ( teams[team].uses_shroud() || teams[team].uses_fog())
{
static const std::string sighted_str("sighted");
game_events::fire(sighted_str,*it,loc);
}
}
}
}
return cleared_locations.empty() == false;
}
}
void recalculate_fog(const gamemap& map, const gamestatus& status,
const game_data& gamedata, unit_map& units,
std::vector<team>& teams, int team) {
teams[team].refog();
for(unit_map::iterator i = units.begin(); i != units.end(); ++i) {
if(static_cast<int>(i->second.side()) == team + 1) {
const unit_movement_resetter move_resetter(i->second);
clear_shroud_unit(map,status,gamedata,units,i->first,teams,team,NULL,NULL);
}
}
game_events::pump();
}
bool clear_shroud(game_display& disp, const gamestatus& status,
const gamemap& map, const game_data& gamedata,
unit_map& units, std::vector<team>& teams, int team)
{
if(teams[team].uses_shroud() == false && teams[team].uses_fog() == false)
return false;
bool result = false;
unit_map::iterator i;
for(i = units.begin(); i != units.end(); ++i) {
if(static_cast<int>(i->second.side()) == team + 1) {
const unit_movement_resetter move_resetter(i->second);
result |= clear_shroud_unit(map,status,gamedata,units,i->first,teams,team,NULL,NULL);
}
}
game_events::pump();
if (teams[team].uses_fog()) {
recalculate_fog(map,status,gamedata,units,teams,team);
}
disp.labels().recalculate_shroud();
return result;
}
size_t move_unit(game_display* disp, const game_data& gamedata,
const gamestatus& status, const gamemap& map,
unit_map& units, std::vector<team>& teams,
std::vector<gamemap::location> route,
replay* move_recorder, undo_list* undo_stack,
gamemap::location *next_unit, bool continue_move, bool should_clear_shroud)
{
wassert(route.empty() == false);
// Stop the user from issuing any commands while the unit is moving
const events::command_disabler disable_commands;
unit_map::iterator ui = units.find(route.front());
wassert(ui != units.end());
ui->second.set_goto(gamemap::location());
const size_t team_num = ui->second.side()-1;
team& team = teams[team_num];
const bool check_shroud = should_clear_shroud && team.auto_shroud_updates() &&
(team.uses_shroud() || team.uses_fog());
std::set<gamemap::location> known_units;
if(check_shroud) {
for(unit_map::const_iterator u = units.begin(); u != units.end(); ++u) {
if(team.fogged(u->first.x,u->first.y) == false) {
known_units.insert(u->first);
team.see(u->second.side()-1);
}
}
}
// See how far along the given path we can move.
const int starting_moves = ui->second.movement_left();
int moves_left = starting_moves;
std::set<gamemap::location> seen_units;
bool discovered_unit = false;
bool should_clear_stack = false;
std::vector<gamemap::location>::const_iterator step;
std::string ambushed_string;
for(step = route.begin()+1; step != route.end(); ++step) {
const t_translation::t_letter terrain = map[*step];
const unit_map::const_iterator enemy_unit = units.find(*step);
const bool skirmisher = ui->second.get_ability_bool("skirmisher",*step);
const int mv = ui->second.movement_cost(terrain);
if(discovered_unit || continue_move == false && seen_units.empty() == false ||
mv > moves_left || enemy_unit != units.end() && team.is_enemy(enemy_unit->second.side())) {
break;
} else {
moves_left -= mv;
}
if(!skirmisher && enemy_zoc(map,units,teams,*step,team,ui->second.side())) {
moves_left = 0;
}
// If we use fog or shroud, see if we have sighted an enemy unit,
// in which case we should stop immediately.
// Cannot use check shroud, because also need to check if delay shroud is on.
if(should_clear_shroud && (team.uses_shroud() || team.uses_fog())) {
if(units.count(*step) == 0 && !map.is_village(*step)) {
LOG_NG << "checking for units from " << (step->x+1) << "," << (step->y+1) << "\n";
// Temporarily reset the unit's moves to full
const unit_movement_resetter move_resetter(ui->second);
// We have to swap out any unit that is already in the hex,
// so we can put our unit there, then we'll swap back at the end.
const temporary_unit_placer unit_placer(units,*step,ui->second);
if( team.auto_shroud_updates()) {
should_clear_stack |= clear_shroud_unit(map,status,gamedata,units,*step,teams,
ui->second.side()-1,&known_units,&seen_units);
} else {
clear_shroud_unit(map,status,gamedata,units,*step,teams,
ui->second.side()-1,&known_units,&seen_units);
}
if(should_clear_stack) {
disp->invalidate_all();
}
}
}
// Check to see if the unit was deleted
// during a sighted event in clear_shroud_unit()
ui = units.find(route.front());
if(ui == units.end()) {
//! @todo FIXME: the correct behavior for sighted event would be
// to halt movement, then fire "sighted" after firing "moveto" (see below).
// However, since we have fired "sighted" during movement calculations
// this is a workaround to prevent a crash.
if(move_recorder != NULL) {
move_recorder->add_movement(route.front(),*step);
}
return (step - route.begin());
}
// Check if we have discovered an invisible enemy unit
gamemap::location adjacent[6];
get_adjacent_tiles(*step,adjacent);
for(int i = 0; i != 6; ++i) {
// Check if we are checking ourselves
if(adjacent[i] == ui->first)
continue;
const unit_map::const_iterator it = units.find(adjacent[i]);
if(it != units.end() && teams[ui->second.side()-1].is_enemy(it->second.side()) &&
it->second.invisible(it->first,units,teams)) {
discovered_unit = true;
unit_ability_list hides = it->second.get_abilities("hides",it->first);
for(std::vector<std::pair<config*,gamemap::location> >::const_iterator hide_it = hides.cfgs.begin();
hide_it != hides.cfgs.end(); ++hide_it) {
ambushed_string =(*hide_it->first)["alert"];
}
should_clear_stack = true;
moves_left = 0;
break;
}
}
}
// Make sure we don't tread on another unit.
std::vector<gamemap::location>::const_iterator begin = route.begin();
std::vector<gamemap::location> steps(begin,step);
while (!steps.empty()) {
gamemap::location const &loc = steps.back();
if (units.count(loc) == 0)
break;
moves_left += ui->second.movement_cost(map[loc]);
steps.pop_back();
}
wassert(steps.size() <= route.size());
// If we can't get all the way there and have to set a go-to.
if(steps.size() != route.size() && discovered_unit == false) {
if(seen_units.empty() == false) {
ui->second.set_interrupted_move(route.back());
} else {
ui->second.set_goto(route.back());
}
} else {
ui->second.set_interrupted_move(gamemap::location());
}
if(steps.size() < 2) {
return 0;
}
if (next_unit != NULL)
*next_unit = steps.back();
// Move the unit on the screen. Hide the unit in its current location,
// but don't actually remove it until the move is done,
// so that while the unit is moving status etc.
// will still display the correct number of units.
unit_display::move_unit(map,steps,ui->second,teams);
ui->second.set_movement(moves_left);
std::pair<gamemap::location,unit> *p = units.extract(ui->first);
p->first = steps.back();
units.add(p);
ui = units.find(p->first);
if(move_recorder != NULL) {
move_recorder->add_movement(steps.front(),steps.back());
}
bool event_mutated = false;
int orig_village_owner = -1;
int action_time_bonus = 0;
if(map.is_village(steps.back())) {
orig_village_owner = village_owner(steps.back(),teams);
if(size_t(orig_village_owner) != team_num) {
ui->second.set_movement(0);
event_mutated = get_village(steps.back(),*disp,teams,team_num,units,&action_time_bonus);
}
}
if(disp != NULL) {
// Clear display helpers before firing events
disp->unhighlight_reach();
disp->set_route(NULL);
disp->draw();
}
if(game_events::fire("moveto",steps.back())) {
event_mutated = true;
}
ui = units.find(steps.back());
if(undo_stack != NULL) {
if(event_mutated || should_clear_stack || ui == units.end()) {
apply_shroud_changes(*undo_stack,disp,status,map,gamedata,units,teams,team_num);
undo_stack->clear();
} else {
// MP_COUNTDOWN: added param
undo_stack->push_back(undo_action(ui->second,steps,starting_moves,action_time_bonus,orig_village_owner));
}
}
if(disp != NULL) {
// Show messages on the screen here
if(discovered_unit) {
if (ambushed_string.empty())
ambushed_string = _("Ambushed!");
// We've been ambushed, display an appropriate message
disp->announce(ambushed_string, font::BAD_COLOUR);
}
if(continue_move == false && seen_units.empty() == false) {
// The message depends on how many units have been sighted,
// and whether they are allies or enemies, so calculate that out here
int nfriends = 0, nenemies = 0;
for(std::set<gamemap::location>::const_iterator i = seen_units.begin(); i != seen_units.end(); ++i) {
LOG_NG << "processing unit at " << (i->x+1) << "," << (i->y+1) << "\n";
const unit_map::const_iterator u = units.find(*i);
// Unit may have been removed by an event.
if(u == units.end()) {
LOG_NG << "was removed\n";
continue;
}
if(team.is_enemy(u->second.side())) {
++nenemies;
} else {
++nfriends;
}
LOG_NG << "processed...\n";
teams[team_num].see(u->second.side()-1);
}
// The message we display is different depending on
// whether the units sighted were enemies or friends,
// and their respective number.
utils::string_map symbols;
symbols["friends"] = lexical_cast<std::string>(nfriends);
symbols["enemies"] = lexical_cast<std::string>(nenemies);
std::string message;
SDL_Color msg_colour;
if(nfriends == 0 || nenemies == 0) {
if(nfriends > 0) {
message = vngettext("Friendly unit sighted", "$friends friendly units sighted", nfriends, symbols);
msg_colour = font::GOOD_COLOUR;
} else if(nenemies > 0) {
message = vngettext("Enemy unit sighted!", "$enemies enemy units sighted!", nenemies, symbols);
msg_colour = font::BAD_COLOUR;
}
}
else {
symbols["friendphrase"] = vngettext("Part of 'Units sighted! (...)' sentence^1 friendly", "$friends friendly", nfriends, symbols);
symbols["enemyphrase"] = vngettext("Part of 'Units sighted! (...)' sentence^1 enemy", "$enemies enemy", nenemies, symbols);
message = vgettext("Units sighted! ($friendphrase, $enemyphrase)", symbols);
msg_colour = font::NORMAL_COLOUR;
}
if(steps.size() < route.size()) {
// See if the "Continue Move" action has an associated hotkey
const hotkey::hotkey_item& hk = hotkey::get_hotkey(hotkey::HOTKEY_CONTINUE_MOVE);
if(!hk.null()) {
symbols["hotkey"] = hk.get_name();
message += "\n" + vgettext("(press $hotkey to keep moving)", symbols);
}
}
disp->announce(message, msg_colour);
}
disp->draw();
disp->recalculate_minimap();
}
wassert(steps.size() <= route.size());
return steps.size();
}
bool unit_can_move(const gamemap::location& loc, const unit_map& units,
const gamemap& map, const std::vector<team>& teams)
{
const unit_map::const_iterator u_it = units.find(loc);
wassert(u_it != units.end());
const unit& u = u_it->second;
const team& current_team = teams[u.side()-1];
if(!u.attacks_left() && u.movement_left()==0)
return false;
// Units with goto commands that have already done their gotos this turn
// (i.e. don't have full movement left) should have red globes.
if(u.has_moved() && u.has_goto()) {
return false;
}
gamemap::location locs[6];
get_adjacent_tiles(loc,locs);
for(int n = 0; n != 6; ++n) {
if(map.on_board(locs[n])) {
const unit_map::const_iterator i = units.find(locs[n]);
if(i != units.end()) {
if(!i->second.incapacitated() && current_team.is_enemy(i->second.side())) {
return true;
}
}
if(u.movement_cost(map[locs[n]]) <= u.movement_left()) {
return true;
}
}
}
return false;
}
void apply_shroud_changes(undo_list& undos, game_display* disp, const gamestatus& status, const gamemap& map,
const game_data& gamedata, unit_map& units, std::vector<team>& teams, int team)
{
// No need to do this if the team isn't using fog or shroud.
if(!teams[team].uses_shroud() && !teams[team].uses_fog())
return;
/*
This function works thusly:
1. run through the list of undo_actions
2. for each one, play back the unit's move
3. for each location along the route, clear any "shrouded" squares that the unit can see
4. clear the undo_list
5. call clear_shroud to update the fog of war for each unit.
6. fix up associated display stuff (done in a similar way to turn_info::undo())
*/
for(undo_list::iterator un = undos.begin(); un != undos.end(); ++un) {
std::cout << "Turning an undo...\n";
if(un->is_recall() || un->is_recruit()) continue;
// We're not really going to mutate the unit, just temporarily
// set its moves to maximum, but then switch them back.
const unit_movement_resetter move_resetter(un->affected_unit);
std::vector<gamemap::location>::const_iterator step;
for(step = un->route.begin(); step != un->route.end(); ++step) {
// We have to swap out any unit that is already in the hex,
// so we can put our unit there, then we'll swap back at the end.
const temporary_unit_placer unit_placer(units,*step,un->affected_unit);
clear_shroud_unit(map,status,gamedata,units,*step,teams,team,NULL,NULL);
//! @todo FIXME
// There is potential for a bug, here. If the "sighted"
// events, raised by the clear_shroud_unit function,
// loops on all units, changing them all, the unit which
// was swapped by the temporary unit placer will not be
// affected. However, if we place the pump() out of the
// temporary_unit_placer scope, the "sighted" event will
// be raised with an invalid source unit, which is even
// worse.
game_events::pump();
}
}
if(disp != NULL) {
disp->invalidate_unit();
disp->invalidate_game_status();
clear_shroud(*disp,status,map,gamedata,units,teams,team);
disp->recalculate_minimap();
disp->unhighlight_reach();
disp->set_route(NULL);
disp->invalidate_all();
} else {
recalculate_fog(map,status,gamedata,units,teams,team);
}
}
bool backstab_check(const gamemap::location& attacker_loc,
const gamemap::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
gamemap::location adj[6];
get_adjacent_tiles(defender_loc, adj);
int i;
for(i = 0; i != 6; ++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]);
if(opp == units.end()) return false; // No opposite unit
if(opp->second.incapacitated()) return false;
if(size_t(defender->second.side()-1) >= teams.size() ||
size_t(opp->second.side()-1) >= teams.size())
return true; // If sides aren't valid teams, then they are enemies
if(teams[defender->second.side()-1].is_enemy(opp->second.side()))
return true; // Defender and opposite are enemies
return false; // Defender and opposite are friends
}