mirror of
https://github.com/wesnoth/wesnoth
synced 2025-05-04 15:43:18 +00:00
1528 lines
52 KiB
C++
1528 lines
52 KiB
C++
/* $Id$ */
|
|
/*
|
|
Copyright (C) 2006 - 2007 by Joerg Hinrichs <joerg.hinrichs@alice-dsl.de>
|
|
wesnoth playturn Copyright (C) 2003 by David White <dave@whitevine.net>
|
|
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License version 2
|
|
or at your option any later version.
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY.
|
|
|
|
See the COPYING file for more details.
|
|
*/
|
|
|
|
#include "mouse_events.hpp"
|
|
|
|
#include "attack_prediction.hpp"
|
|
#include "cursor.hpp"
|
|
#include "dialogs.hpp"
|
|
#include "game_events.hpp"
|
|
#include "gettext.hpp"
|
|
#include "marked-up_text.hpp"
|
|
#include "menu_events.hpp"
|
|
#include "preferences_display.hpp"
|
|
#include "sound.hpp"
|
|
#include "replay.hpp"
|
|
#include "show_dialog.hpp"
|
|
#include "unit_abilities.hpp"
|
|
#include "wassert.hpp"
|
|
#include "wml_separators.hpp"
|
|
#include "unit_display.hpp"
|
|
#include "sdl_utils.hpp"
|
|
|
|
#include <cstdlib>
|
|
|
|
namespace events{
|
|
|
|
int commands_disabled = 0;
|
|
|
|
command_disabler::command_disabler()
|
|
{
|
|
++commands_disabled;
|
|
}
|
|
|
|
command_disabler::~command_disabler()
|
|
{
|
|
--commands_disabled;
|
|
}
|
|
|
|
static bool command_active()
|
|
{
|
|
#ifdef __APPLE__
|
|
return (SDL_GetModState()&KMOD_META) != 0;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
namespace{
|
|
//minimum dragging distance to fire the drag&drop
|
|
const double drag_threshold = 14.0;
|
|
|
|
// This preview pane is shown in the "Damage Calculations" dialog.
|
|
class battle_prediction_pane : public gui::preview_pane
|
|
{
|
|
public:
|
|
|
|
// Lengthy constructor.
|
|
battle_prediction_pane(game_display &disp, const battle_context& bc, const gamemap& map,
|
|
const std::vector<team>& teams, const unit_map& units,
|
|
const gamestatus& status, const game_data& gamedata,
|
|
const gamemap::location& attacker_loc, const gamemap::location& defender_loc);
|
|
|
|
// This method is called to draw the dialog contents.
|
|
void draw_contents();
|
|
|
|
// Hack: pretend the preview pane goes to the left.
|
|
bool left_side() const { return 1; }
|
|
|
|
// Unused.
|
|
void set_selection(int) {}
|
|
|
|
private:
|
|
game_display &disp_;
|
|
const battle_context& bc_;
|
|
const gamemap& map_;
|
|
const std::vector<team>& teams_;
|
|
const unit_map& units_;
|
|
const gamestatus& status_;
|
|
const game_data& gamedata_;
|
|
const gamemap::location& attacker_loc_;
|
|
const gamemap::location& defender_loc_;
|
|
const unit& attacker_;
|
|
const unit& defender_;
|
|
|
|
// Layout constants.
|
|
static const int inter_line_gap_;
|
|
static const int inter_column_gap_;
|
|
static const int inter_units_gap_;
|
|
static const int max_hp_distrib_rows_;
|
|
|
|
// Layout computations.
|
|
std::string attacker_label_, defender_label_;
|
|
int attacker_label_width_, defender_label_width_;
|
|
|
|
std::vector<std::string> attacker_left_strings_, attacker_right_strings_;
|
|
std::vector<std::string> defender_left_strings_, defender_right_strings_;
|
|
int attacker_strings_width_, attacker_left_strings_width_, attacker_right_strings_width_;
|
|
int defender_strings_width_, defender_left_strings_width_, defender_right_strings_width_;
|
|
int units_strings_height_;
|
|
|
|
std::string hp_distrib_string_;
|
|
surface attacker_hp_distrib_, defender_hp_distrib_;
|
|
int hp_distrib_string_width_;
|
|
int attacker_hp_distrib_width_, defender_hp_distrib_width_;
|
|
int attacker_hp_distrib_height_, defender_hp_distrib_height_, hp_distribs_height_;
|
|
|
|
int attacker_width_, defender_width_, units_width_;
|
|
int dialog_width_, dialog_height_;
|
|
|
|
// This method builds the strings describing the unit damage modifiers.
|
|
// Read the code to understand the arguments.
|
|
void get_unit_strings(const battle_context::unit_stats& stats,
|
|
const unit& u, const gamemap::location& u_loc, float u_unscathed,
|
|
const unit& opp, const gamemap::location& opp_loc, const attack_type *opp_weapon,
|
|
std::vector<std::string>& left_strings, std::vector<std::string>& right_strings,
|
|
int& left_strings_width, int& right_strings_width, int& strings_width);
|
|
|
|
// Utility method that returns the length of the longest string in a vector of strings.
|
|
int get_strings_max_length(const std::vector<std::string>& strings);
|
|
|
|
// This method builds the vector containing the <HP, probability> pairs
|
|
// that are required to draw the image of the hitpoints distribution of
|
|
// a combatant after a fight. The method takes as input the probability
|
|
// distribution of the hitpoints of the combatant after the fight.
|
|
void get_hp_prob_vector(const std::vector<double>& hp_dist,
|
|
std::vector<std::pair<int, double> >& hp_prob_vector);
|
|
|
|
// This method draws a unit in the dialog pane. Read the code to understand
|
|
// the arguments.
|
|
void draw_unit(int x_off, int damage_line_skip, int left_strings_width,
|
|
const std::vector<std::string>& left_strings,
|
|
const std::vector<std::string>& right_strings,
|
|
const std::string& label, int label_width,
|
|
surface& hp_distrib, int hp_distrib_width);
|
|
|
|
// This method draws the image of the hitpoints distribution of a
|
|
// combatant after a fight. The method takes as input the
|
|
// "hp_prob_vector" computed above and the stats of the combatants.
|
|
// It draws the image in the surface 'surf' and set the width and
|
|
// height of the image in the fields specified.
|
|
void get_hp_distrib_surface(const std::vector<std::pair<int, double> >& hp_prob_vector,
|
|
const battle_context::unit_stats& stats,
|
|
const battle_context::unit_stats& opp_stats,
|
|
surface& surf, int& width, int& height);
|
|
|
|
// This method blends a RGB color. The method takes as input a surface,
|
|
// the RGB color to blend and a value specifying how much blending to
|
|
// apply. The blended color is returned. Caution: if you use a
|
|
// transparent color, make sure the resulting color is not equal to the
|
|
// transparent color.
|
|
Uint32 blend_rgb(const surface& surf, unsigned char r, unsigned char g, unsigned char b, unsigned char drop);
|
|
};
|
|
|
|
const int battle_prediction_pane::inter_line_gap_ = 3;
|
|
const int battle_prediction_pane::inter_column_gap_ = 30;
|
|
const int battle_prediction_pane::inter_units_gap_ = 30;
|
|
const int battle_prediction_pane::max_hp_distrib_rows_ = 10;
|
|
|
|
battle_prediction_pane::battle_prediction_pane(game_display &disp, const battle_context& bc, const gamemap& map,
|
|
const std::vector<team>& teams, const unit_map& units,
|
|
const gamestatus& status, const game_data& gamedata,
|
|
const gamemap::location& attacker_loc, const gamemap::location& defender_loc)
|
|
: gui::preview_pane(disp.video()), disp_(disp), bc_(bc), map_(map), teams_(teams), units_(units), status_(status),
|
|
gamedata_(gamedata), attacker_loc_(attacker_loc), defender_loc_(defender_loc),
|
|
attacker_(units.find(attacker_loc)->second), defender_(units.find(defender_loc)->second)
|
|
{
|
|
// Predict the battle outcome.
|
|
combatant attacker_combatant(bc.get_attacker_stats());
|
|
combatant defender_combatant(bc.get_defender_stats());
|
|
attacker_combatant.fight(defender_combatant);
|
|
|
|
const battle_context::unit_stats& attacker_stats = bc.get_attacker_stats();
|
|
const battle_context::unit_stats& defender_stats = bc.get_defender_stats();
|
|
|
|
// Create the hitpoints distribution graphics.
|
|
std::vector<std::pair<int, double> > hp_prob_vector;
|
|
get_hp_prob_vector(attacker_combatant.hp_dist, hp_prob_vector);
|
|
get_hp_distrib_surface(hp_prob_vector, attacker_stats, defender_stats, attacker_hp_distrib_,
|
|
attacker_hp_distrib_width_, attacker_hp_distrib_height_);
|
|
get_hp_prob_vector(defender_combatant.hp_dist, hp_prob_vector);
|
|
get_hp_distrib_surface(hp_prob_vector, defender_stats, attacker_stats, defender_hp_distrib_,
|
|
defender_hp_distrib_width_, defender_hp_distrib_height_);
|
|
hp_distribs_height_ = maximum<int>(attacker_hp_distrib_height_, defender_hp_distrib_height_);
|
|
|
|
// Build the strings and compute the layout.
|
|
std::stringstream str;
|
|
|
|
attacker_label_ = _("Attacker");
|
|
defender_label_ = _("Defender");
|
|
attacker_label_width_ = font::line_width(attacker_label_, font::SIZE_PLUS, TTF_STYLE_BOLD);
|
|
defender_label_width_ = font::line_width(defender_label_, font::SIZE_PLUS, TTF_STYLE_BOLD);
|
|
|
|
// Get the units strings.
|
|
get_unit_strings(attacker_stats, attacker_, attacker_loc_, attacker_combatant.untouched,
|
|
defender_, defender_loc_, defender_stats.weapon,
|
|
attacker_left_strings_, attacker_right_strings_,
|
|
attacker_left_strings_width_, attacker_right_strings_width_, attacker_strings_width_);
|
|
|
|
get_unit_strings(defender_stats, defender_, defender_loc_, defender_combatant.untouched,
|
|
attacker_, attacker_loc_, attacker_stats.weapon,
|
|
defender_left_strings_, defender_right_strings_,
|
|
defender_left_strings_width_, defender_right_strings_width_, defender_strings_width_);
|
|
|
|
units_strings_height_ = maximum<int>(attacker_left_strings_.size(), defender_left_strings_.size())
|
|
* (font::SIZE_NORMAL + inter_line_gap_) + 14;
|
|
|
|
hp_distrib_string_ = _("Expected Battle Result (HP)");
|
|
hp_distrib_string_width_ = font::line_width(hp_distrib_string_, font::SIZE_SMALL);
|
|
|
|
attacker_width_ = maximum<int>(attacker_label_width_, attacker_strings_width_);
|
|
attacker_width_ = maximum<int>(attacker_width_, hp_distrib_string_width_);
|
|
attacker_width_ = maximum<int>(attacker_width_, attacker_hp_distrib_width_);
|
|
defender_width_ = maximum<int>(defender_label_width_, defender_strings_width_);
|
|
defender_width_ = maximum<int>(defender_width_, hp_distrib_string_width_);
|
|
defender_width_ = maximum<int>(defender_width_, defender_hp_distrib_width_);
|
|
units_width_ = maximum<int>(attacker_width_, defender_width_);
|
|
|
|
dialog_width_ = 2 * units_width_ + inter_units_gap_;
|
|
dialog_height_ = 15 + 24 + units_strings_height_ + 14 + 19 + hp_distribs_height_ + 18;
|
|
|
|
// Set the dialog size.
|
|
set_measurements(dialog_width_, dialog_height_);
|
|
}
|
|
|
|
void battle_prediction_pane::get_unit_strings(const battle_context::unit_stats& stats,
|
|
const unit& u, const gamemap::location& u_loc, float u_unscathed,
|
|
const unit& opp, const gamemap::location& opp_loc, const attack_type *opp_weapon,
|
|
std::vector<std::string>& left_strings, std::vector<std::string>& right_strings,
|
|
int& left_strings_width, int& right_strings_width, int& strings_width)
|
|
{
|
|
std::stringstream str;
|
|
char str_buf[10];
|
|
|
|
// With a weapon.
|
|
if(stats.weapon != NULL) {
|
|
|
|
// Set specials context (for safety, it should not have changed normally).
|
|
const attack_type *weapon = stats.weapon;
|
|
weapon->set_specials_context(u_loc, opp_loc, &gamedata_, &units_, &map_, &status_, &teams_, stats.is_attacker, opp_weapon);
|
|
|
|
// Get damage modifiers.
|
|
unit_ability_list dmg_specials = weapon->get_specials("damage");
|
|
unit_abilities::effect dmg_effect(dmg_specials, weapon->damage(), stats.backstab_pos);
|
|
|
|
// Get the SET damage modifier, if any.
|
|
const unit_abilities::individual_effect *set_dmg_effect = NULL;
|
|
unit_abilities::effect_list::const_iterator i;
|
|
for(i = dmg_effect.begin(); i != dmg_effect.end(); ++i) {
|
|
if(i->type == unit_abilities::SET) {
|
|
set_dmg_effect = &*i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Either user the SET modifier or the base weapon damage.
|
|
if(set_dmg_effect == NULL) {
|
|
left_strings.push_back(weapon->name());
|
|
str.str("");
|
|
str << weapon->damage();
|
|
right_strings.push_back(str.str());
|
|
} else {
|
|
left_strings.push_back((*set_dmg_effect->ability)["name"]);
|
|
str.str("");
|
|
str << set_dmg_effect->value;
|
|
right_strings.push_back(str.str());
|
|
}
|
|
|
|
// Process the ADD damage modifiers.
|
|
for(i = dmg_effect.begin(); i != dmg_effect.end(); ++i) {
|
|
if(i->type == unit_abilities::ADD) {
|
|
left_strings.push_back((*i->ability)["name"]);
|
|
str.str("");
|
|
if(i->value >= 0) str << "+" << i->value;
|
|
else str << i->value;
|
|
right_strings.push_back(str.str());
|
|
}
|
|
}
|
|
|
|
// Process the MUL damage modifiers.
|
|
for(i = dmg_effect.begin(); i != dmg_effect.end(); ++i) {
|
|
if(i->type == unit_abilities::MUL) {
|
|
left_strings.push_back((*i->ability)["name"]);
|
|
str.str("");
|
|
str << "* " << (i->value / 100);
|
|
if(i->value % 100) {
|
|
str << "." << ((i->value % 100) / 10);
|
|
if(i->value % 10) str << (i->value % 10);
|
|
}
|
|
right_strings.push_back(str.str());
|
|
}
|
|
}
|
|
|
|
// Resistance modifier.
|
|
int resistance_modifier = opp.damage_from(*weapon, !stats.is_attacker, opp_loc);
|
|
if(resistance_modifier != 100) {
|
|
str.str("");
|
|
if(stats.is_attacker) str << _("Defender");
|
|
else str << _("Attacker");
|
|
if(resistance_modifier < 100) str << _(" resistance vs ");
|
|
else str << _(" vulnerability vs ");
|
|
str << gettext(weapon->type().c_str());
|
|
left_strings.push_back(str.str());
|
|
str.str("");
|
|
str << "* " << (resistance_modifier / 100) << "." << ((resistance_modifier % 100) / 10);
|
|
right_strings.push_back(str.str());
|
|
}
|
|
|
|
// Slowed penalty.
|
|
if(stats.is_slowed) {
|
|
left_strings.push_back(_("Slowed"));
|
|
right_strings.push_back("* 0.5");
|
|
}
|
|
|
|
// Time of day modifier.
|
|
int tod_modifier = combat_modifier(status_, units_, u_loc, u.alignment(), u.is_fearless(), map_);
|
|
if(tod_modifier != 0) {
|
|
left_strings.push_back(_("Time of day"));
|
|
str.str("");
|
|
str << (tod_modifier > 0 ? "+" : "") << tod_modifier << "%";
|
|
right_strings.push_back(str.str());
|
|
}
|
|
|
|
// Leadership bonus.
|
|
int leadership_bonus = 0;
|
|
under_leadership(units_, u_loc, &leadership_bonus);
|
|
if(leadership_bonus != 0) {
|
|
left_strings.push_back(_("Leadership"));
|
|
str.str("");
|
|
str << "+" << leadership_bonus << "%";
|
|
right_strings.push_back(str.str());
|
|
}
|
|
|
|
// Total damage.
|
|
left_strings.push_back(_("Total damage"));
|
|
str.str("");
|
|
str << stats.damage << "-" << stats.num_blows << " (" << stats.chance_to_hit << "%)";
|
|
right_strings.push_back(str.str());
|
|
|
|
// Without a weapon.
|
|
} else {
|
|
left_strings.push_back(_("No usable weapon"));
|
|
right_strings.push_back("");
|
|
}
|
|
|
|
// Unscathed probability.
|
|
left_strings.push_back(_("Chance of being unscathed"));
|
|
snprintf(str_buf, 10, "%.1f%%", static_cast<float>(u_unscathed * 100.0));
|
|
str_buf[9] = '\0'; //prevents _snprintf error
|
|
right_strings.push_back(str_buf);
|
|
|
|
#if 0 // might not be en English!
|
|
// Fix capitalisation of left strings.
|
|
for(int i = 0; i < (int) left_strings.size(); i++)
|
|
if(left_strings[i].size() > 0) left_strings[i][0] = toupper(left_strings[i][0]);
|
|
#endif
|
|
|
|
// Compute the width of the strings.
|
|
left_strings_width = get_strings_max_length(left_strings);
|
|
right_strings_width = get_strings_max_length(right_strings);
|
|
strings_width = left_strings_width + inter_column_gap_ + right_strings_width;
|
|
}
|
|
|
|
int battle_prediction_pane::get_strings_max_length(const std::vector<std::string>& strings)
|
|
{
|
|
int max_len = 0;
|
|
|
|
for(int i = 0; i < static_cast<int>(strings.size()); i++)
|
|
max_len = maximum<int>(font::line_width(strings[i], font::SIZE_NORMAL), max_len);
|
|
|
|
return max_len;
|
|
}
|
|
|
|
void battle_prediction_pane::get_hp_prob_vector(const std::vector<double>& hp_dist,
|
|
std::vector<std::pair<int, double> >& hp_prob_vector)
|
|
{
|
|
hp_prob_vector.clear();
|
|
|
|
// First, we sort the probabilities in ascending order.
|
|
std::vector<std::pair<double, int> > prob_hp_vector;
|
|
int i;
|
|
|
|
for(i = 0; i < static_cast<int>(hp_dist.size()); i++) {
|
|
double prob = hp_dist[i];
|
|
|
|
// We keep only values above 0.1%.
|
|
if(prob > 0.001)
|
|
prob_hp_vector.push_back(std::pair<double, int>(prob, i));
|
|
}
|
|
|
|
std::sort(prob_hp_vector.begin(), prob_hp_vector.end());
|
|
|
|
// We store a few of the highest probability hitpoint values.
|
|
int nb_elem = minimum<int>(max_hp_distrib_rows_, prob_hp_vector.size());
|
|
|
|
for(i = prob_hp_vector.size() - nb_elem;
|
|
i < static_cast<int>(prob_hp_vector.size()); i++) {
|
|
|
|
hp_prob_vector.push_back(std::pair<int, double>
|
|
(prob_hp_vector[i].second, prob_hp_vector[i].first));
|
|
}
|
|
|
|
// Then, we sort the hitpoint values in ascending order.
|
|
std::sort(hp_prob_vector.begin(), hp_prob_vector.end());
|
|
}
|
|
|
|
void battle_prediction_pane::draw_contents()
|
|
{
|
|
// We must align both damage lines.
|
|
int damage_line_skip = maximum<int>(attacker_left_strings_.size(), defender_left_strings_.size()) - 2;
|
|
|
|
draw_unit(0, damage_line_skip,
|
|
attacker_left_strings_width_, attacker_left_strings_, attacker_right_strings_,
|
|
attacker_label_, attacker_label_width_, attacker_hp_distrib_, attacker_hp_distrib_width_);
|
|
|
|
draw_unit(units_width_ + inter_units_gap_, damage_line_skip,
|
|
defender_left_strings_width_, defender_left_strings_, defender_right_strings_,
|
|
defender_label_, defender_label_width_, defender_hp_distrib_, defender_hp_distrib_width_);
|
|
}
|
|
|
|
void battle_prediction_pane::draw_unit(int x_off, int damage_line_skip, int left_strings_width,
|
|
const std::vector<std::string>& left_strings,
|
|
const std::vector<std::string>& right_strings,
|
|
const std::string& label, int label_width,
|
|
surface& hp_distrib, int hp_distrib_width)
|
|
{
|
|
CVideo& screen = disp_.video();
|
|
int i;
|
|
|
|
// NOTE. A preview pane is not made to be used alone and it is not
|
|
// centered in the middle of the dialog. We "fix" this problem by moving
|
|
// the clip rectangle 10 pixels to the right. This is a kludge and it
|
|
// should be removed by 1) writing a custom dialog handler, or
|
|
// 2) modify preview_pane so that it accepts {left, middle, right} as
|
|
// layout possibilities.
|
|
|
|
// Get clip rectangle and center it
|
|
SDL_Rect clip_rect = location();
|
|
clip_rect.x += 10;
|
|
|
|
// Current vertical offset. We draw the dialog line-by-line, starting at the top.
|
|
int y_off = 15;
|
|
|
|
// Draw unit label.
|
|
font::draw_text_line(&screen, clip_rect, font::SIZE_15, font::NORMAL_COLOUR, label,
|
|
clip_rect.x + x_off + (units_width_ - label_width) / 2, clip_rect.y + y_off, 0, TTF_STYLE_BOLD);
|
|
|
|
y_off += 24;
|
|
|
|
// Draw unit left and right strings except the last two (total damage and unscathed probability).
|
|
for(i = 0; i < static_cast<int>(left_strings.size()) - 2; i++) {
|
|
font::draw_text_line(&screen, clip_rect, font::SIZE_NORMAL, font::NORMAL_COLOUR, left_strings[i],
|
|
clip_rect.x + x_off, clip_rect.y + y_off + (font::SIZE_NORMAL + inter_line_gap_) * i,
|
|
0, TTF_STYLE_NORMAL);
|
|
|
|
font::draw_text_line(&screen, clip_rect, font::SIZE_NORMAL, font::NORMAL_COLOUR, right_strings[i],
|
|
clip_rect.x + x_off + left_strings_width + inter_column_gap_,
|
|
clip_rect.y + y_off + (font::SIZE_NORMAL + inter_line_gap_) * i, 0, TTF_STYLE_NORMAL);
|
|
}
|
|
|
|
// Ensure both damage lines are aligned.
|
|
y_off += damage_line_skip * (font::SIZE_NORMAL + inter_line_gap_) + 14;
|
|
|
|
// Draw total damage and unscathed probability.
|
|
for(i = 0; i < 2; i++) {
|
|
const std::string& left_string = left_strings[left_strings.size() - 2 + i];
|
|
const std::string& right_string = right_strings[right_strings.size() - 2 + i];
|
|
|
|
font::draw_text_line(&screen, clip_rect, font::SIZE_NORMAL, font::NORMAL_COLOUR, left_string,
|
|
clip_rect.x + x_off, clip_rect.y + y_off + (font::SIZE_NORMAL + inter_line_gap_) * i,
|
|
0, TTF_STYLE_NORMAL);
|
|
|
|
font::draw_text_line(&screen, clip_rect, font::SIZE_NORMAL, font::NORMAL_COLOUR, right_string,
|
|
clip_rect.x + x_off + left_strings_width + inter_column_gap_,
|
|
clip_rect.y + y_off + (font::SIZE_NORMAL + inter_line_gap_) * i, 0, TTF_STYLE_NORMAL);
|
|
}
|
|
|
|
y_off += 2 * (font::SIZE_NORMAL + inter_line_gap_) + 14;
|
|
|
|
// Draw hitpoints distribution string.
|
|
font::draw_text(&screen, clip_rect, font::SIZE_SMALL, font::NORMAL_COLOUR, hp_distrib_string_,
|
|
clip_rect.x + x_off + (units_width_ - hp_distrib_string_width_) / 2, clip_rect.y + y_off);
|
|
|
|
y_off += 19;
|
|
|
|
// Draw hitpoints distributions.
|
|
video().blit_surface(clip_rect.x + x_off + (units_width_ - hp_distrib_width) / 2, clip_rect.y + y_off, hp_distrib);
|
|
}
|
|
|
|
void battle_prediction_pane::get_hp_distrib_surface(const std::vector<std::pair<int, double> >& hp_prob_vector,
|
|
const battle_context::unit_stats& stats,
|
|
const battle_context::unit_stats& opp_stats,
|
|
surface& surf, int& width, int& height)
|
|
{
|
|
// Font size. If you change this, you must update the separator space.
|
|
int fs = font::SIZE_SMALL;
|
|
|
|
// Space before HP separator.
|
|
int hp_sep = 24 + 6;
|
|
|
|
// Bar space between both separators.
|
|
int bar_space = 150;
|
|
|
|
// Space after percentage separator.
|
|
int percent_sep = 43 + 6;
|
|
|
|
// Surface width and height.
|
|
width = 30 + 2 + bar_space + 2 + percent_sep;
|
|
height = 5 + (fs + 2) * hp_prob_vector.size();
|
|
|
|
// Create the surface.
|
|
surf = SDL_CreateRGBSurface(SDL_SWSURFACE, width, height,
|
|
image::pixel_format->BitsPerPixel,
|
|
image::pixel_format->Rmask,
|
|
image::pixel_format->Gmask,
|
|
image::pixel_format->Bmask,
|
|
image::pixel_format->Amask);
|
|
|
|
SDL_Rect clip_rect = {0, 0, width, height};
|
|
Uint32 grey_color = SDL_MapRGB(surf->format, 0xb7, 0xc1, 0xc1);
|
|
Uint32 transparent_color = SDL_MapRGB(surf->format, 1, 1, 1);
|
|
|
|
// Enable transparency.
|
|
SDL_SetColorKey(surf, SDL_SRCCOLORKEY, transparent_color);
|
|
SDL_FillRect(surf, &clip_rect, transparent_color);
|
|
|
|
// Draw the surrounding borders and separators.
|
|
SDL_Rect top_border_rect = {0, 0, width, 2};
|
|
SDL_FillRect(surf, &top_border_rect, grey_color);
|
|
|
|
SDL_Rect bottom_border_rect = {0, height - 2, width, 2};
|
|
SDL_FillRect(surf, &bottom_border_rect, grey_color);
|
|
|
|
SDL_Rect left_border_rect = {0, 0, 2, height};
|
|
SDL_FillRect(surf, &left_border_rect, grey_color);
|
|
|
|
SDL_Rect right_border_rect = {width - 2, 0, 2, height};
|
|
SDL_FillRect(surf, &right_border_rect, grey_color);
|
|
|
|
SDL_Rect hp_sep_rect = {hp_sep, 0, 2, height};
|
|
SDL_FillRect(surf, &hp_sep_rect, grey_color);
|
|
|
|
SDL_Rect percent_sep_rect = {width - percent_sep - 2, 0, 2, height};
|
|
SDL_FillRect(surf, &percent_sep_rect, grey_color);
|
|
|
|
// Draw the rows (lower HP values are at the bottom).
|
|
for(int i = 0; i < static_cast<int>(hp_prob_vector.size()); i++) {
|
|
char str_buf[10];
|
|
|
|
// Get the HP and probability.
|
|
int hp = hp_prob_vector[hp_prob_vector.size() - i - 1].first;
|
|
double prob = hp_prob_vector[hp_prob_vector.size() - i - 1].second;
|
|
|
|
SDL_Color row_color;
|
|
|
|
// Death line is red.
|
|
if(hp == 0) {
|
|
SDL_Color color = {0xe5, 0, 0, 0};
|
|
row_color = color;
|
|
}
|
|
|
|
// Below current hitpoints value is orange.
|
|
else if(hp < static_cast<int>(stats.hp)) {
|
|
// Stone is grey.
|
|
if(opp_stats.stones) {
|
|
SDL_Color color = {0x9a, 0x9a, 0x9a, 0};
|
|
row_color = color;
|
|
} else {
|
|
SDL_Color color = {0xf4, 0xc9, 0, 0};
|
|
row_color = color;
|
|
}
|
|
}
|
|
|
|
// Current hitpoints value and above is green.
|
|
else {
|
|
SDL_Color color = {0x08, 0xca, 0, 0};
|
|
row_color = color;
|
|
}
|
|
|
|
// Print HP, aligned right.
|
|
snprintf(str_buf, 10, "%d", hp);
|
|
str_buf[9] = '\0'; //prevents _snprintf error
|
|
int hp_width = font::line_width(str_buf, fs);
|
|
|
|
// Draw bars.
|
|
font::draw_text_line(surf, clip_rect, fs, font::NORMAL_COLOUR, str_buf,
|
|
hp_sep - hp_width - 2, 2 + (fs + 2) * i, 0, TTF_STYLE_NORMAL);
|
|
|
|
int bar_len = maximum<int>(static_cast<int>((prob * (bar_space - 4)) + 0.5), 2);
|
|
|
|
SDL_Rect bar_rect_1 = {hp_sep + 4, 6 + (fs + 2) * i, bar_len, 8};
|
|
SDL_FillRect(surf, &bar_rect_1, blend_rgb(surf, row_color.r, row_color.g, row_color.b, 100));
|
|
|
|
SDL_Rect bar_rect_2 = {hp_sep + 4, 7 + (fs + 2) * i, bar_len, 6};
|
|
SDL_FillRect(surf, &bar_rect_2, blend_rgb(surf, row_color.r, row_color.g, row_color.b, 66));
|
|
|
|
SDL_Rect bar_rect_3 = {hp_sep + 4, 8 + (fs + 2) * i, bar_len, 4};
|
|
SDL_FillRect(surf, &bar_rect_3, blend_rgb(surf, row_color.r, row_color.g, row_color.b, 33));
|
|
|
|
SDL_Rect bar_rect_4 = {hp_sep + 4, 9 + (fs + 2) * i, bar_len, 2};
|
|
SDL_FillRect(surf, &bar_rect_4, blend_rgb(surf, row_color.r, row_color.g, row_color.b, 0));
|
|
|
|
// Draw probability percentage, aligned right.
|
|
const char *prob_str_format = NULL;
|
|
|
|
if(prob > 0.9995) prob_str_format = "100 %%";
|
|
else if(prob >= 0.1) prob_str_format = "%4.1f %%";
|
|
else prob_str_format = " %3.1f %%";
|
|
|
|
snprintf(str_buf, 10, prob_str_format, static_cast<float>(100.0 * (prob + 0.0005)));
|
|
str_buf[9] = '\0'; //prevents _snprintf error
|
|
int prob_width = font::line_width(str_buf, fs);
|
|
font::draw_text_line(surf, clip_rect, fs, font::NORMAL_COLOUR, str_buf,
|
|
width - prob_width - 4, 2 + (fs + 2) * i, 0, TTF_STYLE_NORMAL);
|
|
}
|
|
}
|
|
|
|
Uint32 battle_prediction_pane::blend_rgb(const surface& surf, unsigned char r, unsigned char g, unsigned char b, unsigned char drop)
|
|
{
|
|
// We simply decrement each component.
|
|
if(r < drop) r = 0; else r -= drop;
|
|
if(g < drop) g = 0; else g -= drop;
|
|
if(b < drop) b = 0; else b -= drop;
|
|
|
|
return SDL_MapRGB(surf->format, r, g, b);
|
|
}
|
|
|
|
// This class is used when the user clicks on the button
|
|
// to show the "Damage Calculations" dialog.
|
|
class attack_prediction_displayer : public gui::dialog_button_action
|
|
{
|
|
public:
|
|
attack_prediction_displayer(game_display& disp, const std::vector<battle_context>& bc_vector, const gamemap& map,
|
|
const std::vector<team>& teams, const unit_map& units,
|
|
const gamestatus& status, const game_data& gamedata,
|
|
const gamemap::location& attacker_loc, const gamemap::location& defender_loc)
|
|
: disp_(disp), bc_vector_(bc_vector), map_(map), teams_(teams), units_(units), status_(status),
|
|
gamedata_(gamedata), attacker_loc_(attacker_loc), defender_loc_(defender_loc) {}
|
|
|
|
// This method is called when the button is pressed.
|
|
RESULT button_pressed(int selection)
|
|
{
|
|
// Get the selected weapon, if any.
|
|
const size_t index = size_t(selection);
|
|
|
|
if(index < bc_vector_.size()) {
|
|
battle_prediction_pane battle_pane(disp_, bc_vector_[index], map_, teams_, units_, status_,
|
|
gamedata_, attacker_loc_, defender_loc_);
|
|
std::vector<gui::preview_pane*> preview_panes;
|
|
preview_panes.push_back(&battle_pane);
|
|
|
|
gui::show_dialog(disp_, NULL, _("Damage Calculations"), "", gui::OK_ONLY, NULL, &preview_panes);
|
|
}
|
|
|
|
return gui::CONTINUE_DIALOG;
|
|
}
|
|
|
|
private:
|
|
game_display &disp_;
|
|
const std::vector<battle_context>& bc_vector_;
|
|
const gamemap& map_;
|
|
const std::vector<team>& teams_;
|
|
const unit_map& units_;
|
|
const gamestatus& status_;
|
|
const game_data& gamedata_;
|
|
const gamemap::location& attacker_loc_;
|
|
const gamemap::location& defender_loc_;
|
|
};
|
|
} //end anonymous namespace
|
|
|
|
mouse_handler::mouse_handler(game_display* gui, std::vector<team>& teams, unit_map& units, gamemap& map,
|
|
gamestatus& status, const game_data& gameinfo, undo_list& undo_stack, undo_list& redo_stack, game_state& game_state):
|
|
gui_(gui), teams_(teams), units_(units), map_(map), status_(status), gameinfo_(gameinfo),
|
|
undo_stack_(undo_stack), redo_stack_(redo_stack), game_state_(game_state)
|
|
{
|
|
minimap_scrolling_ = false;
|
|
dragging_ = false;
|
|
dragging_started_ = false;
|
|
drag_from_x_ = 0;
|
|
drag_from_y_ = 0;
|
|
enemy_paths_ = false;
|
|
path_turns_ = 0;
|
|
undo_ = false;
|
|
show_menu_ = false;
|
|
over_route_ = false;
|
|
team_num_ = 1;
|
|
}
|
|
|
|
void mouse_handler::set_team(const int team_number)
|
|
{
|
|
team_num_ = team_number;
|
|
}
|
|
|
|
void mouse_handler::mouse_motion(const SDL_MouseMotionEvent& event, const bool browse)
|
|
{
|
|
mouse_motion(event.x,event.y, browse);
|
|
}
|
|
|
|
void mouse_handler::mouse_update(const bool browse)
|
|
{
|
|
int x, y;
|
|
SDL_GetMouseState(&x,&y);
|
|
mouse_motion(x, y, browse, true);
|
|
}
|
|
|
|
void mouse_handler::mouse_motion(int x, int y, const bool browse, bool update)
|
|
{
|
|
if(minimap_scrolling_) {
|
|
//if the game is run in a window, we could miss a LMB/MMB up event
|
|
// if it occurs outside our window.
|
|
// thus, we need to check if the LMB/MMB is still down
|
|
minimap_scrolling_ = ((SDL_GetMouseState(NULL,NULL) & (SDL_BUTTON(1) | SDL_BUTTON(2))) != 0);
|
|
if(minimap_scrolling_) {
|
|
const gamemap::location& loc = (*gui_).minimap_location_on(x,y);
|
|
if(loc.valid()) {
|
|
if(loc != last_hex_) {
|
|
last_hex_ = loc;
|
|
(*gui_).scroll_to_tile(loc,game_display::WARP,false);
|
|
}
|
|
} else {
|
|
// clicking outside of the minimap will end minimap scrolling
|
|
minimap_scrolling_ = false;
|
|
}
|
|
}
|
|
if(minimap_scrolling_) return;
|
|
}
|
|
|
|
const gamemap::location new_hex = (*gui_).hex_clicked_on(x,y);
|
|
|
|
// Fire the drag & drop only after minimal drag distance
|
|
// While we check the mouse buttons state, we also grab fresh position data.
|
|
int mx = drag_from_x_; // some default value to prevent unlikely SDL bug
|
|
int my = drag_from_y_;
|
|
if (dragging_ && !dragging_started_ && (SDL_GetMouseState(&mx,&my) & SDL_BUTTON_LEFT != 0)) {
|
|
const double drag_distance = pow(drag_from_x_- mx, 2) + pow(drag_from_y_- my, 2);
|
|
if (drag_distance > drag_threshold*drag_threshold) {
|
|
dragging_started_ = true;
|
|
cursor::set_dragging(true);
|
|
}
|
|
}
|
|
|
|
if(new_hex != last_hex_) {
|
|
update = true;
|
|
|
|
if (last_hex_.valid()) {
|
|
// we store the previous hexes used to propose attack direction
|
|
previous_hex_ = last_hex_;
|
|
// the hex of the selected unit is also "free"
|
|
if (last_hex_ == selected_hex_ || find_unit(last_hex_) == units_.end()) {
|
|
previous_free_hex_ = last_hex_;
|
|
}
|
|
}
|
|
last_hex_ = new_hex;
|
|
}
|
|
|
|
if (update) {
|
|
if(new_hex.valid() == false) {
|
|
current_route_.steps.clear();
|
|
(*gui_).set_route(NULL);
|
|
}
|
|
|
|
(*gui_).highlight_hex(new_hex);
|
|
|
|
const unit_map::iterator selected_unit = find_unit(selected_hex_);
|
|
const unit_map::iterator mouseover_unit = find_unit(new_hex);
|
|
|
|
// we search if there is an attack possibility and where
|
|
gamemap::location attack_from = current_unit_attacks_from(new_hex);
|
|
|
|
//see if we should show the normal cursor, the movement cursor, or
|
|
//the attack cursor
|
|
//If the cursor is on WAIT, we don't change it and let the setter
|
|
//of this state end it
|
|
if (cursor::get() != cursor::WAIT) {
|
|
if(selected_unit != units_.end() && selected_unit->second.side() == team_num_
|
|
&& !selected_unit->second.incapacitated() && !browse) {
|
|
if (attack_from.valid()) {
|
|
cursor::set(dragging_started_ ? cursor::ATTACK_DRAG : cursor::ATTACK);
|
|
} else if (mouseover_unit==units_.end() && current_paths_.routes.count(new_hex)) {
|
|
cursor::set(dragging_started_ ? cursor::MOVE_DRAG : cursor::MOVE);
|
|
} else {
|
|
// selecte unit can't attack or move there
|
|
cursor::set(cursor::NORMAL);
|
|
}
|
|
} else {
|
|
// no selected unit or we can't move it
|
|
cursor::set(cursor::NORMAL);
|
|
}
|
|
}
|
|
|
|
// show (or cancel) the attack direction indicator
|
|
if (attack_from.valid() && !browse) {
|
|
gui_->set_attack_indicator(attack_from, new_hex);
|
|
} else {
|
|
gui_->clear_attack_indicator();
|
|
}
|
|
|
|
if(enemy_paths_) {
|
|
enemy_paths_ = false;
|
|
current_paths_ = paths();
|
|
gui_->unhighlight_reach();
|
|
} else if(over_route_) {
|
|
over_route_ = false;
|
|
current_route_.steps.clear();
|
|
(*gui_).set_route(NULL);
|
|
}
|
|
|
|
// the destination is the pointed hex or the adjacent hex
|
|
// used to attack it
|
|
gamemap::location dest;
|
|
unit_map::const_iterator dest_un;
|
|
if (attack_from.valid()) {
|
|
dest = attack_from;
|
|
dest_un = find_unit(dest);
|
|
} else {
|
|
dest = new_hex;
|
|
dest_un = mouseover_unit;
|
|
}
|
|
|
|
if(dest == selected_hex_ || dest_un != units_.end()) {
|
|
current_route_.steps.clear();
|
|
(*gui_).set_route(NULL);
|
|
} else if(!current_paths_.routes.empty() && map_.on_board(selected_hex_) &&
|
|
map_.on_board(new_hex)) {
|
|
|
|
if(selected_unit != units_.end() && !selected_unit->second.incapacitated()) {
|
|
// the movement_reset is active only if it's not the unit's turn
|
|
unit_movement_resetter move_reset(selected_unit->second,
|
|
selected_unit->second.side() != team_num_);
|
|
current_route_ = get_route(selected_unit, dest, viewing_team());
|
|
if(!browse) {
|
|
(*gui_).set_route(¤t_route_);
|
|
}
|
|
}
|
|
}
|
|
|
|
unit_map::iterator un = mouseover_unit;
|
|
|
|
if(un != units_.end() && current_paths_.routes.empty() && !(*gui_).fogged(un->first)) {
|
|
if (un->second.side() != team_num_) {
|
|
//unit under cursor is not on our team, highlight reach
|
|
unit_movement_resetter move_reset(un->second);
|
|
|
|
const bool teleport = un->second.get_ability_bool("teleport",un->first);
|
|
current_paths_ = paths(map_,status_,gameinfo_,units_,new_hex,teams_,
|
|
false,teleport,viewing_team(),path_turns_);
|
|
gui_->highlight_reach(current_paths_);
|
|
enemy_paths_ = true;
|
|
} else {
|
|
//unit is on our team, show path if the unit has one
|
|
const gamemap::location go_to = un->second.get_goto();
|
|
if(map_.on_board(go_to)) {
|
|
paths::route route = get_route(un, go_to, current_team());
|
|
gui_->set_route(&route);
|
|
}
|
|
over_route_ = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
unit_map::iterator mouse_handler::selected_unit()
|
|
{
|
|
unit_map::iterator res = find_unit(selected_hex_);
|
|
if(res != units_.end()) {
|
|
return res;
|
|
} else {
|
|
return find_unit(last_hex_);
|
|
}
|
|
}
|
|
|
|
unit_map::iterator mouse_handler::find_unit(const gamemap::location& hex)
|
|
{
|
|
return find_visible_unit(units_,hex,map_,teams_,viewing_team());
|
|
}
|
|
|
|
unit_map::const_iterator mouse_handler::find_unit(const gamemap::location& hex) const
|
|
{
|
|
return find_visible_unit(units_,hex,map_,teams_,viewing_team());
|
|
}
|
|
|
|
gamemap::location mouse_handler::current_unit_attacks_from(const gamemap::location& loc)
|
|
{
|
|
const unit_map::const_iterator current = find_unit(selected_hex_);
|
|
if(current == units_.end() || current->second.side() != team_num_
|
|
|| current->second.attacks_left()==0) {
|
|
return gamemap::location();
|
|
}
|
|
|
|
const unit_map::const_iterator enemy = find_unit(loc);
|
|
if(enemy == units_.end() || current_team().is_enemy(enemy->second.side()) == false
|
|
|| enemy->second.incapacitated())
|
|
{
|
|
return gamemap::location();
|
|
}
|
|
|
|
const gamemap::location::DIRECTION preferred = loc.get_relative_dir(previous_hex_);
|
|
const gamemap::location::DIRECTION second_preferred = loc.get_relative_dir(previous_free_hex_);
|
|
|
|
int best_rating = 100;//smaller is better
|
|
gamemap::location res;
|
|
gamemap::location adj[6];
|
|
get_adjacent_tiles(loc,adj);
|
|
|
|
for(size_t n = 0; n != 6; ++n) {
|
|
if(map_.on_board(adj[n]) == false) {
|
|
continue;
|
|
}
|
|
|
|
if(adj[n] != selected_hex_ && find_unit(adj[n]) != units_.end()) {
|
|
continue;
|
|
}
|
|
|
|
if(current_paths_.routes.count(adj[n])) {
|
|
static const size_t NDIRECTIONS = gamemap::location::NDIRECTIONS;
|
|
unsigned int difference = abs(int(preferred - n));
|
|
if(difference > NDIRECTIONS/2) {
|
|
difference = NDIRECTIONS - difference;
|
|
}
|
|
unsigned int second_difference = abs(int(second_preferred - n));
|
|
if(second_difference > NDIRECTIONS/2) {
|
|
second_difference = NDIRECTIONS - second_difference;
|
|
}
|
|
const int rating = difference * 2 + (second_difference > difference);
|
|
if(rating < best_rating || res.valid() == false) {
|
|
best_rating = rating;
|
|
res = adj[n];
|
|
}
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
paths::route mouse_handler::get_route(unit_map::const_iterator un, gamemap::location go_to, team &team)
|
|
{
|
|
// The pathfinder will check unit visibility (fogged/stealthy).
|
|
const shortest_path_calculator calc(un->second,team,units_,teams_,map_);
|
|
|
|
std::set<gamemap::location> allowed_teleports;
|
|
|
|
if(un->second.get_ability_bool("teleport",un->first)) {
|
|
// search all known empty friendly villages
|
|
for(std::set<gamemap::location>::const_iterator i = team.villages().begin();
|
|
i != team.villages().end(); ++i) {
|
|
if (viewing_team().is_enemy(un->second.side()) && viewing_team().fogged(i->x,i->y))
|
|
continue;
|
|
|
|
unit_map::const_iterator occupant = find_unit(*i);
|
|
if (occupant != units_.end() && occupant != un)
|
|
continue;
|
|
|
|
allowed_teleports.insert(*i);
|
|
}
|
|
}
|
|
|
|
paths::route route = a_star_search(un->first, go_to, 10000.0, &calc, map_.w(), map_.h(), &allowed_teleports);
|
|
//FIXME: this is wrong, the function return a number of turns, not the moves left
|
|
//but we seems not to use it for the moment
|
|
route.move_left = route_turns_to_complete(un->second, route, viewing_team(), units_,teams_,map_);
|
|
|
|
return route;
|
|
}
|
|
|
|
void mouse_handler::mouse_press(const SDL_MouseButtonEvent& event, const bool browse)
|
|
{
|
|
show_menu_ = false;
|
|
mouse_update(browse);
|
|
int scrollx = 0;
|
|
int scrolly = 0;
|
|
|
|
if(is_left_click(event) && event.state == SDL_RELEASED) {
|
|
minimap_scrolling_ = false;
|
|
dragging_ = false;
|
|
cursor::set_dragging(false);
|
|
if (dragging_started_ && !browse && !commands_disabled) {
|
|
left_click(event, browse);
|
|
}
|
|
dragging_started_= false;
|
|
} else if(is_middle_click(event) && event.state == SDL_RELEASED) {
|
|
minimap_scrolling_ = false;
|
|
} else if(is_left_click(event) && event.state == SDL_PRESSED) {
|
|
left_click(event, browse);
|
|
if (!browse && !commands_disabled) {
|
|
dragging_ = true;
|
|
dragging_started_ = false;
|
|
SDL_GetMouseState(&drag_from_x_, &drag_from_y_);
|
|
}
|
|
} else if(is_right_click(event) && event.state == SDL_PRESSED) {
|
|
// The first right-click cancel the selection if any,
|
|
// the second open the context menu
|
|
dragging_ = false;
|
|
dragging_started_ = false;
|
|
cursor::set_dragging(false);
|
|
if (selected_hex_.valid() && find_unit(selected_hex_) != units_.end()) {
|
|
select_hex(gamemap::location(), browse);
|
|
} else {
|
|
gui_->draw(); // redraw highlight (and maybe some more)
|
|
const theme::menu* const m = gui_->get_theme().context_menu();
|
|
if (m != NULL)
|
|
show_menu_ = true;
|
|
else
|
|
LOG_STREAM(warn, display) << "no context menu found...\n";
|
|
}
|
|
} else if(is_middle_click(event) && event.state == SDL_PRESSED) {
|
|
// clicked on a hex on the minimap? then initiate minimap scrolling
|
|
const gamemap::location& loc = gui_->minimap_location_on(event.x,event.y);
|
|
minimap_scrolling_ = false;
|
|
if(loc.valid()) {
|
|
minimap_scrolling_ = true;
|
|
last_hex_ = loc;
|
|
gui_->scroll_to_tile(loc,game_display::WARP,false);
|
|
} else {
|
|
const SDL_Rect& rect = gui_->map_area();
|
|
const int centerx = (rect.x + rect.w)/2;
|
|
const int centery = (rect.y + rect.h)/2;
|
|
|
|
const int xdisp = event.x - centerx;
|
|
const int ydisp = event.y - centery;
|
|
|
|
gui_->scroll(xdisp,ydisp);
|
|
}
|
|
} else if (event.button == SDL_BUTTON_WHEELUP) {
|
|
scrolly = - preferences::scroll_speed();
|
|
} else if (event.button == SDL_BUTTON_WHEELDOWN) {
|
|
scrolly = preferences::scroll_speed();
|
|
} else if (event.button == SDL_BUTTON_WHEELLEFT) {
|
|
scrollx = - preferences::scroll_speed();
|
|
} else if (event.button == SDL_BUTTON_WHEELRIGHT) {
|
|
scrollx = preferences::scroll_speed();
|
|
}
|
|
|
|
if (scrollx != 0 || scrolly != 0) {
|
|
CKey pressed;
|
|
// Alt + mousewheel do an 90° rotation on the scroll direction
|
|
if (pressed[SDLK_LALT] || pressed[SDLK_RALT])
|
|
gui_->scroll(scrolly,scrollx);
|
|
else
|
|
gui_->scroll(scrollx,scrolly);
|
|
}
|
|
|
|
if (!dragging_ && dragging_started_) {
|
|
dragging_started_ = false;
|
|
cursor::set_dragging(false);
|
|
}
|
|
|
|
mouse_update(browse);
|
|
|
|
}
|
|
|
|
bool mouse_handler::is_left_click(const SDL_MouseButtonEvent& event)
|
|
{
|
|
return event.button == SDL_BUTTON_LEFT && !command_active();
|
|
}
|
|
|
|
bool mouse_handler::is_middle_click(const SDL_MouseButtonEvent& event)
|
|
{
|
|
return event.button == SDL_BUTTON_MIDDLE;
|
|
}
|
|
|
|
bool mouse_handler::is_right_click(const SDL_MouseButtonEvent& event)
|
|
{
|
|
return event.button == SDL_BUTTON_RIGHT || event.button == SDL_BUTTON_LEFT && command_active();
|
|
}
|
|
|
|
void mouse_handler::left_click(const SDL_MouseButtonEvent& event, const bool browse)
|
|
{
|
|
dragging_ = false;
|
|
dragging_started_ = false;
|
|
cursor::set_dragging(false);
|
|
undo_ = false;
|
|
bool check_shroud = teams_[team_num_ - 1].auto_shroud_updates();
|
|
|
|
// clicked on a hex on the minimap? then initiate minimap scrolling
|
|
const gamemap::location& loc = gui_->minimap_location_on(event.x,event.y);
|
|
minimap_scrolling_ = false;
|
|
if(loc.valid()) {
|
|
minimap_scrolling_ = true;
|
|
last_hex_ = loc;
|
|
gui_->scroll_to_tile(loc,game_display::WARP,false);
|
|
return;
|
|
}
|
|
|
|
//we use the last registered highlighted hex
|
|
//since it's what update our global state
|
|
gamemap::location hex = last_hex_;
|
|
|
|
unit_map::iterator u = find_unit(selected_hex_);
|
|
|
|
//if the unit is selected and then itself clicked on,
|
|
//any goto command is cancelled
|
|
if(u != units_.end() && !browse && selected_hex_ == hex && u->second.side() == team_num_) {
|
|
u->second.set_goto(gamemap::location());
|
|
}
|
|
|
|
unit_map::iterator clicked_u = find_unit(hex);
|
|
|
|
//if we can move to that tile
|
|
std::map<gamemap::location,paths::route>::const_iterator
|
|
route = enemy_paths_ ? current_paths_.routes.end() :
|
|
current_paths_.routes.find(hex);
|
|
|
|
const gamemap::location src = selected_hex_;
|
|
paths orig_paths = current_paths_;
|
|
const gamemap::location& attack_from = current_unit_attacks_from(hex);
|
|
|
|
//see if we're trying to do a attack or move-and-attack
|
|
if(!browse && !commands_disabled && attack_from.valid()) {
|
|
if (attack_from == selected_hex_) { //no move needed
|
|
if (attack_enemy(u, clicked_u) == false) {
|
|
return;
|
|
}
|
|
}
|
|
else if (move_unit_along_current_route(false)) {//move the unit without updating shroud
|
|
// a WML event could have invalidated both attacker and defender
|
|
// so make sure they're valid before attacking
|
|
u = find_unit(attack_from);
|
|
unit_map::iterator enemy = find_unit(hex);
|
|
if(u != units_.end() && u->second.side() == team_num_ &&
|
|
enemy != units_.end() && current_team().is_enemy(enemy->second.side()) && !enemy->second.incapacitated()) {
|
|
//if shroud or fog is active, rememember units and after attack check if someone isn't seen
|
|
std::set<gamemap::location> known_units;
|
|
|
|
if (teams_[team_num_-1].uses_shroud() || teams_[team_num_-1].uses_fog()){
|
|
for(unit_map::const_iterator u = units_.begin(); u != units_.end(); ++u) {
|
|
if(teams_[team_num_-1].fogged(u->first.x,u->first.y) == false) {
|
|
known_units.insert(u->first);
|
|
teams_[team_num_-1].see(u->second.side()-1);
|
|
}
|
|
}
|
|
}
|
|
if(!commands_disabled && attack_enemy(u,enemy) == false) {
|
|
undo_ = true;
|
|
selected_hex_ = src;
|
|
gui_->select_hex(src);
|
|
current_paths_ = orig_paths;
|
|
gui_->highlight_reach(current_paths_);
|
|
return;
|
|
}
|
|
else //attack == true
|
|
{
|
|
if (teams_[team_num_-1].uses_shroud() || teams_[team_num_-1].uses_fog()){
|
|
//check if some new part of map discovered or is active delay shroud updates, which need special care
|
|
if (clear_shroud(*gui_, status_, map_, gameinfo_, units_, teams_, team_num_ - 1)||!teams_[team_num_-1].auto_shroud_updates()){
|
|
clear_undo_stack();
|
|
gui_->invalidate_all();
|
|
gui_->draw();
|
|
//some new part of map discovered
|
|
for(unit_map::const_iterator u = units_.begin(); u != units_.end(); ++u) {
|
|
if(teams_[team_num_-1].fogged(u->first.x,u->first.y) == false) {
|
|
//check if unit is not known
|
|
if (known_units.find(u->first)==known_units.end())
|
|
{
|
|
game_events::raise("sighted",u->first,attack_from);
|
|
}
|
|
}
|
|
}
|
|
game_events::pump();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(check_shroud && clear_shroud(*gui_, status_, map_, gameinfo_, units_, teams_, team_num_ - 1)) {
|
|
clear_undo_stack();
|
|
gui_->invalidate_all();
|
|
gui_->draw();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//otherwise we're trying to move to a hex
|
|
else if(!commands_disabled && !browse && selected_hex_.valid() && selected_hex_ != hex &&
|
|
u != units_.end() && u->second.side() == team_num_ &&
|
|
clicked_u == units_.end() && !current_route_.steps.empty() &&
|
|
current_route_.steps.front() == selected_hex_) {
|
|
move_unit_along_current_route(check_shroud);
|
|
} else {
|
|
// we select a (maybe empty) hex
|
|
select_hex(hex, browse);
|
|
}
|
|
}
|
|
|
|
void mouse_handler::select_hex(const gamemap::location& hex, const bool browse) {
|
|
selected_hex_ = hex;
|
|
gui_->select_hex(hex);
|
|
gui_->clear_attack_indicator();
|
|
gui_->set_route(NULL);
|
|
|
|
unit_map::iterator u = find_unit(hex);
|
|
if(hex.valid() && u != units_.end() ) {
|
|
next_unit_ = u->first;
|
|
|
|
// if it's not the unit's turn, we reset its moves
|
|
unit_movement_resetter move_reset(u->second, u->second.side() != team_num_);
|
|
const bool teleport = u->second.get_ability_bool("teleport",u->first);
|
|
current_paths_ = paths(map_,status_,gameinfo_,units_,hex,teams_,
|
|
false,teleport,viewing_team(),path_turns_);
|
|
show_attack_options(u);
|
|
gui_->highlight_reach(current_paths_);
|
|
// the highlight now comes from selection
|
|
// and not from the mouseover on an enemy
|
|
enemy_paths_ = false;
|
|
gui_->set_route(NULL);
|
|
|
|
// selection have impact only if we are not observing and it's our unit
|
|
if (!browse && u->second.side() == gui_->viewing_team()+1) {
|
|
sound::play_UI_sound("select-unit.wav");
|
|
u->second.set_selecting(*gui_, u->first);
|
|
|
|
game_events::fire("select", selected_hex_);
|
|
}
|
|
|
|
} else {
|
|
gui_->unhighlight_reach();
|
|
current_paths_ = paths();
|
|
current_route_.steps.clear();
|
|
}
|
|
}
|
|
|
|
void mouse_handler::clear_undo_stack()
|
|
{
|
|
if(teams_[team_num_ - 1].auto_shroud_updates() == false)
|
|
apply_shroud_changes(undo_stack_,gui_,status_,map_,gameinfo_,units_,teams_,team_num_-1);
|
|
undo_stack_.clear();
|
|
}
|
|
|
|
bool mouse_handler::move_unit_along_current_route(bool check_shroud)
|
|
{
|
|
const std::vector<gamemap::location> steps = current_route_.steps;
|
|
if(steps.empty()) {
|
|
return false;
|
|
}
|
|
|
|
const size_t moves = ::move_unit(gui_,gameinfo_,status_,map_,units_,teams_,
|
|
steps,&recorder,&undo_stack_,&next_unit_,false,check_shroud);
|
|
|
|
cursor::set(cursor::NORMAL);
|
|
|
|
gui_->invalidate_game_status();
|
|
|
|
selected_hex_ = gamemap::location();
|
|
gui_->select_hex(gamemap::location());
|
|
|
|
gui_->set_route(NULL);
|
|
gui_->unhighlight_reach();
|
|
current_paths_ = paths();
|
|
|
|
if(moves == 0)
|
|
return false;
|
|
|
|
redo_stack_.clear();
|
|
|
|
wassert(moves <= steps.size());
|
|
const gamemap::location& dst = steps[moves-1];
|
|
const unit_map::const_iterator u = units_.find(dst);
|
|
|
|
//u may be equal to units_.end() in the case of e.g. a [teleport]
|
|
if(u != units_.end()) {
|
|
//Reselect the unit if the move was interrupted
|
|
if(dst != steps.back()) {
|
|
selected_hex_ = dst;
|
|
gui_->select_hex(dst);
|
|
}
|
|
|
|
current_route_.steps.clear();
|
|
|
|
//check if we are now adjacent to an enemy,
|
|
//we reselect the unit (old 1.2.x behavior)
|
|
gamemap::location adj[6];
|
|
get_adjacent_tiles(dst,adj);
|
|
for(int i = 0; i != 6; i++) {
|
|
unit_map::iterator adj_unit = find_unit(adj[i]);
|
|
if (adj_unit != units_.end() && current_team().is_enemy(adj_unit->second.side())) {
|
|
selected_hex_ = dst;
|
|
gui_->select_hex(dst);
|
|
const bool teleport = u->second.get_ability_bool("teleport",u->first);
|
|
current_paths_ = paths(map_,status_,gameinfo_,units_,dst,teams_,
|
|
false,teleport,viewing_team(),path_turns_);
|
|
show_attack_options(u);
|
|
gui_->highlight_reach(current_paths_);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return moves == steps.size();
|
|
}
|
|
|
|
bool mouse_handler::attack_enemy(unit_map::iterator attacker, unit_map::iterator defender)
|
|
{
|
|
try {
|
|
return attack_enemy_(attacker, defender);
|
|
} catch(std::bad_alloc) {
|
|
lg::wml_error << "Memory exhausted a unit has either a lot hitpoints or a negative amount.\n";
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
bool mouse_handler::attack_enemy_(unit_map::iterator attacker, unit_map::iterator defender)
|
|
{
|
|
//we must get locations by value instead of by references, because the iterators
|
|
//may become invalidated later
|
|
const gamemap::location attacker_loc = attacker->first;
|
|
const gamemap::location defender_loc = defender->first;
|
|
|
|
std::vector<std::string> items;
|
|
|
|
std::vector<battle_context> bc_vector;
|
|
unsigned int i, best = 0;
|
|
for (i = 0; i < attacker->second.attacks().size(); i++) {
|
|
// skip weapons with attack_weight=0
|
|
if (attacker->second.attacks()[i].attack_weight() > 0) {
|
|
battle_context bc(map_, teams_, units_, status_, gameinfo_, attacker->first, defender->first, i);
|
|
bc_vector.push_back(bc);
|
|
if (bc.better_attack(bc_vector[best], 0.5)) {
|
|
best = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < bc_vector.size(); i++) {
|
|
const battle_context::unit_stats& att = bc_vector[i].get_attacker_stats();
|
|
const battle_context::unit_stats& def = bc_vector[i].get_defender_stats();
|
|
config tmp_config;
|
|
attack_type no_weapon(tmp_config, "fake_attack", false);
|
|
const attack_type& attw = attack_type(*att.weapon);
|
|
const attack_type& defw = attack_type(def.weapon ? *def.weapon : no_weapon);
|
|
|
|
//if there is an attack special or defend special, we output a single space for the other unit, to make sure
|
|
//that the attacks line up nicely.
|
|
std::string special_pad = "";
|
|
if (!attw.weapon_specials().empty() || !defw.weapon_specials().empty())
|
|
special_pad = " ";
|
|
|
|
std::stringstream atts;
|
|
if (i == best) {
|
|
atts << DEFAULT_ITEM;
|
|
}
|
|
atts << IMAGE_PREFIX << attw.icon() << COLUMN_SEPARATOR
|
|
<< font::BOLD_TEXT << attw.name() << "\n" << att.damage << "-"
|
|
<< att.num_blows << " " << (attw.range().empty() ? "" : gettext(attw.range().c_str())) << " (" << att.chance_to_hit << "%)\n"
|
|
<< attw.weapon_specials() << special_pad
|
|
<< COLUMN_SEPARATOR << _("vs") << COLUMN_SEPARATOR
|
|
<< font::BOLD_TEXT << defw.name() << "\n" << def.damage << "-"
|
|
<< def.num_blows << " " << (defw.range().empty() ? "" : gettext(defw.range().c_str())) << " (" << def.chance_to_hit << "%)\n"
|
|
<< defw.weapon_specials() << special_pad << COLUMN_SEPARATOR
|
|
<< IMAGE_PREFIX << defw.icon();
|
|
|
|
items.push_back(atts.str());
|
|
}
|
|
|
|
//make it so that when we attack an enemy, the attacking unit
|
|
//is again shown in the status bar, so that we can easily
|
|
//compare between the attacking and defending unit
|
|
gui_->highlight_hex(gamemap::location());
|
|
gui_->draw(true,true);
|
|
|
|
attack_prediction_displayer ap_displayer(*gui_, bc_vector, map_, teams_, units_, status_, gameinfo_, attacker_loc, defender_loc);
|
|
std::vector<gui::dialog_button_info> buttons;
|
|
buttons.push_back(gui::dialog_button_info(&ap_displayer, _("Damage Calculations")));
|
|
|
|
int res = 0;
|
|
|
|
{
|
|
dialogs::unit_preview_pane attacker_preview(*gui_,&map_,attacker->second,dialogs::unit_preview_pane::SHOW_BASIC,true);
|
|
dialogs::unit_preview_pane defender_preview(*gui_,&map_,defender->second,dialogs::unit_preview_pane::SHOW_BASIC,false);
|
|
std::vector<gui::preview_pane*> preview_panes;
|
|
preview_panes.push_back(&attacker_preview);
|
|
preview_panes.push_back(&defender_preview);
|
|
|
|
res = gui::show_dialog(*gui_,NULL,_("Attack Enemy"),
|
|
_("Choose weapon:")+std::string("\n"),
|
|
gui::OK_CANCEL,&items,&preview_panes,"",NULL,-1,NULL,-1,-1,
|
|
NULL,&buttons);
|
|
}
|
|
|
|
cursor::set(cursor::NORMAL);
|
|
if(size_t(res) < bc_vector.size()) {
|
|
const battle_context::unit_stats &att = bc_vector[res].get_attacker_stats();
|
|
const battle_context::unit_stats &def = bc_vector[res].get_defender_stats();
|
|
|
|
attacker->second.set_goto(gamemap::location());
|
|
clear_undo_stack();
|
|
redo_stack_.clear();
|
|
|
|
current_paths_ = paths();
|
|
gui_->clear_attack_indicator();
|
|
gui_->unhighlight_reach();
|
|
|
|
gui_->draw();
|
|
|
|
const bool defender_human = teams_[defender->second.side()-1].is_human();
|
|
|
|
recorder.add_attack(attacker_loc,defender_loc,att.attack_num,def.attack_num);
|
|
|
|
//MP_COUNTDOWN grant time bonus for attacking
|
|
current_team().set_action_bonus_count(1 + current_team().action_bonus_count());
|
|
|
|
try {
|
|
attack(*gui_,map_,teams_,attacker_loc,defender_loc,att.attack_num,def.attack_num,units_,status_,gameinfo_);
|
|
} catch(end_level_exception&) {
|
|
//if the level ends due to a unit being killed, still see if
|
|
//either the attacker or defender should advance
|
|
dialogs::advance_unit(gameinfo_,map_,units_,attacker_loc,*gui_);
|
|
dialogs::advance_unit(gameinfo_,map_,units_,defender_loc,*gui_,!defender_human);
|
|
throw;
|
|
}
|
|
|
|
dialogs::advance_unit(gameinfo_,map_,units_,attacker_loc,*gui_);
|
|
dialogs::advance_unit(gameinfo_,map_,units_,defender_loc,*gui_,!defender_human);
|
|
|
|
selected_hex_ = gamemap::location();
|
|
current_route_.steps.clear();
|
|
gui_->set_route(NULL);
|
|
|
|
check_victory(units_,teams_);
|
|
|
|
gui_->draw();
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void mouse_handler::show_attack_options(unit_map::const_iterator u)
|
|
{
|
|
team& current_team = teams_[team_num_-1];
|
|
|
|
if(u == units_.end() || u->second.attacks_left() == 0)
|
|
return;
|
|
|
|
for(unit_map::const_iterator target = units_.begin(); target != units_.end(); ++target) {
|
|
if(current_team.is_enemy(target->second.side()) &&
|
|
distance_between(target->first,u->first) == 1 && !target->second.incapacitated()) {
|
|
current_paths_.routes[target->first] = paths::route();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool mouse_handler::unit_in_cycle(unit_map::const_iterator it)
|
|
{
|
|
if(it->second.side() == team_num_ && unit_can_move(it->first,units_,map_,teams_) && it->second.user_end_turn() == false && !gui_->fogged(it->first)) {
|
|
bool is_enemy = current_team().is_enemy(int(gui_->viewing_team()+1));
|
|
return is_enemy == false || it->second.invisible(it->first,units_,teams_) == false;
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
#define LOCAL_VARIABLES \
|
|
unit_map::const_iterator it = units_.find(next_unit_);\
|
|
const unit_map::const_iterator itx = it;\
|
|
const unit_map::const_iterator begin = units_.begin();\
|
|
const unit_map::const_iterator end = units_.end()
|
|
|
|
void mouse_handler::cycle_units(const bool browse)
|
|
{
|
|
LOCAL_VARIABLES;
|
|
|
|
if (it == end) {
|
|
for (it = begin; it != end && !unit_in_cycle(it); ++it);
|
|
} else {
|
|
do {
|
|
++it;
|
|
if (it == end) it = begin;
|
|
} while (it != itx && !unit_in_cycle(it));
|
|
}
|
|
if (it!=itx) {
|
|
gui_->scroll_to_tile(it->first,game_display::WARP);
|
|
select_hex(it->first, browse);
|
|
} else {
|
|
next_unit_ = gamemap::location();
|
|
}
|
|
|
|
mouse_update(browse);
|
|
}
|
|
|
|
void mouse_handler::cycle_back_units(const bool browse)
|
|
{
|
|
LOCAL_VARIABLES;
|
|
|
|
if (it == end) {
|
|
while (it != begin && !unit_in_cycle(--it));
|
|
if (!unit_in_cycle(it)) it = itx;
|
|
} else {
|
|
do {
|
|
if (it == begin) it = end;
|
|
--it;
|
|
} while (it != itx && !unit_in_cycle(it));
|
|
}
|
|
|
|
if (it!=itx) {
|
|
gui_->scroll_to_tile(it->first,game_display::WARP);
|
|
select_hex(it->first, browse);
|
|
} else {
|
|
next_unit_ = gamemap::location();
|
|
}
|
|
|
|
mouse_update(browse);
|
|
}
|
|
|
|
void mouse_handler::set_current_paths(paths new_paths) {
|
|
gui_->unhighlight_reach();
|
|
current_paths_ = new_paths;
|
|
current_route_.steps.clear();
|
|
gui_->set_route(NULL);
|
|
}
|
|
|
|
}
|