wesnoth/src/actions/move.cpp
2025-02-17 12:59:51 -06:00

1481 lines
48 KiB
C++

/*
Copyright (C) 2003 - 2025
by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
/**
* @file
* Movement.
*/
#include "actions/move.hpp"
#include "actions/undo.hpp"
#include "actions/vision.hpp"
#include "game_events/pump.hpp"
#include "preferences/preferences.hpp"
#include "gettext.hpp"
#include "hotkey/hotkey_item.hpp"
#include "hotkey/hotkey_command.hpp"
#include "log.hpp"
#include "map/map.hpp"
#include "mouse_handler_base.hpp"
#include "pathfind/pathfind.hpp"
#include "pathfind/teleport.hpp"
#include "replay.hpp"
#include "replay_helper.hpp"
#include "synced_context.hpp"
#include "play_controller.hpp"
#include "resources.hpp"
#include "units/udisplay.hpp"
#include "font/standard_colors.hpp"
#include "formula/string_utils.hpp"
#include "team.hpp"
#include "units/unit.hpp"
#include "units/animation_component.hpp"
#include "whiteboard/manager.hpp"
#include <deque>
#include <set>
static lg::log_domain log_engine("engine");
#define ERR_NG LOG_STREAM(err, log_engine)
#define DBG_NG LOG_STREAM(debug, log_engine)
namespace actions {
void move_unit_spectator::add_seen_friend(const unit_map::const_iterator &u)
{
seen_friends_.push_back(u);
}
void move_unit_spectator::add_seen_enemy(const unit_map::const_iterator &u)
{
seen_enemies_.push_back(u);
}
const unit_map::const_iterator& move_unit_spectator::get_ambusher() const
{
return ambusher_;
}
const unit_map::const_iterator& move_unit_spectator::get_failed_teleport() const
{
return failed_teleport_;
}
const std::vector<unit_map::const_iterator>& move_unit_spectator::get_seen_enemies() const
{
return seen_enemies_;
}
const std::vector<unit_map::const_iterator>& move_unit_spectator::get_seen_friends() const
{
return seen_friends_;
}
const unit_map::const_iterator& move_unit_spectator::get_unit() const
{
return unit_;
}
move_unit_spectator::move_unit_spectator(const unit_map& units)
: ambusher_(units.end())
, failed_teleport_(units.end())
, seen_enemies_()
, seen_friends_()
, unit_(units.end())
, tiles_entered_(0)
, interrupted_(false)
{
}
move_unit_spectator::move_unit_spectator()
: ambusher_()
, failed_teleport_()
, seen_enemies_()
, seen_friends_()
, unit_()
, tiles_entered_(0)
, interrupted_(false)
{
}
move_unit_spectator::~move_unit_spectator()
{
}
void move_unit_spectator::reset(const unit_map &units)
{
ambusher_ = units.end();
failed_teleport_ = units.end();
seen_enemies_.clear();
seen_friends_.clear();
unit_ = units.end();
tiles_entered_ = 0;
interrupted_ = false;
}
void move_unit_spectator::error(const std::string& message)
{
ERR_NG << message;
}
void move_unit_spectator::set_ambusher(const unit_map::const_iterator &u)
{
ambusher_ = u;
}
void move_unit_spectator::set_failed_teleport(const unit_map::const_iterator &u)
{
failed_teleport_ = u;
}
void move_unit_spectator::set_unit(const unit_map::const_iterator &u)
{
unit_ = u;
}
namespace {
/**
* The number of the side that preivously owned the village that the unit stepped on
* Note, that recruit/recall actions can also take a village if the unit was recruits/recalled onto a village
*/
struct take_village_step : undo_action
{
int original_village_owner;
bool take_village_timebonus;
map_location loc;
take_village_step(
int orig_village_owner, bool time_bonus, map_location loc)
: original_village_owner(orig_village_owner)
, take_village_timebonus(time_bonus)
, loc(loc)
{
}
take_village_step(const config& cfg)
: original_village_owner(cfg["village_owner"].to_int())
, take_village_timebonus(cfg["village_timebonus"].to_bool())
, loc(cfg)
{
}
static const char* get_type_impl() { return "take_village"; }
virtual const char* get_type() const { return get_type_impl(); }
virtual ~take_village_step() { }
/** Writes this into the provided config. */
virtual void write(config& cfg) const
{
undo_action::write(cfg);
cfg["village_owner"] = original_village_owner;
cfg["village_timebonus"] = take_village_timebonus;
loc.write(cfg);
}
/** Undoes this action. */
virtual bool undo(int)
{
team& current_team = resources::controller->current_team();
if(resources::gameboard->map().is_village(loc)) {
get_village(loc, original_village_owner, nullptr, false);
// MP_COUNTDOWN take away capture bonus
if(take_village_timebonus) {
current_team.set_action_bonus_count(current_team.action_bonus_count() - 1);
}
}
return true;
}
};
static auto reg_undo_take_village_step = undo_action_container::subaction_factory<take_village_step>();
}
game_events::pump_result_t get_village(const map_location& loc, int side, bool *action_timebonus, bool fire_event)
{
std::vector<team> &teams = resources::gameboard->teams();
team *t = static_cast<unsigned>(side - 1) < teams.size() ? &teams[side - 1] : nullptr;
if (t && t->owns_village(loc)) {
return game_events::pump_result_t();
}
bool not_defeated = t && !resources::gameboard->team_is_defeated(*t);
bool grants_timebonus = false;
int old_owner_side = 0;
// We strip the village off all other sides, unless it is held by an ally
// and our side is already defeated (and thus we can't occupy it)
for(team& tm : teams) {
int i_side = tm.side();
if (!t || not_defeated || t->is_enemy(i_side)) {
if(tm.owns_village(loc)) {
old_owner_side = i_side;
tm.lose_village(loc);
}
if (side != i_side && action_timebonus) {
grants_timebonus = true;
}
}
}
if (!t) {
return game_events::pump_result_t();
}
if(grants_timebonus) {
t->set_action_bonus_count(1 + t->action_bonus_count());
*action_timebonus = true;
}
if(not_defeated) {
if (display::get_singleton() != nullptr) {
display::get_singleton()->invalidate(loc);
}
resources::undo_stack->add_custom<take_village_step>(old_owner_side, grants_timebonus, loc);
return t->get_village(loc, old_owner_side, fire_event ? resources::gamedata : nullptr);
}
return game_events::pump_result_t();
}
namespace { // Private helpers for move_unit()
/** Helper class for move_unit(). */
class unit_mover {
typedef std::vector<map_location>::const_iterator route_iterator;
public:
unit_mover(const unit_mover&) = delete;
unit_mover& operator=(const unit_mover&) = delete;
unit_mover(const std::vector<map_location> & route,
move_unit_spectator *move_spectator,
bool skip_sightings, bool skip_ally_sightings);
~unit_mover();
/** Determines how far along the route the unit can expect to move this turn. */
bool check_expected_movement();
/** Attempts to move the unit along the expected path. */
void try_actual_movement(bool show);
/** Does some bookkeeping and event firing, for use after movement. */
void post_move();
/** Shows the various on-screen messages, for use after movement. */
void feedback() const;
/** Attempts to teleport the unit to a map_location. */
void try_teleport(bool show);
/** After checking expected movement, this is the expected path. */
std::vector<map_location> expected_path() const
{ return std::vector<map_location>(begin_, expected_end_); }
/** After moving, this is the final hex reached. */
const map_location & final_hex() const
{ return *move_loc_; }
/** The number of hexes actually entered. */
std::size_t steps_travelled() const
{ return move_loc_ - begin_; }
/** After moving, use this to detect if movement was less than expected. */
bool stopped_early() const { return expected_end_ != real_end_; }
/**
* After moving, use this to detect if something happened that would
* interrupt movement (even if movement ended for a different reason).
*/
bool interrupted(bool include_end_of_move_events=true) const
{
return ambushed_ || blocked() || sighted_ || teleport_failed_ ||
(include_end_of_move_events ? (wml_removed_unit_ || wml_move_aborted_): event_mutated_mid_move_ ) ||
!move_it_.valid();
}
private: // functions
/** Returns whether or not movement was blocked by a non-ambushing enemy. */
bool blocked() const { return blocked_loc_ != map_location::null_location(); }
/** Checks the expected route for hidden units. */
void cache_hidden_units(const route_iterator & start,
const route_iterator & stop);
/** Fires the enter_hex or exit_hex event and updates our data as needed. */
void fire_hex_event(const std::string & event_name,
const route_iterator & current,
const route_iterator & other);
/** AI moves are supposed to not change the "goto" order. */
bool is_ai_move() const
{
return spectator_ && spectator_->is_ai_move();
}
/** Checks how far it appears we can move this turn. */
route_iterator plot_turn(const route_iterator & start,
const route_iterator & stop);
/** Updates our stored info after a WML event might have changed something. */
void post_wml(game_events::pump_result_t pump_res, const route_iterator & step);
void post_wml(game_events::pump_result_t pump_res) { return post_wml(pump_res, full_end_); }
/** Fires the sighted events that were raised earlier. */
void pump_sighted(const route_iterator & from);
/** Returns the ambush alert (if any) for the given unit. */
static std::string read_ambush_string(const unit & ambusher);
/** Reveals the unit at the indicated location. */
void reveal_ambusher(const map_location & hex, bool update_alert=true);
/** Returns whether or not undoing this move should be blocked. */
bool undo_blocked() const
{ return ambushed_ || blocked() || wml_removed_unit_ || wml_undo_disabled_ || fog_changed_ ||
teleport_failed_; }
// The remaining private functions are suggested to be inlined because
// each is used in only one place. (They are separate functions to ease
// code reading.)
/** Checks for ambushers around @a hex, setting flags as appropriate. */
inline void check_for_ambushers(const map_location & hex);
/** Makes sure the path is not obstructed by a unit. */
inline bool check_for_obstructing_unit(const map_location & hex,
const map_location & prev_hex);
/** Moves the unit the next step. */
inline bool do_move(const route_iterator & step_from,
const route_iterator & step_to,
unit_display::unit_mover & animator);
/** Teleports the unit. */
inline bool do_teleport(unit_display::unit_mover& animator);
/** Clears fog/shroud and handles units being sighted. */
inline void handle_fog(const map_location & hex, bool new_animation);
inline bool is_reasonable_stop(const map_location & hex) const;
/** Reveals the units stored in ambushers_ (and blocked_loc_). */
inline void reveal_ambushers();
/** Makes sure the units in ambushers_ still exist. */
inline void validate_ambushers();
private: // data
// (The order of the fields is somewhat important for the constructor.)
// Movement parameters (these decrease the number of parameters needed
// for individual functions).
move_unit_spectator * const spectator_;
const bool skip_sighting_;
const bool skip_ally_sighting_;
const bool playing_team_is_viewing_;
// Needed to interface with unit_display::unit_mover.
const std::vector<map_location> & route_;
// The route to traverse.
const route_iterator begin_;
const route_iterator full_end_; // The end of the plotted route.
route_iterator expected_end_; // The end of this turn's portion of the plotted route.
route_iterator ambush_limit_; // How far we can go before encountering hidden units, ignoring allied units.
route_iterator obstructed_; // Points to either full_end_ or a hex we cannot enter. This is used so that exit_hex can fire before we decide we cannot enter this hex.
route_iterator real_end_; // How far we actually can move this turn.
// The unit that is moving.
unit_map::iterator move_it_;
// This data stores the state from before the move started.
const int orig_side_;
int orig_moves_;
const map_location::direction orig_dir_;
const map_location goto_;
// This data tracks the current state as the move is in progress.
int current_side_;
team * current_team_; // Will default to the original team if the moving unit becomes invalid.
bool current_uses_fog_;
route_iterator move_loc_; // Will point to the last moved-to location (in case the moving unit disappears).
// Data accumulated while making the move.
map_location zoc_stop_;
map_location ambush_stop_; // Could be inaccurate if ambushed_ is false.
map_location blocked_loc_; // Location of a blocking, enemy, non-ambusher unit.
bool ambushed_;
bool show_ambush_alert_;
bool wml_removed_unit_;
bool wml_undo_disabled_;
bool wml_move_aborted_;
bool event_mutated_mid_move_; // Cache of wml_removed_unit_ || wml_move_aborted_ from just before the end-of-move handling.
bool fog_changed_;
bool sighted_; // Records if sightings were made that could interrupt movement.
bool sighted_stop_; // Records if sightings were made that did interrupt movement (the same as sighted_ unless movement ended for another reason).
bool teleport_failed_;
std::size_t enemy_count_;
std::size_t friend_count_;
std::string ambush_string_;
std::vector<map_location> ambushers_;
std::deque<int> moves_left_; // The front value is what the moving unit's remaining moves should be set to after the next step through the route.
shroud_clearer clearer_;
};
/**
* This constructor assumes @a route is not empty, and it will assert() that
* there is a unit at route.front().
* Iterators into @a route must remain valid for the life of this object.
* It is assumed that move_spectator is only supplied for AI moves (only
* affects whether or not gotos are changed).
*/
unit_mover::unit_mover(const std::vector<map_location> & route,
move_unit_spectator *move_spectator,
bool skip_sightings, bool skip_ally_sightings)
: spectator_(move_spectator)
, skip_sighting_(skip_sightings)
, skip_ally_sighting_(skip_ally_sightings)
, playing_team_is_viewing_(display::get_singleton()->viewing_team_is_playing() || display::get_singleton()->show_everything())
, route_(route)
, begin_(route.begin())
, full_end_(route.end())
, expected_end_(begin_)
, ambush_limit_(begin_)
, obstructed_(full_end_)
, real_end_(begin_)
// Unit information:
, move_it_(resources::gameboard->units().find(*begin_))
, orig_side_(( static_cast<void>(assert(move_it_ != resources::gameboard->units().end())), move_it_->side() ))
, orig_moves_(move_it_->movement_left())
, orig_dir_(move_it_->facing())
, goto_( is_ai_move() ? move_it_->get_goto() : route.back() )
, current_side_(orig_side_)
, current_team_(&resources::gameboard->get_team(current_side_))
, current_uses_fog_(current_team_->fog_or_shroud() && current_team_->auto_shroud_updates())
, move_loc_(begin_)
// The remaining fields are set to some sort of "zero state".
, zoc_stop_(map_location::null_location())
, ambush_stop_(map_location::null_location())
, blocked_loc_(map_location::null_location())
, ambushed_(false)
, show_ambush_alert_(false)
, wml_removed_unit_(false)
, wml_undo_disabled_(false)
, wml_move_aborted_(false)
, event_mutated_mid_move_(false)
, fog_changed_(false)
, sighted_(false)
, sighted_stop_(false)
, teleport_failed_(false)
, enemy_count_(0)
, friend_count_(0)
, ambush_string_()
, ambushers_()
, moves_left_()
, clearer_()
{
if ( !is_ai_move() )
// Clear the "goto" instruction during movement.
// (It will be reset in the destructor if needed.)
move_it_->set_goto(map_location::null_location());
}
unit_mover::~unit_mover()
{
// Set the "goto" order? (Not if WML set it.)
if ( !is_ai_move() && move_it_.valid() &&
move_it_->get_goto() == map_location::null_location() )
{
// Only set the goto if movement was not complete and was not
// interrupted.
if (real_end_ != full_end_ && !interrupted(false)) {
// End-of-move-events do not cancel a goto. (Use case: tutorial S2.)
move_it_->set_goto(goto_);
}
}
}
// Private inlines:
/**
* Checks for ambushers around @a hex, setting flags as appropriate.
*/
inline void unit_mover::check_for_ambushers(const map_location & hex)
{
const unit_map &units = resources::gameboard->units();
// Need to check each adjacent hex for hidden enemies.
for(const map_location& loc : get_adjacent_tiles(hex)) {
const unit_map::const_iterator neighbor_it = units.find(loc);
if ( neighbor_it != units.end() &&
current_team_->is_enemy(neighbor_it->side()) &&
neighbor_it->invisible(loc) )
{
// Ambushed!
ambushed_ = true;
ambush_stop_ = hex;
ambushers_.push_back(loc);
}
}
}
/**
* Makes sure the path is not obstructed by a unit.
* @param hex The hex to check.
* @param prev_hex The previous hex in the route (used to detect a teleport).
* @return true if @a hex is obstructed.
*/
inline bool unit_mover::check_for_obstructing_unit(const map_location & hex,
const map_location & prev_hex)
{
const unit_map::const_iterator blocking_unit = resources::gameboard->units().find(hex);
// If no unit, then the path is not obstructed.
if (blocking_unit == resources::gameboard->units().end()) {
return false;
}
// Check for units blocking a teleport exit. This can now only happen
// if these units are not visible to the current side, as otherwise no
// valid path is found.
if ( !tiles_adjacent(hex, prev_hex) ) {
if ( current_team_->is_enemy(blocking_unit->side()) ) {
// Enemy units always block the tunnel.
teleport_failed_ = true;
return true;
} else {
// By contrast, allied units (of a side which does not share vision) only
// block the tunnel if pass_allied_units=true. Whether the teleport is possible
// is checked by getting the teleport map with the see_all flag set to true.
const pathfind::teleport_map teleports = pathfind::get_teleport_locations(*move_it_, *current_team_, true, false, false);
const auto allowed_teleports = teleports.get_adjacents(prev_hex);
if(allowed_teleports.count(hex) == 0) {
teleport_failed_ = true;
return true;
}
}
}
if ( current_team_->is_enemy(blocking_unit->side()) ) {
// Trying to go through an enemy.
blocked_loc_ = hex;
return true;
}
// If we get here, the unit does not interfere with movement.
return false;
}
/**
* Moves the unit the next step.
* @a step_to is the hex being moved to.
* @a step_from is the hex before that in the route.
* (The unit is actually at *move_loc_.)
* @a animator is the unit_display::unit_mover being used.
* @return whether or not we started a new animation.
*/
inline bool unit_mover::do_move(const route_iterator & step_from,
const route_iterator & step_to,
unit_display::unit_mover & animator)
{
game_display &disp = *game_display::get_singleton();
// Adjust the movement even if we cannot move yet.
// We will eventually be able to move if nothing unexpected
// happens, and if something does happen, this movement is the
// cost to discover it.
move_it_->set_movement(moves_left_.front(), true);
moves_left_.pop_front();
// Invalidate before moving so we invalidate neighbor hexes if needed.
move_it_->anim_comp().invalidate(disp);
// Attempt actually moving. Fails if *step_to is occupied.
auto [unit_it, success] = resources::gameboard->units().move(*move_loc_, *step_to);
if(success) {
resources::undo_stack->add_move(
unit_it.get_shared_ptr(), move_loc_, step_to + 1, orig_moves_, unit_it->facing());
orig_moves_ = unit_it->movement_left();
// Update the moving unit.
move_it_ = unit_it;
move_it_->set_facing(step_from->get_relative_dir(*step_to));
// Disable bars. The expectation here is that the animation
// unit_mover::finish() will clean after us at a later point. Ugly,
// but it works.
move_it_->anim_comp().set_standing(false);
disp.invalidate_unit_after_move(*move_loc_, *step_to);
disp.invalidate(*step_to);
move_loc_ = step_to;
// Show this move.
animator.proceed_to(move_it_.get_shared_ptr(), step_to - begin_,
move_it_->appearance_changed(), false);
move_it_->set_appearance_changed(false);
disp.redraw_minimap();
}
return success;
}
/**
* Teleports the unit to begin_ + 1.
* @a animator is the unit_display::unit_mover being used.
* @return whether or not we started a new animation.
*/
inline bool unit_mover::do_teleport(unit_display::unit_mover& animator)
{
game_display& disp = *game_display::get_singleton();
const route_iterator step_to = begin_ + 1;
// Invalidate before moving so we invalidate neighbor hexes if needed.
move_it_->anim_comp().invalidate(disp);
// Attempt actually moving. Fails if *step_to is occupied.
auto [unit_it, success] = resources::gameboard->units().move(*begin_, *step_to);
if(success) {
// Update the moving unit.
move_it_ = unit_it;
move_it_->set_facing(begin_->get_relative_dir(*step_to));
move_it_->anim_comp().set_standing(false);
disp.invalidate_unit_after_move(*begin_, *step_to);
disp.invalidate(*step_to);
move_loc_ = step_to;
// Show this move.
animator.proceed_to(move_it_.get_shared_ptr(), step_to - begin_, move_it_->appearance_changed(), false);
move_it_->set_appearance_changed(false);
disp.redraw_minimap();
}
return success;
}
/**
* Clears fog/shroud and raises events for units being sighted.
* Only call this if the current team uses fog or shroud.
* @a hex is both the center of fog clearing and the filtered location of
* the moving unit when the sighted events will be fired.
*/
inline void unit_mover::handle_fog(const map_location & hex,
bool new_animation)
{
// Clear the fog.
if ( clearer_.clear_unit(hex, *move_it_, *current_team_, nullptr,
&enemy_count_, &friend_count_, spectator_,
!new_animation) )
{
clearer_.invalidate_after_clear();
fog_changed_ = true;
}
// Check for sighted units?
if ( !skip_sighting_ ) {
sighted_ = enemy_count_ != 0 ;
}
if( !skip_sighting_ && !skip_ally_sighting_ )
{
sighted_ |= (friend_count_ != 0);
}
}
/**
* @return true if an unscheduled stop at @a hex is not likely to negatively
* impact the player's plans.
* (E.g. it would not kill movement by making an unintended village capture.)
*/
inline bool unit_mover::is_reasonable_stop(const map_location & hex) const
{
// We cannot reasonably stop if move_it_ could not be moved to this
// hex (the hex was occupied by someone else).
if (*move_loc_ != hex) {
return false;
}
// We can reasonably stop if the hex is not an unowned village.
return !resources::gameboard->map().is_village(hex) ||
current_team_->owns_village(hex);
}
/**
* Reveals the units stored in ambushers_ (and blocked_loc_).
* Also sets ambush_string_.
* May fire "sighted" events.
* Only call this if appropriate; this function does not itself check
* ambushed_ or blocked().
*/
inline void unit_mover::reveal_ambushers()
{
// Reveal the blocking unit.
if (blocked()) {
reveal_ambusher(blocked_loc_, false);
}
// Reveal ambushers.
for(const map_location & reveal : ambushers_) {
reveal_ambusher(reveal, true);
}
// Default "Ambushed!" message?
if (ambush_string_.empty()) {
ambush_string_ = _("Ambushed!");
}
}
/**
* Makes sure the units in ambushers_ still exist.
*/
inline void unit_mover::validate_ambushers()
{
const unit_map &units = resources::gameboard->units();
// Loop through the previously-detected ambushers.
std::size_t i = 0;
while ( i != ambushers_.size() ) {
if (units.count(ambushers_[i]) == 0) {
// Ambusher is gone.
ambushers_.erase(ambushers_.begin() + i);
}
else {
// Proceed to the next ambusher.
++i;
}
}
}
// Private utilities:
/**
* Checks the expected route for hidden units.
* This basically handles all the checks for surprises that can be done
* without visibly notifying a player. Thus this can be called at the
* start of movement and immediately after events, rather than tie up
* CPU time in the middle of animating a move.
*
* @param[in] start The beginning of the path to check.
* @param[in] stop The end of the path to check.
*/
void unit_mover::cache_hidden_units(const route_iterator & start,
const route_iterator & stop)
{
// Clear the old cache.
obstructed_ = full_end_;
blocked_loc_ = map_location::null_location();
teleport_failed_ = false;
// The ambush cache needs special treatment since we cannot re-detect
// an ambush if we are already at the ambushed location.
ambushed_ = ambushed_ && ambush_stop_ == *start;
if ( ambushed_ ) {
validate_ambushers();
ambushed_ = !ambushers_.empty();
}
if ( !ambushed_ ) {
ambush_stop_ = map_location::null_location();
ambushers_.clear();
}
// Update the shroud clearer.
clearer_.cache_units(current_uses_fog_ ? current_team_ : nullptr);
// Abort for null routes.
if ( start == stop ) {
ambush_limit_ = start;
return;
}
// This loop will end with ambush_limit_ pointing one element beyond
// where the unit would be forced to stop by a hidden unit.
for ( ambush_limit_ = start+1; ambush_limit_ != stop; ++ambush_limit_ ) {
// Check if we need to stop in the previous hex.
if ( ambushed_ ) {
break;
}
// Check for being unable to enter this hex.
if ( check_for_obstructing_unit(*ambush_limit_, *(ambush_limit_-1)) ) {
// No replay check here? Makes some sense, I guess.
obstructed_ = ambush_limit_++; // The limit needs to be after obstructed_ in order for the latter to do anything.
break;
}
// We can enter this hex.
// See if we are stopped in this hex.
check_for_ambushers(*ambush_limit_);
}
}
/**
* Fires the enter_hex or exit_hex event and updates our data as needed.
*
* @param[in] event_name The name of the event ("enter_hex" or "exit_hex").
* @param[in] current The currently occupied hex.
* @param[in] other The secondary hex to provide to the event.
*
*/
void unit_mover::fire_hex_event(const std::string & event_name,
const route_iterator & current,
const route_iterator & other)
{
const game_events::entity_location mover(*move_it_, *current);
post_wml(resources::game_events->pump().fire(event_name, mover, *other), current);
}
/**
* Checks how far it appears we can move this turn.
*
* @param[in] start The beginning of the plotted path.
* @param[in] stop The end of the plotted path.
*
* @return An end iterator for the path that can be traversed this turn.
*/
unit_mover::route_iterator unit_mover::plot_turn(const route_iterator & start,
const route_iterator & stop)
{
const gamemap &map = resources::gameboard->map();
// Handle null routes.
if (start == stop) {
return start;
}
int remaining_moves = move_it_->movement_left();
zoc_stop_ = map_location::null_location();
moves_left_.clear();
if ( start != begin_ ) {
// Check for being unable to leave the current hex.
if ( !move_it_->get_ability_bool("skirmisher", *start) &&
pathfind::enemy_zoc(*current_team_, *start, *current_team_) )
zoc_stop_ = *start;
}
// This loop will end with end pointing one element beyond where the
// unit thinks it will stop (the usual notion of "end" for iterators).
route_iterator end = start + 1;
for ( ; end != stop; ++end )
{
// Break out of the loop if we cannot leave the previous hex.
if ( zoc_stop_ != map_location::null_location() ) {
break;
}
remaining_moves -= move_it_->movement_cost(map[*end]);
if ( remaining_moves < 0 ) {
break;
}
// We can enter this hex. Record the cost.
moves_left_.push_back(remaining_moves);
// Check for being unable to leave this hex.
if (!move_it_->get_ability_bool("skirmisher", *end) &&
pathfind::enemy_zoc(*current_team_, *end, *current_team_))
{
zoc_stop_ = *end;
}
}
route_iterator min_end = start == begin_ ? start : start + 1;
while (end != min_end && resources::gameboard->has_visible_unit(*(end - 1), *current_team_)) {
// Backtrack.
--end;
}
return end;
}
/**
* Updates our stored info after a WML event might have changed something.
*
* @param step Indicates the position in the path where we might need to start recalculating movement.
* Set this to full_end_ (or do not supply it) to skip such recalculations (because movement has finished).
*
* @returns false if continuing is impossible (i.e. we lost the moving unit).
*/
void unit_mover::post_wml(game_events::pump_result_t pump_res, const route_iterator & step)
{
wml_move_aborted_ |= std::get<1>(pump_res);
wml_undo_disabled_ |= std::get<0>(pump_res);
// Re-find the moving unit.
move_it_ = resources::gameboard->units().find(*move_loc_);
const bool found = move_it_ != resources::gameboard->units().end();
// Update the current unit data.
current_side_ = found ? move_it_->side() : orig_side_;
current_team_ = &resources::gameboard->get_team(current_side_);
current_uses_fog_ = current_team_->fog_or_shroud() &&
( current_side_ != orig_side_ ||
current_team_->auto_shroud_updates() );
// Update the path.
if ( found && step != full_end_ ) {
const route_iterator new_limit = plot_turn(step, expected_end_);
cache_hidden_units(step, new_limit);
// Just in case: length 0 paths become length 1 paths.
if (ambush_limit_ == step) {
++ambush_limit_;
}
}
wml_removed_unit_ |= !found;
}
/**
* Fires the sighted events that were raised earlier.
*
* @param[in] from Points to the hex the sighting unit currently occupies.
*
* @return sets event_mutated_ || wml_move_aborted_ to true if this event should interrupt movement.
*/
void unit_mover::pump_sighted(const route_iterator & from)
{
game_events::pump_result_t pump_res = clearer_.fire_events();
post_wml(pump_res, from);
}
/**
* Returns the ambush alert (if any) for the given unit.
*/
std::string unit_mover::read_ambush_string(const unit & ambusher)
{
for(const unit_ability &hide : ambusher.get_abilities("hides"))
{
const std::string & ambush_string = (*hide.ability_cfg)["alert"].str();
if (!ambush_string.empty()) {
return ambush_string;
}
}
// No string found.
return std::string();
}
/**
* Reveals the unit at the indicated location.
* Can also update the current ambushed alert.
* May fire "sighted" events.
*/
void unit_mover::reveal_ambusher(const map_location & hex, bool update_alert)
{
// Convenient alias:
unit_map &units = resources::gameboard->units();
game_display &disp = *game_display::get_singleton();
// Find the unit at the indicated location.
unit_map::iterator ambusher = units.find(hex);
if ( ambusher != units.end() ) {
// Prepare for sighted events.
std::vector<int> sight_cache(get_sides_not_seeing(*ambusher));
// Make sure the unit is visible (during sighted events, and in case
// we had to backtrack).
ambusher->set_state(unit::STATE_UNCOVERED, true);
// Record this in the move spectator.
if (spectator_) {
spectator_->set_ambusher(ambusher);
}
// Override the default ambushed message?
if ( update_alert ) {
// Observers don't get extra information.
if ( playing_team_is_viewing_ || !disp.fogged(hex) ) {
show_ambush_alert_ = true;
// We only support one custom ambush message; use the first one.
if (ambush_string_.empty()) {
ambush_string_ = read_ambush_string(*ambusher);
}
}
}
// Make sure this hex is drawn correctly.
disp.invalidate(hex);
// Fire sighted events.
auto [wml_undo_blocked, wml_move_aborted] = actor_sighted(*ambusher, &sight_cache);
// TODO: should we call post_wml ?
wml_move_aborted_ |= wml_move_aborted;
wml_undo_disabled_ |= wml_undo_blocked;
}
}
// Public interface:
/**
* Determines how far along the route the unit can expect to move this turn.
* This is based solely on data known to the player, and will not plot a move
* that ends on another (known) unit.
* (For example, this prevents a player from plotting a multi-turn move that
* has this turn's movement ending on a (slower) unit, and thereby clearing
* fog as if the moving unit actually made it on top of that other unit.)
*
* @returns false if the expectation is to not move at all.
*/
bool unit_mover::check_expected_movement()
{
expected_end_ = plot_turn(begin_, full_end_);
return expected_end_ != begin_;
}
/**
* Attempts to move the unit along the expected path.
* (This will do nothing unless check_expected_movement() was called first.)
*
* @param[in] show Set to false to suppress animations.
*/
void unit_mover::try_actual_movement(bool show)
{
static const std::string enter_hex_str("enter hex");
static const std::string exit_hex_str("exit hex");
bool obstructed_stop = false;
// Check for hidden units along the expected path before we start
// animating and firing events.
cache_hidden_units(begin_, expected_end_);
if ( begin_ != ambush_limit_ ) {
// Cache the moving unit's visibility.
std::vector<int> not_seeing = get_sides_not_seeing(*move_it_);
// Prepare to animate.
unit_display::unit_mover animator(route_, show);
animator.start(move_it_.get_shared_ptr());
// Traverse the route to the hex where we need to stop.
// Each iteration performs the move from real_end_-1 to real_end_.
for ( real_end_ = begin_+1; real_end_ != ambush_limit_; ++real_end_ ) {
const route_iterator step_from = real_end_ - 1;
// See if we can leave *step_from.
// Already accounted for: ambusher
if ( wml_removed_unit_ || wml_move_aborted_) {
break;
}
if ( sighted_ && is_reasonable_stop(*step_from) ) {
sighted_stop_ = true;
break;
}
// Already accounted for: ZoC
// Already accounted for: movement cost
fire_hex_event(exit_hex_str, step_from, real_end_);
if (wml_removed_unit_ || wml_move_aborted_) {
break;
}
if ( real_end_ == obstructed_ ) {
// We did not check for being a replay when checking for an
// obstructed hex, so we do not check can_break here.
obstructed_stop = true;
break;
}
// We can leave *step_from. Make the move to *real_end_.
bool new_animation = do_move(step_from, real_end_, animator);
// Update the fog.
if ( current_uses_fog_ )
handle_fog(*real_end_, new_animation);
animator.wait_for_anims();
// Fire the events for this step.
// (These return values are not checked since real_end_ still
// needs to be incremented. The wml_move_aborted_ check will break
// us out of the loop if needed.)
fire_hex_event(enter_hex_str, real_end_, step_from);
// Sighted events only fire if we could stop due to sighting.
if (is_reasonable_stop(*real_end_)) {
pump_sighted(real_end_);
}
}//for
// Make sure any remaining sighted events get fired.
pump_sighted(real_end_-1);
if ( move_it_.valid() ) {
// Finish animating.
animator.finish(move_it_.get_shared_ptr());
// Check for the moving unit being seen.
auto [wml_undo_blocked, wml_move_aborted] = actor_sighted(*move_it_, &not_seeing);
// TODO: should we call post_wml ?
wml_move_aborted_ |= wml_move_aborted;
wml_undo_disabled_ |= wml_undo_blocked;
}
}//if
// Some flags were set to indicate why we might stop.
// Update those to reflect whether or not we got to them.
ambushed_ = ambushed_ && real_end_ == ambush_limit_;
if (!obstructed_stop) {
blocked_loc_ = map_location::null_location();
}
teleport_failed_ = teleport_failed_ && obstructed_stop;
// event_mutated_ does not get unset, regardless of other reasons
// for stopping, but we do save its current value.
event_mutated_mid_move_ = wml_removed_unit_ || wml_move_aborted_;
}
/**
* Attempts to teleport the unit to a map_location.
*
* @param[in] show Set to false to suppress animations.
*/
void unit_mover::try_teleport(bool show)
{
const route_iterator step_from = real_end_ - 1;
std::vector<int> not_seeing = get_sides_not_seeing(*move_it_);
// Prepare to animate.
unit_display::unit_mover animator(route_, show);
animator.start(move_it_.get_shared_ptr());
fire_hex_event("exit hex", step_from, begin_);
bool new_animation = do_teleport(animator);
if(current_uses_fog_)
handle_fog(*(begin_ + 1), new_animation);
animator.wait_for_anims();
fire_hex_event("enter hex", begin_, step_from);
if(is_reasonable_stop(*step_from)) {
pump_sighted(step_from);
}
pump_sighted(step_from);
if(move_it_.valid()) {
// Finish animating.
animator.finish(move_it_.get_shared_ptr());
// Check for the moving unit being seen.
auto [wml_undo_blocked, wml_move_aborted] = actor_sighted(*move_it_, &not_seeing);
wml_move_aborted_ |= wml_move_aborted;
wml_undo_disabled_ |= wml_undo_blocked;
}
post_move();
}
/**
* Does some bookkeeping and event firing, for use after movement.
* This includes village capturing and the undo stack.
*/
void unit_mover::post_move()
{
const map_location & final_loc = final_hex();
int orig_village_owner = 0;
bool action_time_bonus = false;
// Reveal ambushers?
if (ambushed_ || blocked()) {
reveal_ambushers();
}
else if (teleport_failed_ && spectator_) {
spectator_->set_failed_teleport(resources::gameboard->units().find(*obstructed_));
}
unit::clear_status_caches();
if ( move_it_.valid() ) {
// Update the moving unit.
move_it_->set_interrupted_move(
sighted_stop_ && !resources::whiteboard->is_executing_actions() ?
*(full_end_-1) :
map_location::null_location());
if (ambushed_ || final_loc == zoc_stop_) {
move_it_->set_movement(0, true);
}
// Village capturing.
if ( resources::gameboard->map().is_village(final_loc) ) {
// Is this a capture?
orig_village_owner = resources::gameboard->village_owner(final_loc);
if ( orig_village_owner != current_side_) {
// Captured. Zap movement and take over the village.
move_it_->set_movement(0, true);
post_wml(get_village(final_loc, current_side_, &action_time_bonus));
}
}
}
// Finally, the moveto event.
post_wml(resources::game_events->pump().fire("moveto", final_loc, *begin_));
// Record keeping.
if (spectator_) {
spectator_->set_unit(move_it_);
spectator_->set_interrupted(interrupted());
spectator_->set_tiles_entered(steps_travelled());
}
const bool mover_valid = move_it_.valid();
if(!mover_valid || undo_blocked()) {
synced_context::block_undo();
}
// TODO: this looks wrong, whiteboard shouldn't effect the undo stack during a synced action.
if(resources::whiteboard->is_active() && resources::whiteboard->should_clear_undo()) {
synced_context::block_undo();
}
// Update the screen.
display::get_singleton()->redraw_minimap();
}
/**
* Shows the various on-screen messages, for use after movement.
*/
void unit_mover::feedback() const
{
// Alias some resources.
game_display &disp = *game_display::get_singleton();
// Multiple messages may be displayed simultaneously
// this variable is used to keep them from overlapping
std::string message_prefix = "";
// Ambush feedback?
if ( ambushed_ && show_ambush_alert_ ) {
disp.announce(message_prefix + ambush_string_, font::BAD_COLOR);
message_prefix += " \n";
}
display::announce_options announce_options;
announce_options.discard_previous = false;
// Failed teleport feedback?
if ( playing_team_is_viewing_ && teleport_failed_ ) {
std::string teleport_string = _("Failed teleport! Exit not empty");
disp.announce(message_prefix + teleport_string, font::BAD_COLOR, announce_options);
message_prefix += " \n";
}
// Sighted units feedback?
if ( playing_team_is_viewing_ && (enemy_count_ != 0 || friend_count_ != 0) ) {
// Create the message to display (depends on whether friends,
// enemies, or both were sighted, and on how many of each).
utils::string_map symbols;
symbols["enemies"] = std::to_string(enemy_count_);
symbols["friends"] = std::to_string(friend_count_);
std::string message;
color_t msg_color;
if ( friend_count_ != 0 && enemy_count_ != 0 ) {
// TRANSLATORS: This becomes the "friendphrase" in "Units sighted! ($friendphrase, $enemyphrase)"
symbols["friendphrase"] = VNGETTEXT("Part of 'Units sighted! (...)' sentence^1 friendly", "$friends friendly", friend_count_, symbols);
// TRANSLATORS: This becomes the "enemyphrase" in "Units sighted! ($friendphrase, $enemyphrase)"
symbols["enemyphrase"] = VNGETTEXT("Part of 'Units sighted! (...)' sentence^1 enemy", "$enemies enemy", enemy_count_, symbols);
// TRANSLATORS: Both friends and enemies sighted -- neutral message.
// This is shown when a move is interrupted because units were revealed from the fog of war.
message = VGETTEXT("Units sighted! ($friendphrase, $enemyphrase)", symbols);
msg_color = font::NORMAL_COLOR;
} else if ( enemy_count_ != 0 ) {
// TRANSLATORS: Only enemies sighted -- bad message.
// This is shown when a move is interrupted because units were revealed from the fog of war.
message = VNGETTEXT("Enemy unit sighted!", "$enemies enemy units sighted!", enemy_count_, symbols);
msg_color = font::BAD_COLOR;
} else if ( friend_count_ != 0 ) {
// TRANSLATORS: Only friends sighted -- good message.
// This is shown when a move is interrupted because units were revealed from the fog of war.
message = VNGETTEXT("Friendly unit sighted", "$friends friendly units sighted", friend_count_, symbols);
msg_color = font::GOOD_COLOR;
}
disp.announce(message_prefix + message, msg_color, announce_options);
message_prefix += " \n";
}
// Suggest "continue move"?
if ( playing_team_is_viewing_ && sighted_stop_ && !resources::whiteboard->is_executing_actions() ) {
// See if the "Continue Move" action has an associated hotkey
std::string name = hotkey::get_names(hotkey::hotkey_command::get_command_by_command(hotkey::HOTKEY_CONTINUE_MOVE).id);
if ( !name.empty() ) {
utils::string_map symbols;
symbols["hotkey"] = name;
std::string message = VGETTEXT("(press $hotkey to keep moving)", symbols);
disp.announce(message_prefix + message, font::NORMAL_COLOR, announce_options);
message_prefix += " \n";
}
}
}
}//end anonymous namespace
static void move_unit_internal(unit_mover& mover)
{
bool show_move = !resources::controller->is_skipping_replay() && !resources::controller->is_skipping_actions();
const events::command_disabler disable_commands;
// Attempt moving.
mover.try_actual_movement(show_move);
config co;
config cn {
"stopped_early", mover.stopped_early(),
"final_hex_x", mover.final_hex().wml_x(),
"final_hex_y", mover.final_hex().wml_y(),
};
bool matches_replay = checkup_instance->local_checkup(cn,co);
if(!matches_replay)
{
replay::process_error("calculated movement destination (x="+ cn["final_hex_x"].str() + " y=" + cn["final_hex_y"].str() +
") didn't match the original destination(x="+ co["final_hex_x"].str() + " y=" + co["final_hex_y"].str() + ")\n");
//TODO: move the unit by force to the desired destination with something like mover.reset_final_hex(co["x"], co["y"]);
}
// Bookkeeping, etc.
// also fires the moveto event
mover.post_move();
if (show_move) {
mover.feedback();
}
}
/**
* Moves a unit across the board.
*
* This function handles actual movement, checking terrain costs as well as
* things that might interrupt movement (e.g. ambushes). If the full path
* cannot be reached this turn, the remainder is stored as the unit's "goto"
* instruction. (The unit itself is whatever unit is at the beginning of the
* supplied path.)
*
* @param[in] steps The route to be traveled. The unit to be moved is at the beginning of this route.
* @param[in] continued_move If set to true, this is a continuation of an earlier move (movement is not
* interrupted should units be spotted).
* @param[in] skip_ally_sighted If set to true, movement is not interrupted should allied units be spotted.
* @param[out] move_spectator If supplied, this will be given the information uncovered by the move and about the
* move
*/
void execute_move_unit(const std::vector<map_location>& steps,
bool continued_move,
bool skip_ally_sighted,
move_unit_spectator* move_spectator)
{
// Evaluate this move.
unit_mover mover(steps, move_spectator, continued_move, skip_ally_sighted);
if(!mover.check_expected_movement()) {
DBG_NG << "found expected empty move, aborting";
return;
}
move_unit_internal(mover);
}
void teleport_unit_and_record(const map_location& teleport_from,
const map_location& teleport_to,
move_unit_spectator* /* move_spectator */)
{
synced_context::run_and_throw("debug_teleport",
config{"teleport_from_x", teleport_from.wml_x(), "teleport_from_y", teleport_from.wml_y(), "teleport_to_x",
teleport_to.wml_x(), "teleport_to_y", teleport_to.wml_y()});
}
void teleport_unit_from_replay(const std::vector<map_location> &steps,
bool continued_move, bool skip_ally_sighted, bool show_move)
{
unit_mover mover(steps, nullptr, continued_move, skip_ally_sighted);
mover.try_teleport(show_move);
}
/**
* Wrapper around the other overload.
*
* @param[in] steps The route to be traveled. The unit to be moved is at the beginning of this route.
* @param[in] continued_move If set to true, this is a continuation of an earlier move (movement is not
* interrupted should units be spotted).
* @param[out] interrupted If supplied, then this is set to true if information was uncovered that warrants
* interrupting a chain of actions (and set to false otherwise).
*
* @returns The number of hexes entered. This can safely be used as an index
* into @a steps to get the location where movement ended, provided
* @a steps is not empty (the return value is guaranteed to be less
* than steps.size() ).
*/
std::size_t move_unit_and_record(
const std::vector<map_location>& steps, bool continued_move, bool* interrupted)
{
auto move_spectator = move_unit_spectator();
move_unit_and_record(steps, continued_move, move_spectator);
if(interrupted) {
*interrupted = move_spectator.get_interrupted();
}
return move_spectator.get_tiles_entered();
}
/**
* Moves a unit across the board.
*
* This function handles actual movement, checking terrain costs as well as
* things that might interrupt movement (e.g. ambushes). If the full path
* cannot be reached this turn, the remainder is stored as the unit's "goto"
* instruction. (The unit itself is whatever unit is at the beginning of the
* supplied path.)
*
* @param[in] steps The route to be traveled. The unit to be moved is at the beginning of this route.
* @param[in] continued_move If set to true, this is a continuation of an earlier move (movement is not interrupted should units be spotted).
* @param[out] move_spectator If supplied, this will be given the information uncovered by the move and about the move
*/
void move_unit_and_record(
const std::vector<map_location>& steps, bool continued_move, move_unit_spectator& move_spectator)
{
// Avoid some silliness.
if ( steps.size() < 2 || (steps.size() == 2 && steps.front() == steps.back()) ) {
DBG_NG << "Ignoring a unit trying to jump on its hex at " <<
( steps.empty() ? map_location::null_location() : steps.front() ) << ".";
return;
}
//if we have no fog activated then we always skip sighted
if(resources::gameboard->units().find(steps.front()) != resources::gameboard->units().end())
{
const team &current_team = resources::gameboard->teams()[
resources::gameboard->units().find(steps.front())->side() - 1];
continued_move |= !current_team.fog_or_shroud();
}
const bool skip_ally_sighted = !prefs::get().ally_sighted_interrupts();
synced_context::run_and_throw(
"move", replay_helper::get_movement(steps, continued_move, skip_ally_sighted), move_spectator);
}
}//namespace actions