/* $Id$ */ /* Copyright (C) 2003 - 2007 by David White 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(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(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& 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(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(attacker.attacks().size())); adef = &attacker.attacks()[attacker_weapon]; } if (defender_weapon >= 0) { wassert(defender_weapon < static_cast(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& 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 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(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(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(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& 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 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& 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(is_attacker)); printf("is_poisoned: %d\n", static_cast(is_poisoned)); printf("is_slowed: %d\n", static_cast(is_slowed)); printf("slows: %d\n", static_cast(slows)); printf("drains: %d\n", static_cast(drains)); printf("stones: %d\n", static_cast(stones)); printf("poisons: %d\n", static_cast(poisons)); printf("backstab_pos: %d\n", static_cast(backstab_pos)); printf("swarm: %d\n", static_cast(swarm)); printf("rounds: %d\n", static_cast(rounds)); printf("firststrike: %d\n", static_cast(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 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& 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(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(damage_defender_takes); cfg["chance"] = lexical_cast(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 "<plague_type<plague_type); LOG_NG<<"found unit type:"<second.id()<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(death_loc,newunit)); preferences::encountered_units().insert(newunit.id()); if (update_display_){ gui_.invalidate(death_loc); } } } else { LOG_NG<<"unit not reanimated"<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(damage_attacker_takes); cfg["chance"] = lexical_cast(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 "<plague_type<plague_type); LOG_NG<<"found unit type:"<second.id()<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(death_loc,newunit)); if (update_display_){ gui_.invalidate(death_loc); } } } else { LOG_NG<<"unit not reanimated"<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& 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& 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::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(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(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& 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 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 >::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 >::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 >::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::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 >::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::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(loc,new_unit)); LOG_NG << "firing post_advance event\n"; game_events::fire("post_advance",loc); } void check_victory(unit_map& units, std::vector& teams) { std::vector 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::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::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(map.get_terrain_info(map.get_terrain(loc)).light_modification() , 0); int darken = minimum(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(mod, lighten); darken = minimum(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(bonus, 0); return bonus; } namespace { bool clear_shroud_loc(const gamemap& map, team& tm, const gamemap::location& loc, std::vector* 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& teams, int team, const std::set* known_units, std::set* seen_units) { std::vector 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::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& teams, int team) { teams[team].refog(); for(unit_map::iterator i = units.begin(); i != units.end(); ++i) { if(static_cast(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& 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(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& teams, std::vector 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 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 seen_units; bool discovered_unit = false; bool should_clear_stack = false; std::vector::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 >::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::const_iterator begin = route.begin(); std::vector 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 *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::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(nfriends); symbols["enemies"] = lexical_cast(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& 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& 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::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& 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 }