2736 lines
75 KiB
C++
2736 lines
75 KiB
C++
/*
|
|
Copyright (C) 2003 - 2018 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
|
|
* Routines to manage units.
|
|
*/
|
|
|
|
#include "units/unit.hpp"
|
|
|
|
#include "color.hpp"
|
|
#include "deprecation.hpp"
|
|
#include "display.hpp"
|
|
#include "formatter.hpp"
|
|
#include "formula/string_utils.hpp" // for VGETTEXT
|
|
#include "game_board.hpp" // for game_board
|
|
#include "game_config.hpp" // for add_color_info, etc
|
|
#include "game_data.hpp"
|
|
#include "game_errors.hpp" // for game_error
|
|
#include "game_events/manager.hpp" // for add_events
|
|
#include "preferences/game.hpp" // for encountered_units
|
|
#include "gettext.hpp" // for N_
|
|
#include "lexical_cast.hpp"
|
|
#include "log.hpp" // for LOG_STREAM, logger, etc
|
|
#include "map/map.hpp" // for gamemap
|
|
#include "random.hpp" // for generator, rng
|
|
#include "resources.hpp" // for units, gameboard, teams, etc
|
|
#include "scripting/game_lua_kernel.hpp" // for game_lua_kernel
|
|
#include "side_filter.hpp" // for side_filter
|
|
#include "synced_context.hpp"
|
|
#include "team.hpp" // for team, get_teams, etc
|
|
#include "terrain/filter.hpp" // for terrain_filter
|
|
#include "units/abilities.hpp" // for effect, filter_base_matches
|
|
#include "units/animation.hpp" // for unit_animation
|
|
#include "units/animation_component.hpp" // for unit_animation_component
|
|
#include "units/filter.hpp"
|
|
#include "units/formula_manager.hpp" // for unit_formula_manager
|
|
#include "units/id.hpp"
|
|
#include "units/map.hpp" // for unit_map, etc
|
|
#include "variable.hpp" // for vconfig, etc
|
|
#include "game_version.hpp"
|
|
|
|
#include "utils/functional.hpp"
|
|
#include <boost/dynamic_bitset.hpp>
|
|
#include <boost/function_output_iterator.hpp>
|
|
|
|
#ifdef _MSC_VER
|
|
#pragma warning (push)
|
|
#pragma warning (disable: 4510 4610)
|
|
#endif
|
|
#include <boost/range/algorithm.hpp>
|
|
#ifdef _MSC_VER
|
|
#pragma warning (pop)
|
|
#endif
|
|
|
|
#include <cassert> // for assert
|
|
#include <cstdlib> // for rand
|
|
#include <exception> // for exception
|
|
#include <iterator> // for back_insert_iterator, etc
|
|
#include <new> // for operator new
|
|
#include <ostream> // for operator<<, basic_ostream, etc
|
|
|
|
namespace t_translation { struct terrain_code; }
|
|
|
|
static lg::log_domain log_unit("unit");
|
|
#define DBG_UT LOG_STREAM(debug, log_unit)
|
|
#define LOG_UT LOG_STREAM(info, log_unit)
|
|
#define WRN_UT LOG_STREAM(warn, log_unit)
|
|
#define ERR_UT LOG_STREAM(err, log_unit)
|
|
|
|
namespace
|
|
{
|
|
// "advance" only kept around for backwards compatibility; only "advancement" should be used
|
|
const std::array<std::string, 4> ModificationTypes {{ "advancement", "advance", "trait", "object" }};
|
|
|
|
/**
|
|
* Pointers to units which have data in their internal caches. The
|
|
* destructor of an unit removes itself from the cache, so the pointers are
|
|
* always valid.
|
|
*/
|
|
static std::vector<const unit*> units_with_cache;
|
|
|
|
static const std::string leader_crown_path = "misc/leader-crown.png";
|
|
static std::string internalized_attrs[] {
|
|
"type",
|
|
"id",
|
|
"name",
|
|
"gender",
|
|
"random_gender",
|
|
"variation",
|
|
"role",
|
|
"ai_special",
|
|
"side",
|
|
"underlying_id",
|
|
"overlays",
|
|
"facing",
|
|
"race",
|
|
"level",
|
|
"recall_cost",
|
|
"undead_variation",
|
|
"max_attacks",
|
|
"attacks_left",
|
|
"alpha",
|
|
"zoc",
|
|
"flying",
|
|
"cost",
|
|
"max_hitpoints",
|
|
"max_moves",
|
|
"vision",
|
|
"jamming",
|
|
"max_experience",
|
|
"advances_to",
|
|
"hitpoints",
|
|
"goto_x",
|
|
"goto_y",
|
|
"moves",
|
|
"experience",
|
|
"resting",
|
|
"unrenamable",
|
|
"alignment",
|
|
"canrecruit",
|
|
"extra_recruit",
|
|
"x",
|
|
"y",
|
|
"placement",
|
|
"parent_type",
|
|
"description",
|
|
"usage",
|
|
"halo",
|
|
"ellipse",
|
|
"upkeep",
|
|
"random_traits",
|
|
"generate_name",
|
|
"profile",
|
|
"small_profile",
|
|
"fire_event",
|
|
"passable",
|
|
"overwrite",
|
|
"location_id",
|
|
"hidden",
|
|
// Useless attributes created when saving units to WML:
|
|
"flag_rgb",
|
|
"language_name",
|
|
"image",
|
|
"image_icon"
|
|
};
|
|
|
|
struct internalized_attrs_sorter
|
|
{
|
|
internalized_attrs_sorter()
|
|
{
|
|
std::sort(std::begin(internalized_attrs), std::end(internalized_attrs));
|
|
}
|
|
};
|
|
|
|
// Sort the array to make set_difference below work.
|
|
internalized_attrs_sorter sorter;
|
|
|
|
void warn_unknown_attribute(const config::const_attr_itors& cfg)
|
|
{
|
|
config::const_attribute_iterator cur = cfg.begin();
|
|
config::const_attribute_iterator end = cfg.end();
|
|
|
|
const std::string* cur_known = std::begin(internalized_attrs);
|
|
const std::string* end_known = std::end(internalized_attrs);
|
|
|
|
while(cur_known != end_known) {
|
|
if(cur == end) {
|
|
return;
|
|
}
|
|
int comp = cur->first.compare(*cur_known);
|
|
if(comp < 0) {
|
|
WRN_UT << "Unknown attribute '" << cur->first << "' discarded." << std::endl;
|
|
++cur;
|
|
}
|
|
else if(comp == 0) {
|
|
++cur;
|
|
++cur_known;
|
|
}
|
|
else {
|
|
++cur_known;
|
|
}
|
|
}
|
|
|
|
while(cur != end) {
|
|
WRN_UT << "Unknown attribute '" << cur->first << "' discarded." << std::endl;
|
|
++cur;
|
|
}
|
|
}
|
|
|
|
template<typename T>
|
|
T* copy_or_null(const std::unique_ptr<T>& ptr)
|
|
{
|
|
return ptr ? new T(*ptr) : nullptr;
|
|
}
|
|
} // end anon namespace
|
|
|
|
/**
|
|
* Intrusive Pointer interface
|
|
*
|
|
**/
|
|
|
|
void intrusive_ptr_add_ref(const unit* u)
|
|
{
|
|
assert(u->ref_count_ >= 0);
|
|
// the next code line is to notice possible wrongly initialized units.
|
|
// The 100000 is picked rather randomly. If you are in the situation
|
|
// that you can actually have more then 100000 intrusive_ptr to one unit
|
|
// or if you are sure that the refcounting system works
|
|
// then feel free to remove the next line
|
|
assert(u->ref_count_ < 100000);
|
|
if(u->ref_count_ == 0) {
|
|
LOG_UT << "Freshly constructed" << std::endl;
|
|
}
|
|
++(u->ref_count_);
|
|
}
|
|
|
|
void intrusive_ptr_release(const unit* u)
|
|
{
|
|
assert(u->ref_count_ >= 1);
|
|
assert(u->ref_count_ < 100000); //See comment in intrusive_ptr_add_ref
|
|
if(--(u->ref_count_) == 0)
|
|
{
|
|
DBG_UT << "Deleting a unit: id = " << u->id() << ", uid = " << u->underlying_id() << std::endl;
|
|
delete u;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts a string ID to a unit_type.
|
|
* Throws a game_error exception if the string does not correspond to a type.
|
|
*/
|
|
static const unit_type& get_unit_type(const std::string& type_id)
|
|
{
|
|
if(type_id.empty()) {
|
|
throw unit_type::error("creating unit with an empty type field");
|
|
}
|
|
std::string new_id = type_id;
|
|
unit_type::check_id(new_id);
|
|
const unit_type* i = unit_types.find(new_id);
|
|
if(!i) throw unit_type::error("unknown unit type: " + type_id);
|
|
return *i;
|
|
}
|
|
|
|
static unit_race::GENDER generate_gender(const unit_type& type, bool random_gender)
|
|
{
|
|
const std::vector<unit_race::GENDER>& genders = type.genders();
|
|
assert(genders.size() > 0);
|
|
|
|
if(random_gender == false || genders.size() == 1) {
|
|
return genders.front();
|
|
} else {
|
|
return genders[randomness::generator->get_random_int(0,genders.size()-1)];
|
|
// Note: genders is guaranteed to be non-empty, so this is not a
|
|
// potential division by zero.
|
|
// Note: Whoever wrote this code, you should have used an assertion, to save others hours of work...
|
|
// If the assertion size>0 is failing for you, one possible cause is that you are constructing a unit
|
|
// from a unit type which has not been ``built'' using the unit_type_data methods.
|
|
}
|
|
}
|
|
|
|
static unit_race::GENDER generate_gender(const unit_type& u_type, const config& cfg)
|
|
{
|
|
const std::string& gender = cfg["gender"];
|
|
if(!gender.empty()) {
|
|
return string_gender(gender);
|
|
}
|
|
|
|
return generate_gender(u_type, cfg["random_gender"].to_bool());
|
|
}
|
|
|
|
struct ptr_vector_pushback
|
|
{
|
|
ptr_vector_pushback(boost::ptr_vector<config>& vec) : vec_(&vec) {}
|
|
|
|
void operator()(const config& cfg)
|
|
{
|
|
vec_->push_back(new config(cfg));
|
|
}
|
|
|
|
//Don't use reference to be copyable.
|
|
boost::ptr_vector<config>* vec_;
|
|
};
|
|
|
|
// Copy constructor
|
|
unit::unit(const unit& o)
|
|
: ref_count_(0)
|
|
, loc_(o.loc_)
|
|
, advances_to_(o.advances_to_)
|
|
, type_(o.type_)
|
|
, type_name_(o.type_name_)
|
|
, race_(o.race_)
|
|
, id_(o.id_)
|
|
, name_(o.name_)
|
|
, underlying_id_(o.underlying_id_)
|
|
, undead_variation_(o.undead_variation_)
|
|
, variation_(o.variation_)
|
|
, hit_points_(o.hit_points_)
|
|
, max_hit_points_(o.max_hit_points_)
|
|
, experience_(o.experience_)
|
|
, max_experience_(o.max_experience_)
|
|
, level_(o.level_)
|
|
, recall_cost_(o.recall_cost_)
|
|
, canrecruit_(o.canrecruit_)
|
|
, recruit_list_(o.recruit_list_)
|
|
, alignment_(o.alignment_)
|
|
, flag_rgb_(o.flag_rgb_)
|
|
, image_mods_(o.image_mods_)
|
|
, unrenamable_(o.unrenamable_)
|
|
, side_(o.side_)
|
|
, gender_(o.gender_)
|
|
, formula_man_(new unit_formula_manager(o.formula_manager()))
|
|
, movement_(o.movement_)
|
|
, max_movement_(o.max_movement_)
|
|
, vision_(o.vision_)
|
|
, jamming_(o.jamming_)
|
|
, movement_type_(o.movement_type_)
|
|
, hold_position_(o.hold_position_)
|
|
, end_turn_(o.end_turn_)
|
|
, resting_(o.resting_)
|
|
, attacks_left_(o.attacks_left_)
|
|
, max_attacks_(o.max_attacks_)
|
|
, states_(o.states_)
|
|
, known_boolean_states_(o.known_boolean_states_)
|
|
, variables_(o.variables_)
|
|
, events_(o.events_)
|
|
, filter_recall_(o.filter_recall_)
|
|
, emit_zoc_(o.emit_zoc_)
|
|
, overlays_(o.overlays_)
|
|
, role_(o.role_)
|
|
, attacks_(o.attacks_)
|
|
, facing_(o.facing_)
|
|
, trait_names_(o.trait_names_)
|
|
, trait_descriptions_(o.trait_descriptions_)
|
|
, unit_value_(o.unit_value_)
|
|
, goto_(o.goto_)
|
|
, interrupted_move_(o.interrupted_move_)
|
|
, is_fearless_(o.is_fearless_)
|
|
, is_healthy_(o.is_healthy_)
|
|
, modification_descriptions_(o.modification_descriptions_)
|
|
, anim_comp_(new unit_animation_component(*this, *o.anim_comp_))
|
|
, hidden_(o.hidden_)
|
|
, hp_bar_scaling_(o.hp_bar_scaling_)
|
|
, xp_bar_scaling_(o.xp_bar_scaling_)
|
|
, modifications_(o.modifications_)
|
|
, abilities_(o.abilities_)
|
|
, advancements_(o.advancements_)
|
|
, description_(o.description_)
|
|
, usage_(copy_or_null(o.usage_))
|
|
, halo_(copy_or_null(o.halo_))
|
|
, ellipse_(copy_or_null(o.ellipse_))
|
|
, random_traits_(o.random_traits_)
|
|
, generate_name_(o.generate_name_)
|
|
, upkeep_(o.upkeep_)
|
|
, profile_(o.profile_)
|
|
, small_profile_(o.small_profile_)
|
|
, changed_attributes_(o.changed_attributes_)
|
|
, invisibility_cache_()
|
|
{
|
|
// Copy the attacks rather than just copying references
|
|
for(auto& a : attacks_) {
|
|
a.reset(new attack_type(*a));
|
|
}
|
|
}
|
|
|
|
unit::unit()
|
|
: ref_count_(0)
|
|
, loc_()
|
|
, advances_to_()
|
|
, type_(nullptr)
|
|
, type_name_()
|
|
, race_(&unit_race::null_race)
|
|
, id_()
|
|
, name_()
|
|
, underlying_id_(0)
|
|
, undead_variation_()
|
|
, variation_()
|
|
, hit_points_(1)
|
|
, max_hit_points_(1)
|
|
, experience_(0)
|
|
, max_experience_(1)
|
|
, level_(0)
|
|
, recall_cost_(-1)
|
|
, canrecruit_(false)
|
|
, recruit_list_()
|
|
, alignment_()
|
|
, flag_rgb_()
|
|
, image_mods_()
|
|
, unrenamable_(false)
|
|
, side_(0)
|
|
, gender_(unit_race::NUM_GENDERS)
|
|
, formula_man_(new unit_formula_manager())
|
|
, movement_(0)
|
|
, max_movement_(0)
|
|
, vision_(-1)
|
|
, jamming_(0)
|
|
, movement_type_()
|
|
, hold_position_(false)
|
|
, end_turn_(false)
|
|
, resting_(false)
|
|
, attacks_left_(0)
|
|
, max_attacks_(0)
|
|
, states_()
|
|
, known_boolean_states_()
|
|
, variables_()
|
|
, events_()
|
|
, filter_recall_()
|
|
, emit_zoc_(0)
|
|
, overlays_()
|
|
, role_()
|
|
, attacks_()
|
|
, facing_(map_location::NDIRECTIONS)
|
|
, trait_names_()
|
|
, trait_descriptions_()
|
|
, unit_value_()
|
|
, goto_()
|
|
, interrupted_move_()
|
|
, is_fearless_(false)
|
|
, is_healthy_(false)
|
|
, modification_descriptions_()
|
|
, anim_comp_(new unit_animation_component(*this))
|
|
, hidden_(false)
|
|
, hp_bar_scaling_(0)
|
|
, xp_bar_scaling_(0)
|
|
, modifications_()
|
|
, abilities_()
|
|
, advancements_()
|
|
, description_()
|
|
, usage_()
|
|
, halo_()
|
|
, ellipse_()
|
|
, random_traits_(true)
|
|
, generate_name_(true)
|
|
, upkeep_(upkeep_full())
|
|
, changed_attributes_(0)
|
|
, invisibility_cache_()
|
|
{
|
|
|
|
}
|
|
|
|
void unit::init(const config& cfg, bool use_traits, const vconfig* vcfg)
|
|
{
|
|
loc_ = map_location(cfg["x"], cfg["y"], wml_loc());
|
|
type_ = &get_unit_type(cfg["parent_type"].blank() ? cfg["type"].str() : cfg["parent_type"].str());
|
|
race_ = &unit_race::null_race;
|
|
id_ = cfg["id"].str();
|
|
name_ = cfg["name"].t_str();
|
|
variation_ = cfg["variation"].empty() ? type_->default_variation() : cfg["variation"].str();
|
|
canrecruit_ = cfg["canrecruit"].to_bool();
|
|
gender_ = generate_gender(*type_, cfg);
|
|
role_ = cfg["role"].str();
|
|
//, facing_(map_location::NDIRECTIONS)
|
|
//, anim_comp_(new unit_animation_component(*this))
|
|
hidden_ = cfg["hidden"].to_bool(false);
|
|
hp_bar_scaling_ = cfg["hp_bar_scaling"].blank() ? type_->hp_bar_scaling() : cfg["hp_bar_scaling"];
|
|
xp_bar_scaling_ = cfg["xp_bar_scaling"].blank() ? type_->xp_bar_scaling() : cfg["xp_bar_scaling"];
|
|
random_traits_ = true;
|
|
generate_name_ = true;
|
|
side_ = cfg["side"].to_int();
|
|
|
|
if(side_ <= 0) {
|
|
side_ = 1;
|
|
}
|
|
|
|
validate_side(side_);
|
|
underlying_id_ = n_unit::unit_id(cfg["underlying_id"].to_size_t());
|
|
set_underlying_id(resources::gameboard ? resources::gameboard->unit_id_manager() : n_unit::id_manager::global_instance());
|
|
|
|
if(vcfg) {
|
|
const vconfig& filter_recall = vcfg->child("filter_recall");
|
|
if(!filter_recall.null())
|
|
filter_recall_ = filter_recall.get_config();
|
|
|
|
const vconfig::child_list& events = vcfg->get_children("event");
|
|
for(const vconfig& e : events) {
|
|
events_.add_child("event", e.get_config());
|
|
}
|
|
} else {
|
|
filter_recall_ = cfg.child_or_empty("filter_recall");
|
|
|
|
for(const config& unit_event : cfg.child_range("event")) {
|
|
events_.add_child("event", unit_event);
|
|
}
|
|
}
|
|
|
|
if(resources::game_events) {
|
|
resources::game_events->add_events(events_.child_range("event"));
|
|
}
|
|
|
|
random_traits_ = cfg["random_traits"].to_bool(true);
|
|
facing_ = map_location::parse_direction(cfg["facing"]);
|
|
if(facing_ == map_location::NDIRECTIONS) facing_ = static_cast<map_location::DIRECTION>(randomness::rng::default_instance().get_random_int(0, map_location::NDIRECTIONS-1));
|
|
|
|
if(const config& mods = cfg.child("modifications")) {
|
|
modifications_ = mods;
|
|
}
|
|
|
|
generate_name_ = cfg["generate_name"].to_bool(true);
|
|
|
|
// Apply the unit type's data to this unit.
|
|
advance_to(*type_, use_traits);
|
|
|
|
if(const config::attribute_value* v = cfg.get("overlays")) {
|
|
auto overlays = utils::parenthetical_split(v->str(), ',');
|
|
if(overlays.size() > 0) {
|
|
deprecated_message("[unit]overlays", DEP_LEVEL::PREEMPTIVE, {1, 17, 0}, "This warning is only triggered by the cases that *do* still work: setting [unit]overlays= works, but the [unit]overlays attribute will always be empty if WML tries to read it.");
|
|
config effect;
|
|
config o;
|
|
effect["apply_to"] = "overlay";
|
|
effect["add"] = v->str();
|
|
o.add_child("effect", effect);
|
|
add_modification("object", o);
|
|
}
|
|
}
|
|
|
|
if(const config& variables = cfg.child("variables")) {
|
|
variables_ = variables;
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("race")) {
|
|
if(const unit_race *r = unit_types.find_race(*v)) {
|
|
race_ = r;
|
|
} else {
|
|
race_ = &unit_race::null_race;
|
|
}
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("level")) {
|
|
set_level(v->to_int(level_));
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("undead_variation")) {
|
|
set_undead_variation(v->str());
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("max_attacks")) {
|
|
set_max_attacks(std::max(0, v->to_int(1)));
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("zoc")) {
|
|
set_emit_zoc(v->to_bool(level_ > 0));
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("description")) {
|
|
description_ = *v;
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("cost")) {
|
|
unit_value_ = *v;
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("ellipse")) {
|
|
set_image_ellipse(*v);
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("halo")) {
|
|
set_image_halo(*v);
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("usage")) {
|
|
set_usage(*v);
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("profile")) {
|
|
set_big_profile(v->str());
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("small_profile")) {
|
|
set_small_profile(v->str());
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("max_hitpoints")) {
|
|
set_max_hitpoints(std::max(1, v->to_int(max_hit_points_)));
|
|
}
|
|
if(const config::attribute_value* v = cfg.get("max_moves")) {
|
|
set_total_movement(std::max(1, v->to_int(max_movement_)));
|
|
}
|
|
if(const config::attribute_value* v = cfg.get("max_experience")) {
|
|
set_max_experience(std::max(1, v->to_int(max_experience_)));
|
|
}
|
|
|
|
vision_ = cfg["vision"].to_int(vision_);
|
|
jamming_ = cfg["jamming"].to_int(jamming_);
|
|
|
|
advances_to_t temp_advances = utils::split(cfg["advances_to"]);
|
|
if(temp_advances.size() == 1 && temp_advances.front() == "null") {
|
|
set_advances_to(advances_to_t());
|
|
} else if(temp_advances.size() >= 1 && !temp_advances.front().empty()) {
|
|
set_advances_to(temp_advances);
|
|
}
|
|
|
|
if(const config& ai = cfg.child("ai")) {
|
|
formula_man_->read(ai);
|
|
}
|
|
|
|
// Don't use the unit_type's attacks if this config has its own defined
|
|
if(config::const_child_itors cfg_range = cfg.child_range("attack")) {
|
|
set_attr_changed(UA_ATTACKS);
|
|
attacks_.clear();
|
|
for(const config& c : cfg_range) {
|
|
attacks_.emplace_back(new attack_type(c));
|
|
}
|
|
}
|
|
|
|
// If cfg specifies [advancement]s, replace this [advancement]s with them.
|
|
if(cfg.has_child("advancement")) {
|
|
set_attr_changed(UA_ADVANCEMENTS);
|
|
this->advancements_.clear();
|
|
boost::copy(cfg.child_range("advancement"), boost::make_function_output_iterator(ptr_vector_pushback(advancements_)));
|
|
}
|
|
|
|
// Don't use the unit_type's abilities if this config has its own defined
|
|
// Why do we allow multiple [abilities] tags?
|
|
if(config::const_child_itors cfg_range = cfg.child_range("abilities")) {
|
|
set_attr_changed(UA_ABILITIES);
|
|
abilities_.clear();
|
|
for(const config& abilities : cfg_range) {
|
|
this->abilities_.append(abilities);
|
|
}
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("alignment")) {
|
|
set_attr_changed(UA_ALIGNMENT);
|
|
alignment_.parse(v->str());
|
|
}
|
|
|
|
// Adjust the unit's defense, movement, vision, jamming, resistances, and
|
|
// flying status if this config has its own defined.
|
|
if(cfg.has_child("movement_costs")
|
|
|| cfg.has_child("vision_costs")
|
|
|| cfg.has_child("jamming_costs")
|
|
|| cfg.has_child("defense")
|
|
|| cfg.has_child("resistance")
|
|
|| cfg.has_attribute("flying"))
|
|
{
|
|
set_attr_changed(UA_MOVEMENT_TYPE);
|
|
}
|
|
|
|
movement_type_.merge(cfg);
|
|
|
|
if(const config& status_flags = cfg.child("status")) {
|
|
for(const config::attribute &st : status_flags.attribute_range()) {
|
|
if(st.second.to_bool()) {
|
|
set_state(st.first, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(cfg["ai_special"] == "guardian") {
|
|
set_state(STATE_GUARDIAN, true);
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("invulnerable")) {
|
|
set_state("invulnerable", v->to_bool());
|
|
}
|
|
|
|
goto_.set_wml_x(cfg["goto_x"].to_int());
|
|
goto_.set_wml_y(cfg["goto_y"].to_int());
|
|
|
|
attacks_left_ = std::max(0, cfg["attacks_left"].to_int(max_attacks_));
|
|
|
|
movement_ = std::max(0, cfg["moves"].to_int(max_movement_));
|
|
// we allow negative hitpoints, one of the reasons is that a unit
|
|
// might be stored+unstored during a attack related event before it
|
|
// dies when it has negative hp and when dont want the event to
|
|
// change the unit hp when it was not intended.
|
|
hit_points_ = cfg["hitpoints"].to_int(max_hit_points_);
|
|
|
|
experience_ = cfg["experience"];
|
|
resting_ = cfg["resting"].to_bool();
|
|
unrenamable_ = cfg["unrenamable"].to_bool();
|
|
|
|
// We need to check to make sure that the cfg is not blank and if it
|
|
// isn't pull that value otherwise it goes with the default of -1.
|
|
if(!cfg["recall_cost"].blank()) {
|
|
recall_cost_ = cfg["recall_cost"].to_int(recall_cost_);
|
|
}
|
|
|
|
generate_name();
|
|
|
|
parse_upkeep(cfg["upkeep"]);
|
|
|
|
set_recruits(utils::split(cfg["extra_recruit"]));
|
|
|
|
game_config::add_color_info(cfg);
|
|
warn_unknown_attribute(cfg.attribute_range());
|
|
|
|
#if 0
|
|
// Debug unit animations for units as they appear in game
|
|
for(const auto& anim : anim_comp_->animations_) {
|
|
std::cout << anim.debug() << std::endl;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void unit::clear_status_caches()
|
|
{
|
|
for(auto& u : units_with_cache) {
|
|
u->clear_visibility_cache();
|
|
}
|
|
|
|
units_with_cache.clear();
|
|
}
|
|
|
|
void unit::init(const unit_type& u_type, int side, bool real_unit, unit_race::GENDER gender)
|
|
{
|
|
type_ = &u_type;
|
|
race_ = &unit_race::null_race;
|
|
variation_ = type_->default_variation();
|
|
side_ = side;
|
|
gender_ = gender != unit_race::NUM_GENDERS ? gender : generate_gender(u_type, real_unit);
|
|
facing_ = static_cast<map_location::DIRECTION>(randomness::rng::default_instance().get_random_int(0, map_location::NDIRECTIONS-1));
|
|
upkeep_ = upkeep_full();
|
|
|
|
// Apply the unit type's data to this unit.
|
|
advance_to(u_type, real_unit);
|
|
|
|
if(real_unit) {
|
|
generate_name();
|
|
}
|
|
|
|
set_underlying_id(resources::gameboard ? resources::gameboard->unit_id_manager() : n_unit::id_manager::global_instance());
|
|
|
|
// Set these after traits and modifications have set the maximums.
|
|
movement_ = max_movement_;
|
|
hit_points_ = max_hit_points_;
|
|
attacks_left_ = max_attacks_;
|
|
}
|
|
|
|
unit::~unit()
|
|
{
|
|
try {
|
|
anim_comp_->clear_haloes();
|
|
|
|
// Remove us from the status cache
|
|
auto itor = std::find(units_with_cache.begin(), units_with_cache.end(), this);
|
|
|
|
if(itor != units_with_cache.end()) {
|
|
units_with_cache.erase(itor);
|
|
}
|
|
} catch(const std::exception & e) {
|
|
ERR_UT << "Caught exception when destroying unit: " << e.what() << std::endl;
|
|
} catch(...) {}
|
|
}
|
|
|
|
/**
|
|
* Swap, for copy and swap idiom
|
|
*/
|
|
void unit::swap(unit & o)
|
|
{
|
|
using std::swap;
|
|
|
|
// Don't swap reference count, or it will be incorrect...
|
|
swap(loc_, o.loc_);
|
|
swap(advances_to_, o.advances_to_);
|
|
swap(type_, o.type_);
|
|
swap(type_name_, o.type_name_);
|
|
swap(race_, o.race_);
|
|
swap(id_, o.id_);
|
|
swap(name_, o.name_);
|
|
swap(underlying_id_, o.underlying_id_);
|
|
swap(undead_variation_, o.undead_variation_);
|
|
swap(variation_, o.variation_);
|
|
swap(hit_points_, o.hit_points_);
|
|
swap(max_hit_points_, o.max_hit_points_);
|
|
swap(experience_, o.experience_);
|
|
swap(max_experience_, o.max_experience_);
|
|
swap(level_, o.level_);
|
|
swap(recall_cost_, o.recall_cost_);
|
|
swap(canrecruit_, o.canrecruit_);
|
|
swap(recruit_list_, o.recruit_list_);
|
|
swap(alignment_, o.alignment_);
|
|
swap(flag_rgb_, o.flag_rgb_);
|
|
swap(image_mods_, o.image_mods_);
|
|
swap(unrenamable_, o.unrenamable_);
|
|
swap(side_, o.side_);
|
|
swap(gender_, o.gender_);
|
|
swap(formula_man_, o.formula_man_);
|
|
swap(movement_, o.movement_);
|
|
swap(max_movement_, o.max_movement_);
|
|
swap(vision_, o.vision_);
|
|
swap(jamming_, o.jamming_);
|
|
swap(movement_type_, o.movement_type_);
|
|
swap(hold_position_, o.hold_position_);
|
|
swap(end_turn_, o.end_turn_);
|
|
swap(resting_, o.resting_);
|
|
swap(attacks_left_, o.attacks_left_);
|
|
swap(max_attacks_, o.max_attacks_);
|
|
swap(states_, o.states_);
|
|
swap(known_boolean_states_, o.known_boolean_states_);
|
|
swap(variables_, o.variables_);
|
|
swap(events_, o.events_);
|
|
swap(filter_recall_, o.filter_recall_);
|
|
swap(emit_zoc_, o.emit_zoc_);
|
|
swap(overlays_, o.overlays_);
|
|
swap(role_, o.role_);
|
|
swap(attacks_, o.attacks_);
|
|
swap(facing_, o.facing_);
|
|
swap(trait_names_, o.trait_names_);
|
|
swap(trait_descriptions_, o.trait_descriptions_);
|
|
swap(unit_value_, o.unit_value_);
|
|
swap(goto_, o.goto_);
|
|
swap(interrupted_move_, o.interrupted_move_);
|
|
swap(is_fearless_, o.is_fearless_);
|
|
swap(is_healthy_, o.is_healthy_);
|
|
swap(modification_descriptions_, o.modification_descriptions_);
|
|
swap(anim_comp_, o.anim_comp_);
|
|
swap(hidden_, o.hidden_);
|
|
swap(modifications_, o.modifications_);
|
|
swap(invisibility_cache_, o.invisibility_cache_);
|
|
}
|
|
|
|
void unit::generate_name()
|
|
{
|
|
if(!name_.empty() || !generate_name_) {
|
|
return;
|
|
}
|
|
name_ = race_->generate_name(gender_);
|
|
generate_name_ = false;
|
|
}
|
|
|
|
void unit::generate_traits(bool must_have_only)
|
|
{
|
|
LOG_UT << "Generating a trait for unit type " << type().log_id() << " with must_have_only " << must_have_only << std::endl;
|
|
const unit_type& u_type = type();
|
|
|
|
// Calculate the unit's traits
|
|
config::const_child_itors current_traits = modifications_.child_range("trait");
|
|
std::vector<const config*> candidate_traits;
|
|
|
|
for(const config& t : u_type.possible_traits()) {
|
|
// Skip the trait if the unit already has it.
|
|
const std::string& tid = t["id"];
|
|
bool already = false;
|
|
for(const config& mod : current_traits) {
|
|
if(mod["id"] == tid) {
|
|
already = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(already) {
|
|
continue;
|
|
}
|
|
|
|
// Add the trait if it is mandatory.
|
|
const std::string& avl = t["availability"];
|
|
if(avl == "musthave") {
|
|
modifications_.add_child("trait", t);
|
|
current_traits = modifications_.child_range("trait");
|
|
continue;
|
|
}
|
|
|
|
// The trait is still available, mark it as a candidate for randomizing.
|
|
// For leaders, only traits with availability "any" are considered.
|
|
if(!must_have_only && (!can_recruit() || avl == "any")) {
|
|
candidate_traits.push_back(&t);
|
|
}
|
|
}
|
|
|
|
if(must_have_only) return;
|
|
|
|
// Now randomly fill out to the number of traits required or until
|
|
// there aren't any more traits.
|
|
int nb_traits = current_traits.size();
|
|
int max_traits = u_type.num_traits();
|
|
for(; nb_traits < max_traits && !candidate_traits.empty(); ++nb_traits)
|
|
{
|
|
int num = randomness::generator->get_random_int(0,candidate_traits.size()-1);
|
|
modifications_.add_child("trait", *candidate_traits[num]);
|
|
candidate_traits.erase(candidate_traits.begin() + num);
|
|
}
|
|
|
|
// Once random traits are added, don't do it again.
|
|
// Such as when restoring a saved character.
|
|
random_traits_ = false;
|
|
}
|
|
|
|
std::vector<std::string> unit::get_traits_list() const
|
|
{
|
|
std::vector<std::string> res;
|
|
|
|
for(const config& mod : modifications_.child_range("trait"))
|
|
{
|
|
// Make sure to return empty id trait strings as otherwise
|
|
// names will not match in length (Bug #21967)
|
|
res.push_back(mod["id"]);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
|
|
/**
|
|
* Advances this unit to the specified type.
|
|
* Experience is left unchanged.
|
|
* Current hit point total is left unchanged unless it would violate max HP.
|
|
* Assumes gender_ and variation_ are set to their correct values.
|
|
*/
|
|
void unit::advance_to(const unit_type& u_type, bool use_traits)
|
|
{
|
|
appearance_changed_ = true;
|
|
// For reference, the type before this advancement.
|
|
const unit_type& old_type = type();
|
|
// Adjust the new type for gender and variation.
|
|
const unit_type& new_type = u_type.get_gender_unit_type(gender_).get_variation(variation_);
|
|
|
|
// Reset the scalar values first
|
|
trait_names_.clear();
|
|
trait_descriptions_.clear(),
|
|
is_fearless_ = false;
|
|
is_healthy_ = false;
|
|
image_mods_.clear();
|
|
overlays_.clear();
|
|
|
|
// Clear modification-related caches
|
|
modification_descriptions_.clear();
|
|
|
|
// build unit type ready to create units. Not sure if needed.
|
|
new_type.get_cfg_for_units();
|
|
|
|
if(!new_type.usage().empty()) {
|
|
set_usage(new_type.usage());
|
|
}
|
|
|
|
set_image_halo(new_type.halo());
|
|
if(!new_type.ellipse().empty()) {
|
|
set_image_ellipse(new_type.ellipse());
|
|
}
|
|
|
|
generate_name_ &= new_type.generate_name();
|
|
abilities_ = new_type.abilities_cfg();
|
|
advancements_.clear();
|
|
|
|
for(const config& advancement : new_type.advancements()) {
|
|
advancements_.push_back(new config(advancement));
|
|
}
|
|
|
|
// If unit has specific profile, remember it and keep it after advancing
|
|
if(small_profile_.empty() || small_profile_ == old_type.small_profile()) {
|
|
small_profile_ = new_type.small_profile();
|
|
}
|
|
|
|
if(profile_.empty() || profile_ == old_type.big_profile()) {
|
|
profile_ = new_type.big_profile();
|
|
}
|
|
// NOTE: There should be no need to access old_cfg (or new_cfg) after this
|
|
// line. Particularly since the swap might have affected old_cfg.
|
|
|
|
advances_to_ = new_type.advances_to();
|
|
|
|
race_ = new_type.race();
|
|
type_ = &new_type;
|
|
type_name_ = new_type.type_name();
|
|
description_ = new_type.unit_description();
|
|
undead_variation_ = new_type.undead_variation();
|
|
max_experience_ = new_type.experience_needed(true);
|
|
level_ = new_type.level();
|
|
recall_cost_ = new_type.recall_cost();
|
|
alignment_ = new_type.alignment();
|
|
max_hit_points_ = new_type.hitpoints();
|
|
hp_bar_scaling_ = new_type.hp_bar_scaling();
|
|
xp_bar_scaling_ = new_type.xp_bar_scaling();
|
|
max_movement_ = new_type.movement();
|
|
vision_ = new_type.vision(true);
|
|
jamming_ = new_type.jamming();
|
|
movement_type_ = new_type.movement_type();
|
|
emit_zoc_ = new_type.has_zoc();
|
|
attacks_.clear();
|
|
std::transform(new_type.attacks().begin(), new_type.attacks().end(), std::back_inserter(attacks_), [](const attack_type& atk) {
|
|
return std::make_shared<attack_type>(atk);
|
|
});
|
|
unit_value_ = new_type.cost();
|
|
|
|
max_attacks_ = new_type.max_attacks();
|
|
|
|
flag_rgb_ = new_type.flag_rgb();
|
|
|
|
upkeep_ = upkeep_full();
|
|
parse_upkeep(new_type.get_cfg()["upkeep"]);
|
|
|
|
anim_comp_->reset_after_advance(&new_type);
|
|
|
|
if(random_traits_) {
|
|
generate_traits(!use_traits);
|
|
} else {
|
|
// This will add any "musthave" traits to the new unit that it doesn't already have.
|
|
// This covers the Dark Sorcerer advancing to Lich and gaining the "undead" trait,
|
|
// but random and/or optional traits are not added,
|
|
// and neither are inappropriate traits removed.
|
|
generate_traits(true);
|
|
}
|
|
|
|
// Apply modifications etc, refresh the unit.
|
|
// This needs to be after type and gender are fixed,
|
|
// since there can be filters on the modifications
|
|
// that may result in different effects after the advancement.
|
|
apply_modifications();
|
|
|
|
// Now that modifications are done modifying traits, check if poison should
|
|
// be cleared.
|
|
if(get_state("unpoisonable")) {
|
|
set_state(STATE_POISONED, false);
|
|
}
|
|
|
|
// Now that modifications are done modifying the maximum hit points,
|
|
// enforce this maximum.
|
|
if(hit_points_ > max_hit_points_) {
|
|
hit_points_ = max_hit_points_;
|
|
}
|
|
|
|
// In case the unit carries EventWML, apply it now
|
|
if(resources::game_events) {
|
|
resources::game_events->add_events(new_type.events(), new_type.id());
|
|
}
|
|
bool bool_small_profile = get_attr_changed(UA_SMALL_PROFILE);
|
|
bool bool_profile = get_attr_changed(UA_PROFILE);
|
|
clear_changed_attributes();
|
|
if(bool_small_profile && small_profile_ != new_type.small_profile()) {
|
|
set_attr_changed(UA_SMALL_PROFILE);
|
|
}
|
|
|
|
if(bool_profile && profile_ != new_type.big_profile()) {
|
|
set_attr_changed(UA_PROFILE);
|
|
}
|
|
}
|
|
|
|
std::string unit::big_profile() const
|
|
{
|
|
if(!profile_.empty() && profile_ != "unit_image") {
|
|
return profile_;
|
|
}
|
|
|
|
return absolute_image();
|
|
}
|
|
|
|
std::string unit::small_profile() const
|
|
{
|
|
if(!small_profile_.empty() && small_profile_ != "unit_image") {
|
|
return small_profile_;
|
|
}
|
|
|
|
if(!profile_.empty() && small_profile_ != "unit_image" && profile_ != "unit_image") {
|
|
return profile_;
|
|
}
|
|
|
|
return absolute_image();
|
|
}
|
|
|
|
const std::string& unit::leader_crown()
|
|
{
|
|
return leader_crown_path;
|
|
}
|
|
|
|
const std::string& unit::flag_rgb() const
|
|
{
|
|
return flag_rgb_.empty() ? game_config::unit_rgb : flag_rgb_;
|
|
}
|
|
|
|
static color_t hp_color_impl(int hitpoints, int max_hitpoints)
|
|
{
|
|
double unit_energy = 0.0;
|
|
color_t energy_color {0,0,0,255};
|
|
|
|
if(max_hitpoints > 0) {
|
|
unit_energy = static_cast<double>(hitpoints)/static_cast<double>(max_hitpoints);
|
|
}
|
|
|
|
if(1.0 == unit_energy) {
|
|
energy_color.r = 33;
|
|
energy_color.g = 225;
|
|
energy_color.b = 0;
|
|
} else if(unit_energy > 1.0) {
|
|
energy_color.r = 100;
|
|
energy_color.g = 255;
|
|
energy_color.b = 100;
|
|
} else if(unit_energy >= 0.75) {
|
|
energy_color.r = 170;
|
|
energy_color.g = 255;
|
|
energy_color.b = 0;
|
|
} else if(unit_energy >= 0.5) {
|
|
energy_color.r = 255;
|
|
energy_color.g = 175;
|
|
energy_color.b = 0;
|
|
} else if(unit_energy >= 0.25) {
|
|
energy_color.r = 255;
|
|
energy_color.g = 155;
|
|
energy_color.b = 0;
|
|
} else {
|
|
energy_color.r = 255;
|
|
energy_color.g = 0;
|
|
energy_color.b = 0;
|
|
}
|
|
|
|
return energy_color;
|
|
}
|
|
|
|
color_t unit::hp_color() const
|
|
{
|
|
return hp_color_impl(hitpoints(), max_hitpoints());
|
|
}
|
|
|
|
color_t unit::hp_color(int new_hitpoints) const
|
|
{
|
|
return hp_color_impl(new_hitpoints, hitpoints());
|
|
}
|
|
|
|
color_t unit::xp_color() const
|
|
{
|
|
const color_t near_advance_color {255,255,255,255};
|
|
const color_t mid_advance_color {150,255,255,255};
|
|
const color_t far_advance_color {0,205,205,255};
|
|
const color_t normal_color {0,160,225,255};
|
|
const color_t near_amla_color {225,0,255,255};
|
|
const color_t mid_amla_color {169,30,255,255};
|
|
const color_t far_amla_color {139,0,237,255};
|
|
const color_t amla_color {170,0,255,255};
|
|
|
|
const bool near_advance = static_cast<int>(experience_to_advance()) <= game_config::kill_experience;
|
|
const bool mid_advance = static_cast<int>(experience_to_advance()) <= game_config::kill_experience*2;
|
|
const bool far_advance = static_cast<int>(experience_to_advance()) <= game_config::kill_experience*3;
|
|
|
|
color_t color = normal_color;
|
|
bool major_amla = false;
|
|
for(const config& adv:get_modification_advances()){
|
|
major_amla |= adv["major_amla"].to_bool();
|
|
}
|
|
if(advances_to().size() ||major_amla){
|
|
if(near_advance){
|
|
color=near_advance_color;
|
|
} else if(mid_advance){
|
|
color=mid_advance_color;
|
|
} else if(far_advance){
|
|
color=far_advance_color;
|
|
}
|
|
} else if(get_modification_advances().size()){
|
|
if(near_advance){
|
|
color=near_amla_color;
|
|
} else if(mid_advance){
|
|
color=mid_amla_color;
|
|
} else if(far_advance){
|
|
color=far_amla_color;
|
|
} else {
|
|
color=amla_color;
|
|
}
|
|
}
|
|
|
|
return(color);
|
|
}
|
|
|
|
void unit::set_recruits(const std::vector<std::string>& recruits)
|
|
{
|
|
unit_types.check_types(recruits);
|
|
recruit_list_ = recruits;
|
|
//TODO crab
|
|
//info_.minimum_recruit_price = 0;
|
|
//ai::manager::get_singleton().raise_recruit_list_changed();
|
|
}
|
|
|
|
const std::vector<std::string> unit::advances_to_translated() const
|
|
{
|
|
std::vector<std::string> result;
|
|
for(const std::string& adv_type_id : advances_to_) {
|
|
if(const unit_type* adv_type = unit_types.find(adv_type_id)) {
|
|
result.push_back(adv_type->type_name());
|
|
} else {
|
|
WRN_UT << "unknown unit in advances_to list of type "
|
|
<< type().log_id() << ": " << adv_type_id << std::endl;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void unit::set_advances_to(const std::vector<std::string>& advances_to)
|
|
{
|
|
set_attr_changed(UA_ADVANCE_TO);
|
|
unit_types.check_types(advances_to);
|
|
advances_to_ = advances_to;
|
|
}
|
|
|
|
void unit::set_movement(int moves, bool unit_action)
|
|
{
|
|
// If this was because the unit acted, clear its "not acting" flags.
|
|
if(unit_action) {
|
|
end_turn_ = hold_position_ = false;
|
|
}
|
|
|
|
movement_ = std::max<int>(0, moves);
|
|
}
|
|
|
|
/**
|
|
* Determines if @a mod_dur "matches" @a goal_dur.
|
|
* If goal_dur is not empty, they match if they are equal.
|
|
* If goal_dur is empty, they match if mod_dur is neither empty nor "forever".
|
|
* Helper function for expire_modifications().
|
|
*/
|
|
inline bool mod_duration_match(const std::string& mod_dur, const std::string& goal_dur)
|
|
{
|
|
if(goal_dur.empty()) {
|
|
// Default is all temporary modifications.
|
|
return !mod_dur.empty() && mod_dur != "forever";
|
|
}
|
|
|
|
return mod_dur == goal_dur;
|
|
}
|
|
|
|
void unit::expire_modifications(const std::string& duration)
|
|
{
|
|
// If any modifications expire, then we will need to rebuild the unit.
|
|
const unit_type* rebuild_from = nullptr;
|
|
int hp = hit_points_;
|
|
int mp = movement_;
|
|
// Loop through all types of modifications.
|
|
for(const auto& mod_name : ModificationTypes) {
|
|
// Loop through all modifications of this type.
|
|
// Looping in reverse since we may delete the current modification.
|
|
for(int j = modifications_.child_count(mod_name)-1; j >= 0; --j)
|
|
{
|
|
const config& mod = modifications_.child(mod_name, j);
|
|
|
|
if(mod_duration_match(mod["duration"], duration)) {
|
|
// If removing this mod means reverting the unit's type:
|
|
if(const config::attribute_value* v = mod.get("prev_type")) {
|
|
rebuild_from = &get_unit_type(v->str());
|
|
}
|
|
// Else, if we have not already specified a type to build from:
|
|
else if(rebuild_from == nullptr) {
|
|
rebuild_from = &type();
|
|
}
|
|
|
|
modifications_.remove_child(mod_name, j);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(rebuild_from != nullptr) {
|
|
anim_comp_->clear_haloes();
|
|
advance_to(*rebuild_from);
|
|
hit_points_ = hp;
|
|
movement_ = std::min(mp, max_movement_);
|
|
}
|
|
}
|
|
|
|
void unit::new_turn()
|
|
{
|
|
expire_modifications("turn");
|
|
|
|
end_turn_ = hold_position_;
|
|
movement_ = total_movement();
|
|
attacks_left_ = max_attacks_;
|
|
set_state(STATE_UNCOVERED, false);
|
|
}
|
|
|
|
void unit::end_turn()
|
|
{
|
|
expire_modifications("turn end");
|
|
|
|
set_state(STATE_SLOWED,false);
|
|
if((movement_ != total_movement()) && !(get_state(STATE_NOT_MOVED))) {
|
|
resting_ = false;
|
|
}
|
|
|
|
set_state(STATE_NOT_MOVED,false);
|
|
// Clear interrupted move
|
|
set_interrupted_move(map_location());
|
|
}
|
|
|
|
void unit::new_scenario()
|
|
{
|
|
// Set the goto-command to be going to no-where
|
|
goto_ = map_location();
|
|
|
|
// Expire all temporary modifications.
|
|
expire_modifications("");
|
|
|
|
heal_fully();
|
|
set_state(STATE_SLOWED, false);
|
|
set_state(STATE_POISONED, false);
|
|
set_state(STATE_PETRIFIED, false);
|
|
set_state(STATE_GUARDIAN, false);
|
|
}
|
|
|
|
void unit::heal(int amount)
|
|
{
|
|
int max_hp = max_hitpoints();
|
|
if(hit_points_ < max_hp) {
|
|
hit_points_ += amount;
|
|
|
|
if(hit_points_ > max_hp) {
|
|
hit_points_ = max_hp;
|
|
}
|
|
}
|
|
|
|
if(hit_points_<1) {
|
|
hit_points_ = 1;
|
|
}
|
|
}
|
|
|
|
const std::set<std::string> unit::get_states() const
|
|
{
|
|
std::set<std::string> all_states = states_;
|
|
for(const auto& state : known_boolean_state_names_) {
|
|
if(get_state(state.second)) {
|
|
all_states.insert(state.first);
|
|
}
|
|
}
|
|
|
|
// Backwards compatibility for not_living. Don't remove before 1.12
|
|
if(all_states.count("undrainable") && all_states.count("unpoisonable") && all_states.count("unplagueable")) {
|
|
all_states.insert("not_living");
|
|
}
|
|
|
|
return all_states;
|
|
}
|
|
|
|
bool unit::get_state(const std::string& state) const
|
|
{
|
|
state_t known_boolean_state_id = get_known_boolean_state_id(state);
|
|
if(known_boolean_state_id!=STATE_UNKNOWN){
|
|
return get_state(known_boolean_state_id);
|
|
}
|
|
|
|
// Backwards compatibility for not_living. Don't remove before 1.12
|
|
if(state == "not_living") {
|
|
return
|
|
get_state("undrainable") &&
|
|
get_state("unpoisonable") &&
|
|
get_state("unplagueable");
|
|
}
|
|
|
|
return states_.find(state) != states_.end();
|
|
}
|
|
|
|
void unit::set_state(state_t state, bool value)
|
|
{
|
|
known_boolean_states_[state] = value;
|
|
}
|
|
|
|
bool unit::get_state(state_t state) const
|
|
{
|
|
return known_boolean_states_[state];
|
|
}
|
|
|
|
unit::state_t unit::get_known_boolean_state_id(const std::string& state)
|
|
{
|
|
auto i = known_boolean_state_names_.find(state);
|
|
if(i != known_boolean_state_names_.end()) {
|
|
return i->second;
|
|
}
|
|
|
|
return STATE_UNKNOWN;
|
|
}
|
|
|
|
std::map<std::string, unit::state_t> unit::known_boolean_state_names_ {
|
|
{"slowed", STATE_SLOWED},
|
|
{"poisoned", STATE_POISONED},
|
|
{"petrified", STATE_PETRIFIED},
|
|
{"uncovered", STATE_UNCOVERED},
|
|
{"not_moved", STATE_NOT_MOVED},
|
|
{"unhealable", STATE_UNHEALABLE},
|
|
{"guardian", STATE_GUARDIAN},
|
|
};
|
|
|
|
void unit::set_state(const std::string& state, bool value)
|
|
{
|
|
appearance_changed_ = true;
|
|
state_t known_boolean_state_id = get_known_boolean_state_id(state);
|
|
if(known_boolean_state_id != STATE_UNKNOWN) {
|
|
set_state(known_boolean_state_id, value);
|
|
return;
|
|
}
|
|
|
|
// Backwards compatibility for not_living. Don't remove before 1.12
|
|
if(state == "not_living") {
|
|
set_state("undrainable", value);
|
|
set_state("unpoisonable", value);
|
|
set_state("unplagueable", value);
|
|
}
|
|
|
|
if(value) {
|
|
states_.insert(state);
|
|
} else {
|
|
states_.erase(state);
|
|
}
|
|
}
|
|
|
|
bool unit::has_ability_by_id(const std::string& ability) const
|
|
{
|
|
for(const config::any_child &ab : this->abilities_.all_children_range()) {
|
|
if(ab.cfg["id"] == ability) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void unit::remove_ability_by_id(const std::string& ability)
|
|
{
|
|
set_attr_changed(UA_ABILITIES);
|
|
config::all_children_iterator i = this->abilities_.ordered_begin();
|
|
while (i != this->abilities_.ordered_end()) {
|
|
if(i->cfg["id"] == ability) {
|
|
i = this->abilities_.erase(i);
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool unit::get_attacks_changed() const
|
|
{
|
|
for(const auto& a_ptr : attacks_) {
|
|
if(a_ptr->get_changed()) {
|
|
return true;
|
|
}
|
|
|
|
}
|
|
return false;
|
|
}
|
|
void unit::write(config& cfg, bool write_all) const
|
|
{
|
|
config back;
|
|
auto write_subtag = [&](const std::string& key, const config& child)
|
|
{
|
|
cfg.clear_children(key);
|
|
|
|
if(!child.empty()) {
|
|
cfg.add_child(key, child);
|
|
} else {
|
|
back.add_child(key, child);
|
|
}
|
|
};
|
|
|
|
if(write_all || get_attr_changed(UA_MOVEMENT_TYPE)) {
|
|
movement_type_.write(cfg);
|
|
}
|
|
if(write_all || get_attr_changed(UA_SMALL_PROFILE)) {
|
|
cfg["small_profile"] = small_profile_;
|
|
}
|
|
if(write_all || get_attr_changed(UA_PROFILE)) {
|
|
cfg["profile"] = profile_;
|
|
}
|
|
if(description_ != type().unit_description()) {
|
|
cfg["description"] = description_;
|
|
}
|
|
|
|
if(halo_.get()) {
|
|
cfg["halo"] = *halo_;
|
|
}
|
|
|
|
if(ellipse_.get()) {
|
|
cfg["ellipse"] = *ellipse_;
|
|
}
|
|
|
|
if(usage_.get()) {
|
|
cfg["usage"] = *usage_;
|
|
}
|
|
|
|
write_upkeep(cfg["upkeep"]);
|
|
|
|
cfg["hitpoints"] = hit_points_;
|
|
if(write_all || get_attr_changed(UA_MAX_HP)) {
|
|
cfg["max_hitpoints"] = max_hit_points_;
|
|
}
|
|
cfg["image_icon"] = type().icon();
|
|
cfg["image"] = type().image();
|
|
cfg["random_traits"] = random_traits_;
|
|
cfg["generate_name"] = generate_name_;
|
|
cfg["experience"] = experience_;
|
|
if(write_all || get_attr_changed(UA_MAX_XP)) {
|
|
cfg["max_experience"] = max_experience_;
|
|
}
|
|
cfg["recall_cost"] = recall_cost_;
|
|
|
|
cfg["side"] = side_;
|
|
|
|
cfg["type"] = type_id();
|
|
|
|
if(type_id() != type().base_id()) {
|
|
cfg["parent_type"] = type().base_id();
|
|
}
|
|
|
|
// Support for unit formulas in [ai] and unit-specific variables in [ai] [vars]
|
|
formula_man_->write(cfg);
|
|
|
|
cfg["gender"] = gender_string(gender_);
|
|
cfg["variation"] = variation_;
|
|
cfg["role"] = role_;
|
|
|
|
config status_flags;
|
|
for(const std::string& state : get_states()) {
|
|
status_flags[state] = true;
|
|
}
|
|
|
|
write_subtag("variables", variables_);
|
|
write_subtag("filter_recall", filter_recall_);
|
|
write_subtag("status", status_flags);
|
|
|
|
cfg.clear_children("events");
|
|
cfg.append(events_);
|
|
|
|
// Overlays are exported as the modifications that add them, not as an overlays= value,
|
|
// however removing the key breaks the Gui Debug Tools.
|
|
// \todo does anything depend on the key's value, other than the re-import code in unit::init?
|
|
cfg["overlays"] = "";
|
|
|
|
cfg["name"] = name_;
|
|
cfg["id"] = id_;
|
|
cfg["underlying_id"] = underlying_id_.value;
|
|
|
|
if(can_recruit()) {
|
|
cfg["canrecruit"] = true;
|
|
}
|
|
|
|
cfg["extra_recruit"] = utils::join(recruit_list_);
|
|
|
|
cfg["facing"] = map_location::write_direction(facing_);
|
|
|
|
cfg["goto_x"] = goto_.wml_x();
|
|
cfg["goto_y"] = goto_.wml_y();
|
|
|
|
cfg["moves"] = movement_;
|
|
if(write_all || get_attr_changed(UA_MAX_MP)) {
|
|
cfg["max_moves"] = max_movement_;
|
|
}
|
|
cfg["vision"] = vision_;
|
|
cfg["jamming"] = jamming_;
|
|
|
|
cfg["resting"] = resting_;
|
|
|
|
if(write_all || get_attr_changed(UA_ADVANCE_TO)) {
|
|
cfg["advances_to"] = utils::join(advances_to_);
|
|
}
|
|
|
|
cfg["race"] = race_->id();
|
|
cfg["language_name"] = type_name_;
|
|
cfg["undead_variation"] = undead_variation_;
|
|
if(write_all || get_attr_changed(UA_LEVEL)) {
|
|
cfg["level"] = level_;
|
|
}
|
|
if(write_all || get_attr_changed(UA_ALIGNMENT)) {
|
|
cfg["alignment"] = alignment_.to_string();
|
|
}
|
|
cfg["flag_rgb"] = flag_rgb_;
|
|
cfg["unrenamable"] = unrenamable_;
|
|
|
|
cfg["attacks_left"] = attacks_left_;
|
|
if(write_all || get_attr_changed(UA_MAX_AP)) {
|
|
cfg["max_attacks"] = max_attacks_;
|
|
}
|
|
if(write_all || get_attr_changed(UA_ZOC)) {
|
|
cfg["zoc"] = emit_zoc_;
|
|
}
|
|
cfg["hidden"] = hidden_;
|
|
|
|
if(write_all || get_attr_changed(UA_ATTACKS) || get_attacks_changed()) {
|
|
cfg.clear_children("attack");
|
|
for(attack_ptr i : attacks_) {
|
|
i->write(cfg.add_child("attack"));
|
|
}
|
|
}
|
|
|
|
cfg["cost"] = unit_value_;
|
|
|
|
write_subtag("modifications", modifications_);
|
|
if(write_all || get_attr_changed(UA_ABILITIES)) {
|
|
write_subtag("abilities", abilities_);
|
|
}
|
|
if(write_all || get_attr_changed(UA_ADVANCEMENTS)) {
|
|
cfg.clear_children("advancement");
|
|
for(const config& advancement : this->advancements_) {
|
|
if(!advancement.empty()) {
|
|
cfg.add_child("advancement", advancement);
|
|
}
|
|
}
|
|
}
|
|
cfg.append(back);
|
|
}
|
|
|
|
void unit::set_facing(map_location::DIRECTION dir) const
|
|
{
|
|
if(dir != map_location::NDIRECTIONS && dir != facing_) {
|
|
appearance_changed_ = true;
|
|
facing_ = dir;
|
|
}
|
|
// Else look at yourself (not available so continue to face the same direction)
|
|
}
|
|
|
|
int unit::upkeep() const
|
|
{
|
|
// Leaders do not incur upkeep.
|
|
if(can_recruit()) {
|
|
return 0;
|
|
}
|
|
|
|
return boost::apply_visitor(upkeep_value_visitor(*this), upkeep_);
|
|
}
|
|
|
|
bool unit::loyal() const
|
|
{
|
|
return boost::get<upkeep_loyal>(&upkeep_) != nullptr;
|
|
}
|
|
|
|
int unit::defense_modifier(const t_translation::terrain_code & terrain) const
|
|
{
|
|
int def = movement_type_.defense_modifier(terrain);
|
|
#if 0
|
|
// A [defense] ability is too costly and doesn't take into account target locations.
|
|
// Left as a comment in case someone ever wonders why it isn't a good idea.
|
|
unit_ability_list defense_abilities = get_abilities("defense");
|
|
if(!defense_abilities.empty()) {
|
|
unit_abilities::effect defense_effect(defense_abilities, def, false);
|
|
def = defense_effect.get_composite_value();
|
|
}
|
|
#endif
|
|
return def;
|
|
}
|
|
|
|
bool unit::resistance_filter_matches(const config& cfg, bool attacker, const std::string& damage_name, int res) const
|
|
{
|
|
if(!(cfg["active_on"].empty() || (attacker && cfg["active_on"] == "offense") || (!attacker && cfg["active_on"] == "defense"))) {
|
|
return false;
|
|
}
|
|
|
|
const std::string& apply_to = cfg["apply_to"];
|
|
if(!apply_to.empty()) {
|
|
if(damage_name != apply_to) {
|
|
if(apply_to.find(',') != std::string::npos &&
|
|
apply_to.find(damage_name) != std::string::npos) {
|
|
const std::vector<std::string>& vals = utils::split(apply_to);
|
|
if(std::find(vals.begin(),vals.end(),damage_name) == vals.end()) {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!unit_abilities::filter_base_matches(cfg, res)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int unit::resistance_against(const std::string& damage_name,bool attacker,const map_location& loc, const_attack_ptr weapon, const_attack_ptr opp_weapon) const
|
|
{
|
|
int res = movement_type_.resistance_against(damage_name);
|
|
|
|
unit_ability_list resistance_abilities = get_abilities("resistance",loc, weapon, opp_weapon);
|
|
for(unit_ability_list::iterator i = resistance_abilities.begin(); i != resistance_abilities.end();) {
|
|
if(!resistance_filter_matches(*i->first, attacker, damage_name, 100-res)) {
|
|
i = resistance_abilities.erase(i);
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
|
|
if(!resistance_abilities.empty()) {
|
|
unit_abilities::effect resist_effect(resistance_abilities, 100-res, false);
|
|
|
|
res = 100 - std::min<int>(
|
|
resist_effect.get_composite_value(),
|
|
resistance_abilities.highest("max_value").first
|
|
);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
std::map<std::string, std::string> unit::advancement_icons() const
|
|
{
|
|
std::map<std::string,std::string> temp;
|
|
if(!can_advance()) {
|
|
return temp;
|
|
}
|
|
|
|
if(!advances_to_.empty()) {
|
|
std::ostringstream tooltip;
|
|
const std::string& image = game_config::images::level;
|
|
|
|
for(const std::string& s : advances_to()) {
|
|
if(!s.empty()) {
|
|
tooltip << s << std::endl;
|
|
}
|
|
}
|
|
|
|
temp[image] = tooltip.str();
|
|
}
|
|
|
|
for(const config& adv : get_modification_advances()) {
|
|
const std::string& image = adv["image"];
|
|
if(image.empty()) {
|
|
continue;
|
|
}
|
|
|
|
std::ostringstream tooltip;
|
|
tooltip << temp[image];
|
|
|
|
const std::string& tt = adv["description"];
|
|
if(!tt.empty()) {
|
|
tooltip << tt << std::endl;
|
|
}
|
|
|
|
temp[image] = tooltip.str();
|
|
}
|
|
|
|
return(temp);
|
|
}
|
|
|
|
std::vector<std::pair<std::string, std::string>> unit::amla_icons() const
|
|
{
|
|
std::vector<std::pair<std::string, std::string>> temp;
|
|
std::pair<std::string, std::string> icon; // <image,tooltip>
|
|
|
|
for(const config& adv : get_modification_advances()) {
|
|
icon.first = adv["icon"].str();
|
|
icon.second = adv["description"].str();
|
|
|
|
for(unsigned j = 0, j_count = modification_count("advancement", adv["id"]); j < j_count; ++j) {
|
|
temp.push_back(icon);
|
|
}
|
|
}
|
|
|
|
return(temp);
|
|
}
|
|
|
|
std::vector<config> unit::get_modification_advances() const
|
|
{
|
|
std::vector<config> res;
|
|
for(const config& adv : modification_advancements()) {
|
|
if(adv["strict_amla"].to_bool() && !advances_to_.empty()) {
|
|
continue;
|
|
}
|
|
|
|
if(modification_count("advancement", adv["id"]) >= static_cast<unsigned>(adv["max_times"].to_int(1))) {
|
|
continue;
|
|
}
|
|
|
|
std::vector<std::string> temp_require = utils::split(adv["require_amla"]);
|
|
std::vector<std::string> temp_exclude = utils::split(adv["exclude_amla"]);
|
|
|
|
if(temp_require.empty() && temp_exclude.empty()) {
|
|
res.push_back(adv);
|
|
continue;
|
|
}
|
|
|
|
std::sort(temp_require.begin(), temp_require.end());
|
|
std::sort(temp_exclude.begin(), temp_exclude.end());
|
|
|
|
std::vector<std::string> uniq_require, uniq_exclude;
|
|
|
|
std::unique_copy(temp_require.begin(), temp_require.end(), std::back_inserter(uniq_require));
|
|
std::unique_copy(temp_exclude.begin(), temp_exclude.end(), std::back_inserter(uniq_exclude));
|
|
|
|
bool exclusion_found = false;
|
|
for(const std::string& s : uniq_exclude) {
|
|
int max_num = std::count(temp_exclude.begin(), temp_exclude.end(), s);
|
|
int mod_num = modification_count("advancement", s);
|
|
if(mod_num >= max_num) {
|
|
exclusion_found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(exclusion_found) {
|
|
continue;
|
|
}
|
|
|
|
bool requirements_done = true;
|
|
for(const std::string& s : uniq_require) {
|
|
int required_num = std::count(temp_require.begin(), temp_require.end(), s);
|
|
int mod_num = modification_count("advancement", s);
|
|
if(required_num > mod_num) {
|
|
requirements_done = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(requirements_done) {
|
|
res.push_back(adv);
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
void unit::set_advancements(std::vector<config> advancements)
|
|
{
|
|
set_attr_changed(UA_ADVANCEMENTS);
|
|
this->advancements_.clear();
|
|
for(config& advancement : advancements) {
|
|
this->advancements_.push_back(new config());
|
|
this->advancements_.back().swap(advancement);
|
|
}
|
|
}
|
|
|
|
std::size_t unit::modification_count(const std::string& mod_type, const std::string& id) const
|
|
{
|
|
std::size_t res = 0;
|
|
for(const config& item : modifications_.child_range(mod_type)) {
|
|
if(item["id"] == id) {
|
|
++res;
|
|
}
|
|
}
|
|
|
|
// For backwards compatibility, if asked for "advancement", also count "advance"
|
|
if(mod_type == "advancement") {
|
|
res += modification_count("advance", id);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
const std::set<std::string> unit::builtin_effects {
|
|
"alignment", "attack", "defense", "ellipse", "experience", "fearless",
|
|
"halo", "healthy", "hitpoints", "image_mod", "jamming", "jamming_costs",
|
|
"loyal", "max_attacks", "max_experience", "movement", "movement_costs",
|
|
"new_ability", "new_advancement", "new_animation", "new_attack", "overlay", "profile",
|
|
"recall_cost", "remove_ability", "remove_advancement", "remove_attacks", "resistance",
|
|
"status", "type", "variation", "vision", "vision_costs", "zoc"
|
|
};
|
|
|
|
std::string unit::describe_builtin_effect(std::string apply_to, const config& effect)
|
|
{
|
|
if(apply_to == "attack") {
|
|
std::vector<t_string> attack_names;
|
|
|
|
std::string desc;
|
|
for(attack_ptr a : attacks_) {
|
|
bool affected = a->describe_modification(effect, &desc);
|
|
if(affected && !desc.empty()) {
|
|
attack_names.emplace_back(a->name(), "wesnoth-units");
|
|
}
|
|
}
|
|
if(!attack_names.empty()) {
|
|
utils::string_map symbols;
|
|
symbols["attack_list"] = utils::format_conjunct_list("", attack_names);
|
|
symbols["effect_description"] = desc;
|
|
return VGETTEXT("$attack_list|: $effect_description", symbols);
|
|
}
|
|
} else if(apply_to == "hitpoints") {
|
|
const std::string& increase_total = effect["increase_total"];
|
|
if(!increase_total.empty()) {
|
|
return VGETTEXT(
|
|
"<span color=\"$color\">$number_or_percent</span> HP",
|
|
{{"number_or_percent", utils::print_modifier(increase_total)}, {"color", increase_total[0] == '-' ? "red" : "green"}});
|
|
}
|
|
} else {
|
|
const std::string& increase = effect["increase"];
|
|
if(increase.empty()) {
|
|
return "";
|
|
}
|
|
if(apply_to == "movement") {
|
|
return VNGETTEXT(
|
|
"<span color=\"$color\">$number_or_percent</span> move",
|
|
"<span color=\"$color\">$number_or_percent</span> moves",
|
|
std::stoi(increase),
|
|
{{"number_or_percent", utils::print_modifier(increase)}, {"color", increase[0] == '-' ? "red" : "green"}});
|
|
} else if(apply_to == "vision") {
|
|
return VGETTEXT(
|
|
"<span color=\"$color\">$number_or_percent</span> vision",
|
|
{{"number_or_percent", utils::print_modifier(increase)}, {"color", increase[0] == '-' ? "red" : "green"}});
|
|
} else if(apply_to == "jamming") {
|
|
return VGETTEXT(
|
|
"<span color=\"$color\">$number_or_percent</span> jamming",
|
|
{{"number_or_percent", utils::print_modifier(increase)}, {"color", increase[0] == '-' ? "red" : "green"}});
|
|
} else if(apply_to == "max_experience") {
|
|
// Unlike others, decreasing experience is a *GOOD* thing
|
|
return VGETTEXT(
|
|
"<span color=\"$color\">$number_or_percent</span> XP to advance",
|
|
{{"number_or_percent", utils::print_modifier(increase)}, {"color", increase[0] == '-' ? "green" : "red"}});
|
|
} else if(apply_to == "max_attacks") {
|
|
return VNGETTEXT(
|
|
"<span color=\"$color\">$number_or_percent</span> attack per turn",
|
|
"<span color=\"$color\">$number_or_percent</span> attacks per turn",
|
|
std::stoi(increase),
|
|
{{"number_or_percent", utils::print_modifier(increase)}, {"color", increase[0] == '-' ? "red" : "green"}});
|
|
} else if(apply_to == "recall_cost") {
|
|
// Unlike others, decreasing recall cost is a *GOOD* thing
|
|
return VGETTEXT(
|
|
"<span color=\"$color\">$number_or_percent</span> cost to recall",
|
|
{{"number_or_percent", utils::print_modifier(increase)}, {"color", increase[0] == '-' ? "green" : "red"}});
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
void unit::apply_builtin_effect(std::string apply_to, const config& effect)
|
|
{
|
|
appearance_changed_ = true;
|
|
if(apply_to == "fearless") {
|
|
set_attr_changed(UA_IS_FEARLESS);
|
|
is_fearless_ = effect["set"].to_bool(true);
|
|
} else if(apply_to == "healthy") {
|
|
set_attr_changed(UA_IS_HEALTHY);
|
|
is_healthy_ = effect["set"].to_bool(true);
|
|
} else if(apply_to == "profile") {
|
|
if(const config::attribute_value* v = effect.get("portrait")) {
|
|
set_big_profile((*v).str());
|
|
}
|
|
|
|
if(const config::attribute_value* v = effect.get("small_portrait")) {
|
|
set_small_profile((*v).str());
|
|
}
|
|
|
|
if(const config::attribute_value* v = effect.get("description")) {
|
|
description_ = *v;
|
|
}
|
|
} else if(apply_to == "new_attack") {
|
|
set_attr_changed(UA_ATTACKS);
|
|
attacks_.emplace_back(new attack_type(effect));
|
|
} else if(apply_to == "remove_attacks") {
|
|
set_attr_changed(UA_ATTACKS);
|
|
auto iter = std::remove_if(attacks_.begin(), attacks_.end(), [&effect](attack_ptr a) {
|
|
return a->matches_filter(effect);
|
|
});
|
|
|
|
attacks_.erase(iter, attacks_.end());
|
|
} else if(apply_to == "attack") {
|
|
set_attr_changed(UA_ATTACKS);
|
|
for(attack_ptr a : attacks_) {
|
|
a->apply_modification(effect);
|
|
}
|
|
} else if(apply_to == "hitpoints") {
|
|
LOG_UT << "applying hitpoint mod..." << hit_points_ << "/" << max_hit_points_ << std::endl;
|
|
const std::string& increase_hp = effect["increase"];
|
|
const std::string& increase_total = effect["increase_total"];
|
|
const std::string& set_hp = effect["set"];
|
|
const std::string& set_total = effect["set_total"];
|
|
|
|
// If the hitpoints are allowed to end up greater than max hitpoints
|
|
const bool violate_max = effect["violate_maximum"].to_bool();
|
|
|
|
if(!set_hp.empty()) {
|
|
if(set_hp.back() == '%') {
|
|
hit_points_ = lexical_cast_default<int>(set_hp)*max_hit_points_/100;
|
|
} else {
|
|
hit_points_ = lexical_cast_default<int>(set_hp);
|
|
}
|
|
}
|
|
|
|
if(!set_total.empty()) {
|
|
if(set_total.back() == '%') {
|
|
set_max_hitpoints(lexical_cast_default<int>(set_total)*max_hit_points_/100);
|
|
} else {
|
|
set_max_hitpoints(lexical_cast_default<int>(set_total));
|
|
}
|
|
}
|
|
|
|
if(!increase_total.empty()) {
|
|
// A percentage on the end means increase by that many percent
|
|
set_max_hitpoints(utils::apply_modifier(max_hit_points_, increase_total));
|
|
}
|
|
|
|
if(max_hit_points_ < 1)
|
|
set_max_hitpoints(1);
|
|
|
|
if(effect["heal_full"].to_bool()) {
|
|
heal_fully();
|
|
}
|
|
|
|
if(!increase_hp.empty()) {
|
|
hit_points_ = utils::apply_modifier(hit_points_, increase_hp);
|
|
}
|
|
|
|
LOG_UT << "modded to " << hit_points_ << "/" << max_hit_points_ << std::endl;
|
|
if(hit_points_ > max_hit_points_ && !violate_max) {
|
|
LOG_UT << "resetting hp to max" << std::endl;
|
|
hit_points_ = max_hit_points_;
|
|
}
|
|
|
|
if(hit_points_ < 1) {
|
|
hit_points_ = 1;
|
|
}
|
|
} else if(apply_to == "movement") {
|
|
const std::string& increase = effect["increase"];
|
|
|
|
if(!increase.empty()) {
|
|
set_total_movement(utils::apply_modifier(max_movement_, increase, 1));
|
|
}
|
|
|
|
set_total_movement(effect["set"].to_int(max_movement_));
|
|
|
|
if(movement_ > max_movement_) {
|
|
movement_ = max_movement_;
|
|
}
|
|
} else if(apply_to == "vision") {
|
|
const std::string& increase = effect["increase"];
|
|
|
|
if(!increase.empty()) {
|
|
const int current_vision = vision_ < 0 ? max_movement_ : vision_;
|
|
vision_ = utils::apply_modifier(current_vision, increase, 1);
|
|
}
|
|
|
|
vision_ = effect["set"].to_int(vision_);
|
|
} else if(apply_to == "jamming") {
|
|
const std::string& increase = effect["increase"];
|
|
|
|
if(!increase.empty()) {
|
|
jamming_ = utils::apply_modifier(jamming_, increase, 1);
|
|
}
|
|
|
|
jamming_ = effect["set"].to_int(jamming_);
|
|
} else if(apply_to == "experience") {
|
|
const std::string& increase = effect["increase"];
|
|
const std::string& set = effect["set"];
|
|
|
|
if(!set.empty()) {
|
|
if(set.back() == '%') {
|
|
experience_ = lexical_cast_default<int>(set)*max_experience_/100;
|
|
} else {
|
|
experience_ = lexical_cast_default<int>(set);
|
|
}
|
|
}
|
|
|
|
if(increase.empty() == false) {
|
|
experience_ = utils::apply_modifier(experience_, increase, 1);
|
|
}
|
|
} else if(apply_to == "max_experience") {
|
|
const std::string& increase = effect["increase"];
|
|
const std::string& set = effect["set"];
|
|
|
|
if(set.empty() == false) {
|
|
if(set.back() == '%') {
|
|
set_max_experience(lexical_cast_default<int>(set)*max_experience_/100);
|
|
} else {
|
|
set_max_experience(lexical_cast_default<int>(set));
|
|
}
|
|
}
|
|
|
|
if(increase.empty() == false) {
|
|
set_max_experience(utils::apply_modifier(max_experience_, increase, 1));
|
|
}
|
|
} else if(apply_to == upkeep_loyal::type()) {
|
|
upkeep_ = upkeep_loyal();
|
|
} else if(apply_to == "status") {
|
|
const std::string& add = effect["add"];
|
|
const std::string& remove = effect["remove"];
|
|
|
|
for(const std::string& to_add : utils::split(add))
|
|
{
|
|
set_state(to_add, true);
|
|
}
|
|
|
|
for(const std::string& to_remove : utils::split(remove))
|
|
{
|
|
set_state(to_remove, false);
|
|
}
|
|
} else if(std::find(movetype::effects.cbegin(), movetype::effects.cend(), apply_to) != movetype::effects.cend()) {
|
|
// "movement_costs", "vision_costs", "jamming_costs", "defense", "resistance"
|
|
if(const config& ap = effect.child(apply_to)) {
|
|
set_attr_changed(UA_MOVEMENT_TYPE);
|
|
movement_type_.merge(ap, apply_to, effect["replace"].to_bool());
|
|
}
|
|
} else if(apply_to == "zoc") {
|
|
if(const config::attribute_value* v = effect.get("value")) {
|
|
set_attr_changed(UA_ZOC);
|
|
emit_zoc_ = v->to_bool();
|
|
}
|
|
} else if(apply_to == "new_ability") {
|
|
if(const config& ab_effect = effect.child("abilities")) {
|
|
set_attr_changed(UA_ABILITIES);
|
|
config to_append;
|
|
for(const config::any_child &ab : ab_effect.all_children_range()) {
|
|
if(!has_ability_by_id(ab.cfg["id"])) {
|
|
to_append.add_child(ab.key, ab.cfg);
|
|
}
|
|
}
|
|
this->abilities_.append(to_append);
|
|
}
|
|
} else if(apply_to == "remove_ability") {
|
|
if(const config& ab_effect = effect.child("abilities")) {
|
|
for(const config::any_child &ab : ab_effect.all_children_range()) {
|
|
remove_ability_by_id(ab.cfg["id"]);
|
|
}
|
|
}
|
|
} else if(apply_to == "image_mod") {
|
|
LOG_UT << "applying image_mod" << std::endl;
|
|
std::string mod = effect["replace"];
|
|
if(!mod.empty()){
|
|
image_mods_ = mod;
|
|
}
|
|
LOG_UT << "applying image_mod" << std::endl;
|
|
mod = effect["add"].str();
|
|
if(!mod.empty()){
|
|
if(!image_mods_.empty()) {
|
|
image_mods_ += '~';
|
|
}
|
|
|
|
image_mods_ += mod;
|
|
}
|
|
|
|
game_config::add_color_info(effect);
|
|
LOG_UT << "applying image_mod" << std::endl;
|
|
} else if(apply_to == "new_animation") {
|
|
anim_comp_->apply_new_animation_effect(effect);
|
|
} else if(apply_to == "ellipse") {
|
|
set_image_ellipse(effect["ellipse"]);
|
|
} else if(apply_to == "halo") {
|
|
set_image_halo(effect["halo"]);
|
|
} else if(apply_to == "overlay") {
|
|
const std::string& add = effect["add"];
|
|
const std::string& replace = effect["replace"];
|
|
const std::string& remove = effect["remove"];
|
|
|
|
if(!add.empty()) {
|
|
for(const auto& to_add : utils::parenthetical_split(add, ',')) {
|
|
overlays_.push_back(to_add);
|
|
}
|
|
}
|
|
if(!remove.empty()) {
|
|
for(const auto& to_remove : utils::parenthetical_split(remove, ',')) {
|
|
overlays_.erase(std::remove(overlays_.begin(), overlays_.end(), to_remove), overlays_.end());
|
|
}
|
|
}
|
|
if(add.empty() && remove.empty() && !replace.empty()) {
|
|
overlays_ = utils::parenthetical_split(replace, ',');
|
|
}
|
|
} else if(apply_to == "new_advancement") {
|
|
const std::string& types = effect["types"];
|
|
const bool replace = effect["replace"].to_bool(false);
|
|
set_attr_changed(UA_ADVANCEMENTS);
|
|
|
|
if(!types.empty()) {
|
|
if(replace) {
|
|
advances_to_ = utils::parenthetical_split(types, ',');
|
|
} else {
|
|
std::vector<std::string> temp_advances = utils::parenthetical_split(types, ',');
|
|
std::copy(temp_advances.begin(), temp_advances.end(), std::back_inserter(advances_to_));
|
|
}
|
|
}
|
|
|
|
if(effect.has_child("advancement")) {
|
|
if(replace) {
|
|
advancements_.clear();
|
|
}
|
|
|
|
config temp = effect;
|
|
boost::copy(effect.child_range("advancement"), boost::make_function_output_iterator(ptr_vector_pushback(advancements_)));
|
|
}
|
|
} else if(apply_to == "remove_advancement") {
|
|
const std::string& types = effect["types"];
|
|
const std::string& amlas = effect["amlas"];
|
|
set_attr_changed(UA_ADVANCEMENTS);
|
|
|
|
std::vector<std::string> temp_advances = utils::parenthetical_split(types, ',');
|
|
std::vector<std::string>::iterator iter;
|
|
for(const std::string& unit : temp_advances) {
|
|
iter = std::find(advances_to_.begin(), advances_to_.end(), unit);
|
|
if(iter != advances_to_.end()) {
|
|
advances_to_.erase(iter);
|
|
}
|
|
}
|
|
|
|
temp_advances = utils::parenthetical_split(amlas, ',');
|
|
|
|
for(int i = advancements_.size() - 1; i >= 0; i--) {
|
|
if(std::find(temp_advances.begin(), temp_advances.end(), advancements_[i]["id"]) != temp_advances.end()) {
|
|
advancements_.erase(advancements_.begin() + i);
|
|
}
|
|
}
|
|
} else if(apply_to == "alignment") {
|
|
unit_type::ALIGNMENT new_align;
|
|
if(new_align.parse(effect["set"])) {
|
|
set_alignment(new_align);
|
|
}
|
|
} else if(apply_to == "max_attacks") {
|
|
const std::string& increase = effect["increase"];
|
|
|
|
if(!increase.empty()) {
|
|
set_max_attacks(utils::apply_modifier(max_attacks_, increase, 1));
|
|
}
|
|
} else if(apply_to == "recall_cost") {
|
|
const std::string& increase = effect["increase"];
|
|
const std::string& set = effect["set"];
|
|
const int recall_cost = recall_cost_ < 0 ? resources::gameboard->teams().at(side_).recall_cost() : recall_cost_;
|
|
|
|
if(!set.empty()) {
|
|
if(set.back() == '%') {
|
|
recall_cost_ = lexical_cast_default<int>(set)*recall_cost/100;
|
|
} else {
|
|
recall_cost_ = lexical_cast_default<int>(set);
|
|
}
|
|
}
|
|
|
|
if(!increase.empty()) {
|
|
recall_cost_ = utils::apply_modifier(recall_cost, increase, 1);
|
|
}
|
|
} else if(effect["apply_to"] == "variation") {
|
|
variation_ = effect["name"].str();
|
|
const unit_type* base_type = unit_types.find(type().base_id());
|
|
assert(base_type != nullptr);
|
|
advance_to(*base_type);
|
|
} else if(effect["apply_to"] == "type") {
|
|
std::string prev_type = effect["prev_type"];
|
|
if(prev_type.empty()) {
|
|
prev_type = type().base_id();
|
|
}
|
|
const std::string& new_type_id = effect["name"];
|
|
const unit_type* new_type = unit_types.find(new_type_id);
|
|
if(new_type) {
|
|
const bool heal_full = effect["heal_full"].to_bool(false);
|
|
advance_to(*new_type);
|
|
preferences::encountered_units().insert(new_type_id);
|
|
if(heal_full) {
|
|
heal_fully();
|
|
}
|
|
} else {
|
|
WRN_UT << "unknown type= in [effect]apply_to=type, ignoring" << std::endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
void unit::add_modification(const std::string& mod_type, const config& mod, bool no_add)
|
|
{
|
|
bool generate_description = mod["generate_description"].to_bool(true);
|
|
|
|
if(no_add == false) {
|
|
modifications_.add_child(mod_type, mod);
|
|
}
|
|
|
|
bool set_poisoned = false; // Tracks if the poisoned state was set after the type or variation was changed.
|
|
config last_effect;
|
|
std::vector<t_string> effects_description;
|
|
for(const config& effect : mod.child_range("effect")) {
|
|
// Apply SUF.
|
|
if(const config& afilter = effect.child("filter")) {
|
|
assert(resources::filter_con);
|
|
if(!unit_filter(vconfig(afilter)).matches(*this, loc_)) {
|
|
continue;
|
|
}
|
|
}
|
|
const std::string& apply_to = effect["apply_to"];
|
|
int times = effect["times"].to_int(1);
|
|
t_string description;
|
|
|
|
if(effect["times"] == "per level") {
|
|
times = level_;
|
|
}
|
|
|
|
if(times) {
|
|
while (times > 0) {
|
|
times --;
|
|
|
|
bool was_poisoned = get_state(STATE_POISONED);
|
|
if(apply_to == "variation" || apply_to == "type") {
|
|
// Apply unit type/variation changes last to avoid double applying effects on advance.
|
|
set_poisoned = false;
|
|
last_effect = effect;
|
|
continue;
|
|
}
|
|
|
|
std::string description_component;
|
|
if(resources::lua_kernel) {
|
|
description_component = resources::lua_kernel->apply_effect(apply_to, *this, effect, true);
|
|
} else if(builtin_effects.count(apply_to)) {
|
|
// Normally, the built-in effects are dispatched through Lua so that a user
|
|
// can override them if desired. However, since they're built-in, we can still
|
|
// apply them if the lua kernel is unavailable.
|
|
apply_builtin_effect(apply_to, effect);
|
|
description_component = describe_builtin_effect(apply_to, effect);
|
|
}
|
|
if(!times) {
|
|
description += description_component;
|
|
}
|
|
if(!was_poisoned && get_state(STATE_POISONED)) {
|
|
set_poisoned = true;
|
|
} else if(was_poisoned && !get_state(STATE_POISONED)) {
|
|
set_poisoned = false;
|
|
}
|
|
} // end while
|
|
} else { // for times = per level & level = 0 we still need to rebuild the descriptions
|
|
if(resources::lua_kernel) {
|
|
description += resources::lua_kernel->apply_effect(apply_to, *this, effect, false);
|
|
} else if(builtin_effects.count(apply_to)) {
|
|
description += describe_builtin_effect(apply_to, effect);
|
|
}
|
|
}
|
|
|
|
if(effect["times"] == "per level" && !times) {
|
|
description = VGETTEXT("$effect_description per level", {{"effect_description", description}});
|
|
}
|
|
|
|
if(!description.empty()) {
|
|
effects_description.push_back(description);
|
|
}
|
|
}
|
|
// Apply variations -- only apply if we are adding this for the first time.
|
|
if(!last_effect.empty() && no_add == false) {
|
|
std::string description;
|
|
if(resources::lua_kernel) {
|
|
description = resources::lua_kernel->apply_effect(last_effect["apply_to"], *this, last_effect, true);
|
|
} else if(builtin_effects.count(last_effect["apply_to"])) {
|
|
apply_builtin_effect(last_effect["apply_to"], last_effect);
|
|
description = describe_builtin_effect(last_effect["apply_to"], last_effect);
|
|
}
|
|
effects_description.push_back(description);
|
|
if(set_poisoned)
|
|
// An effect explicitly set the poisoned state, and this
|
|
// should override the unit being immune to poison.
|
|
set_state(STATE_POISONED, true);
|
|
}
|
|
|
|
t_string description;
|
|
|
|
const t_string& mod_description = mod["description"];
|
|
if(!mod_description.empty()) {
|
|
description = mod_description;
|
|
}
|
|
|
|
// Punctuation should be translatable: not all languages use Latin punctuation.
|
|
// (However, there maybe is a better way to do it)
|
|
if(generate_description && !effects_description.empty()) {
|
|
if(!mod_description.empty()) {
|
|
description += "\n";
|
|
}
|
|
|
|
for(const auto& desc_line : effects_description) {
|
|
description += desc_line + "\n";
|
|
}
|
|
}
|
|
|
|
// store trait info
|
|
if(mod_type == "trait") {
|
|
add_trait_description(mod, description);
|
|
}
|
|
|
|
//NOTE: if not a trait, description is currently not used
|
|
}
|
|
|
|
void unit::add_trait_description(const config& trait, const t_string& description)
|
|
{
|
|
const std::string& gender_string = gender_ == unit_race::FEMALE ? "female_name" : "male_name";
|
|
const auto& gender_specific_name = trait[gender_string];
|
|
|
|
const t_string name = gender_specific_name.empty() ? trait["name"] : gender_specific_name;
|
|
|
|
if(!name.empty()) {
|
|
trait_names_.push_back(name);
|
|
trait_descriptions_.push_back(description);
|
|
}
|
|
}
|
|
|
|
std::string unit::absolute_image() const
|
|
{
|
|
return type().icon().empty() ? type().image() : type().icon();
|
|
}
|
|
|
|
std::string unit::default_anim_image() const
|
|
{
|
|
return type().image().empty() ? type().icon() : type().image();
|
|
}
|
|
|
|
void unit::apply_modifications()
|
|
{
|
|
log_scope("apply mods");
|
|
|
|
variables_.clear_children("mods");
|
|
|
|
for(const auto& mod : ModificationTypes) {
|
|
if(mod == "advance" && modifications_.has_child(mod)) {
|
|
deprecated_message("[advance]", DEP_LEVEL::PREEMPTIVE, {1, 15, 0}, "Use [advancement] instead.");
|
|
}
|
|
|
|
for(const config& m : modifications_.child_range(mod)) {
|
|
lg::scope_logger inner_scope_logging_object__(lg::general(), "add mod");
|
|
add_modification(mod, m, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool unit::invisible(const map_location& loc, bool see_all) const
|
|
{
|
|
if(loc != get_location()) {
|
|
DBG_UT << "unit::invisible called: id = " << id() << " loc = " << loc << " get_loc = " << get_location() << std::endl;
|
|
}
|
|
|
|
// This is a quick condition to check, and it does not depend on the
|
|
// location (so might as well bypass the location-based cache).
|
|
if(get_state(STATE_UNCOVERED)) {
|
|
return false;
|
|
}
|
|
|
|
// Fetch from cache
|
|
/**
|
|
* @todo FIXME: We use the cache only when using the default see_all=true
|
|
* Maybe add a second cache if the see_all=false become more frequent.
|
|
*/
|
|
if(see_all) {
|
|
const auto itor = invisibility_cache_.find(loc);
|
|
if(itor != invisibility_cache_.end()) {
|
|
return itor->second;
|
|
}
|
|
}
|
|
|
|
// Test hidden status
|
|
static const std::string hides("hides");
|
|
bool is_inv = get_ability_bool(hides, loc);
|
|
if(is_inv){
|
|
is_inv = (resources::gameboard ? !resources::gameboard->would_be_discovered(loc, side_,see_all) : true);
|
|
}
|
|
|
|
if(see_all) {
|
|
// Add to caches
|
|
if(invisibility_cache_.empty()) {
|
|
units_with_cache.push_back(this);
|
|
}
|
|
|
|
invisibility_cache_[loc] = is_inv;
|
|
}
|
|
|
|
return is_inv;
|
|
}
|
|
|
|
bool unit::is_visible_to_team(const team& team, bool const see_all) const
|
|
{
|
|
const map_location& loc = get_location();
|
|
return is_visible_to_team(loc, team, see_all);
|
|
}
|
|
|
|
bool unit::is_visible_to_team(const map_location& loc, const team& team, bool const see_all) const
|
|
{
|
|
if(!display::get_singleton()->get_map().on_board(loc)) {
|
|
return false;
|
|
}
|
|
|
|
if(see_all) {
|
|
return true;
|
|
}
|
|
|
|
if(team.is_enemy(side()) && invisible(loc)) {
|
|
return false;
|
|
}
|
|
|
|
// allied planned moves are also visible under fog. (we assume that fake units on the map are always whiteboard markers)
|
|
if(!team.is_enemy(side()) && underlying_id_.is_fake()) {
|
|
return true;
|
|
}
|
|
|
|
// when the whiteboard planned unit map is applied, it uses moved the _real_ unit so
|
|
// underlying_id_.is_fake() will be false and the check above will not apply.
|
|
// TODO: improve this check so that is also works for allied planned units but without
|
|
// breaking sp campaigns with allies under fog. We probably need an explicit flag
|
|
// is_planned_ in unit that is set by the whiteboard.
|
|
if(team.side() == side()) {
|
|
return true;
|
|
}
|
|
|
|
if(team.fogged(loc)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void unit::set_underlying_id(n_unit::id_manager& id_manager)
|
|
{
|
|
if(underlying_id_.value == 0) {
|
|
if(synced_context::is_synced() || !resources::gamedata || resources::gamedata->phase() == game_data::INITIAL) {
|
|
underlying_id_ = id_manager.next_id();
|
|
} else {
|
|
underlying_id_ = id_manager.next_fake_id();
|
|
}
|
|
}
|
|
|
|
if(id_.empty() /*&& !underlying_id_.is_fake()*/) {
|
|
std::stringstream ss;
|
|
ss << (type_id().empty() ? "Unit" : type_id()) << "-" << underlying_id_.value;
|
|
id_ = ss.str();
|
|
}
|
|
}
|
|
|
|
unit& unit::mark_clone(bool is_temporary)
|
|
{
|
|
n_unit::id_manager& ids = resources::gameboard ? resources::gameboard->unit_id_manager() : n_unit::id_manager::global_instance();
|
|
if(is_temporary) {
|
|
underlying_id_ = ids.next_fake_id();
|
|
} else {
|
|
if(synced_context::is_synced() || !resources::gamedata || resources::gamedata->phase() == game_data::INITIAL) {
|
|
underlying_id_ = ids.next_id();
|
|
}
|
|
else {
|
|
underlying_id_ = ids.next_fake_id();
|
|
}
|
|
std::string::size_type pos = id_.find_last_of('-');
|
|
if(pos != std::string::npos && pos+1 < id_.size()
|
|
&& id_.find_first_not_of("0123456789", pos+1) == std::string::npos) {
|
|
// this appears to be a duplicate of a generic unit, so give it a new id
|
|
WRN_UT << "assigning new id to clone of generic unit " << id_ << std::endl;
|
|
id_.clear();
|
|
set_underlying_id(ids);
|
|
}
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
|
|
unit_movement_resetter::unit_movement_resetter(const unit &u, bool operate)
|
|
: u_(const_cast<unit&>(u))
|
|
, moves_(u.movement_left(true))
|
|
{
|
|
if(operate) {
|
|
u_.set_movement(u_.total_movement());
|
|
}
|
|
}
|
|
|
|
unit_movement_resetter::~unit_movement_resetter()
|
|
{
|
|
assert(resources::gameboard);
|
|
try {
|
|
if(!resources::gameboard->units().has_unit(&u_)) {
|
|
/*
|
|
* It might be valid that the unit is not in the unit map.
|
|
* It might also mean a no longer valid unit will be assigned to.
|
|
*/
|
|
DBG_UT << "The unit to be removed is not in the unit map." << std::endl;
|
|
}
|
|
|
|
u_.set_movement(moves_);
|
|
} catch(...) {}
|
|
}
|
|
|
|
std::string unit::TC_image_mods() const
|
|
{
|
|
return formatter() << "~RC(" << flag_rgb() << ">" << team::get_side_color_id(side()) << ")";
|
|
}
|
|
|
|
std::string unit::image_mods() const
|
|
{
|
|
if(!image_mods_.empty()) {
|
|
return formatter() << "~" << image_mods_ << TC_image_mods();
|
|
}
|
|
|
|
return TC_image_mods();
|
|
}
|
|
|
|
// Called by the Lua API after resetting an attack pointer.
|
|
bool unit::remove_attack(attack_ptr atk)
|
|
{
|
|
set_attr_changed(UA_ATTACKS);
|
|
auto iter = std::find(attacks_.begin(), attacks_.end(), atk);
|
|
if(iter == attacks_.end()) {
|
|
return false;
|
|
}
|
|
attacks_.erase(iter);
|
|
return true;
|
|
}
|
|
|
|
void unit::remove_attacks_ai()
|
|
{
|
|
if(attacks_left_ == max_attacks_) {
|
|
//TODO: add state_not_attacked
|
|
}
|
|
|
|
set_attacks(0);
|
|
}
|
|
|
|
void unit::remove_movement_ai()
|
|
{
|
|
if(movement_left() == total_movement()) {
|
|
set_state(STATE_NOT_MOVED,true);
|
|
}
|
|
|
|
set_movement(0, true);
|
|
}
|
|
|
|
void unit::set_hidden(bool state) const
|
|
{
|
|
// appearance_changed_ = true;
|
|
hidden_ = state;
|
|
if(!state) {
|
|
return;
|
|
}
|
|
|
|
// We need to get rid of haloes immediately to avoid display glitches
|
|
anim_comp_->clear_haloes();
|
|
}
|
|
|
|
void unit::set_image_halo(const std::string& halo)
|
|
{
|
|
appearance_changed_ = true;
|
|
anim_comp_->clear_haloes();
|
|
halo_.reset(new std::string(halo));
|
|
}
|
|
|
|
void unit::parse_upkeep(const config::attribute_value& upkeep)
|
|
{
|
|
if(upkeep.empty()) {
|
|
return;
|
|
}
|
|
|
|
// TODO: create abetter way to check whether it is actually an int.
|
|
int upkeep_int = upkeep.to_int(-99);
|
|
if(upkeep_int != -99) {
|
|
upkeep_ = upkeep_int;
|
|
} else if(upkeep == upkeep_loyal::type() || upkeep == "free") {
|
|
upkeep_ = upkeep_loyal();
|
|
} else if(upkeep == upkeep_full::type()) {
|
|
upkeep_ = upkeep_full();
|
|
} else {
|
|
WRN_UT << "Found invalid upkeep=\"" << upkeep << "\" in a unit" << std::endl;
|
|
upkeep_ = upkeep_full();
|
|
}
|
|
}
|
|
|
|
void unit::write_upkeep(config::attribute_value& upkeep) const
|
|
{
|
|
upkeep = boost::apply_visitor(upkeep_type_visitor(), upkeep_);
|
|
}
|
|
|
|
void unit::clear_changed_attributes()
|
|
{
|
|
changed_attributes_.reset();
|
|
for(const auto& a_ptr : attacks_) {
|
|
a_ptr->set_changed(false);
|
|
}
|
|
}
|
|
|
|
// Filters unimportant stats from the unit config and returns a checksum of
|
|
// the remaining config.
|
|
std::string get_checksum(const unit& u)
|
|
{
|
|
config unit_config;
|
|
config wcfg;
|
|
u.write(unit_config);
|
|
|
|
const std::string main_keys[] {
|
|
"advances_to",
|
|
"alignment",
|
|
"cost",
|
|
"experience",
|
|
"gender",
|
|
"hitpoints",
|
|
"ignore_race_traits",
|
|
"ignore_global_traits",
|
|
"level",
|
|
"recall_cost",
|
|
"max_attacks",
|
|
"max_experience",
|
|
"max_hitpoints",
|
|
"max_moves",
|
|
"movement",
|
|
"movement_type",
|
|
"race",
|
|
"random_traits",
|
|
"resting",
|
|
"undead_variation",
|
|
"upkeep",
|
|
"zoc",
|
|
""
|
|
};
|
|
|
|
for(int i = 0; !main_keys[i].empty(); ++i) {
|
|
wcfg[main_keys[i]] = unit_config[main_keys[i]];
|
|
}
|
|
|
|
const std::string attack_keys[] {
|
|
"name",
|
|
"type",
|
|
"range",
|
|
"damage",
|
|
"number",
|
|
""
|
|
};
|
|
|
|
for(const config& att : unit_config.child_range("attack")) {
|
|
config& child = wcfg.add_child("attack");
|
|
|
|
for(int i = 0; !attack_keys[i].empty(); ++i) {
|
|
child[attack_keys[i]] = att[attack_keys[i]];
|
|
}
|
|
|
|
for(const config& spec : att.child_range("specials")) {
|
|
config& child_spec = child.add_child("specials", spec);
|
|
|
|
child_spec.recursive_clear_value("description");
|
|
}
|
|
}
|
|
|
|
for(const config& abi : unit_config.child_range("abilities")) {
|
|
config& child = wcfg.add_child("abilities", abi);
|
|
|
|
child.recursive_clear_value("description");
|
|
child.recursive_clear_value("description_inactive");
|
|
child.recursive_clear_value("name");
|
|
child.recursive_clear_value("name_inactive");
|
|
}
|
|
|
|
for(const config& trait : unit_config.child_range("trait")) {
|
|
config& child = wcfg.add_child("trait", trait);
|
|
|
|
child.recursive_clear_value("description");
|
|
child.recursive_clear_value("male_name");
|
|
child.recursive_clear_value("female_name");
|
|
child.recursive_clear_value("name");
|
|
}
|
|
|
|
const std::string child_keys[] {
|
|
"advance_from",
|
|
"defense",
|
|
"movement_costs",
|
|
"vision_costs",
|
|
"jamming_costs",
|
|
"resistance",
|
|
""
|
|
};
|
|
|
|
for(int i = 0; !child_keys[i].empty(); ++i) {
|
|
for(const config& c : unit_config.child_range(child_keys[i])) {
|
|
wcfg.add_child(child_keys[i], c);
|
|
}
|
|
}
|
|
|
|
DBG_UT << wcfg;
|
|
|
|
return wcfg.hash();
|
|
}
|
|
|
|
void swap(unit& lhs, unit& rhs)
|
|
{
|
|
lhs.swap(rhs);
|
|
}
|