mirror of
https://github.com/wesnoth/wesnoth
synced 2025-04-24 20:59:29 +00:00

If two [damage_type]alternative_type= are used with two different types, the chosen type displayed in the pre-combat window will be the one to which the opponent is most vulnerable. That type will then also be used in the attack if it is stronger than the original/replacement_type. In the sidebar (report) all alternative_types are displayed. --------- Co-authored-by: Gunter Labes <soliton@wesnoth.org>
2941 lines
81 KiB
C++
2941 lines
81 KiB
C++
/*
|
|
Copyright (C) 2003 - 2024
|
|
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 "ai/manager.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_events/manager.hpp" // for add_events
|
|
#include "game_version.hpp"
|
|
#include "lexical_cast.hpp"
|
|
#include "log.hpp" // for LOG_STREAM, logger, etc
|
|
#include "map/map.hpp" // for gamemap
|
|
#include "preferences/game.hpp" // for encountered_units
|
|
#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 "synced_context.hpp"
|
|
#include "team.hpp" // for team, get_teams, etc
|
|
#include "units/abilities.hpp" // for effect, filter_base_matches
|
|
#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 "units/types.hpp"
|
|
#include "utils/config_filters.hpp"
|
|
#include "variable.hpp" // for vconfig, etc
|
|
|
|
#include <cassert> // for assert
|
|
#include <cstdlib> // for rand
|
|
#include <exception> // for exception
|
|
#include <iterator> // for back_insert_iterator, etc
|
|
#include <string_view>
|
|
|
|
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::set<std::string_view> 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 const std::set<std::string_view> internalized_attrs {
|
|
"type",
|
|
"id",
|
|
"name",
|
|
"male_name",
|
|
"female_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"
|
|
};
|
|
|
|
void warn_unknown_attribute(const config::const_attr_itors& cfg)
|
|
{
|
|
config::const_attribute_iterator cur = cfg.begin();
|
|
config::const_attribute_iterator end = cfg.end();
|
|
|
|
auto cur_known = internalized_attrs.begin();
|
|
auto end_known = internalized_attrs.end();
|
|
|
|
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.";
|
|
++cur;
|
|
}
|
|
else if(comp == 0) {
|
|
++cur;
|
|
++cur_known;
|
|
}
|
|
else {
|
|
++cur_known;
|
|
}
|
|
}
|
|
|
|
while(cur != end) {
|
|
WRN_UT << "Unknown attribute '" << cur->first << "' discarded.";
|
|
++cur;
|
|
}
|
|
}
|
|
|
|
auto stats_storage_resetter(unit& u, bool clamp = false)
|
|
{
|
|
int hitpoints = u.hitpoints();
|
|
int moves = u.movement_left();
|
|
int attacks = u.attacks_left(true);
|
|
int experience= u.experience();
|
|
bool slowed= u.get_state(unit::STATE_SLOWED);
|
|
bool poisoned= u.get_state(unit::STATE_POISONED);
|
|
return [=, &u] () {
|
|
if(clamp) {
|
|
u.set_movement(std::min(u.total_movement(), moves));
|
|
u.set_hitpoints(std::min(u.max_hitpoints(), hitpoints));
|
|
u.set_attacks(std::min(u.max_attacks(), attacks));
|
|
} else {
|
|
u.set_movement(moves);
|
|
u.set_hitpoints(hitpoints);
|
|
u.set_attacks(attacks);
|
|
}
|
|
u.set_experience(experience);
|
|
u.set_state(unit::STATE_SLOWED, slowed && !u.get_state("unslowable"));
|
|
u.set_state(unit::STATE_POISONED, poisoned && !u.get_state("unpoisonable"));
|
|
};
|
|
}
|
|
} // end anon namespace
|
|
|
|
/**
|
|
* 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)];
|
|
}
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
// Copy constructor
|
|
unit::unit(const unit& o)
|
|
: std::enable_shared_from_this<unit>()
|
|
, 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_)
|
|
, trait_nonhidden_ids_(o.trait_nonhidden_ids_)
|
|
, 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_)
|
|
, special_notes_(o.special_notes_)
|
|
, usage_(o.usage_)
|
|
, halo_(o.halo_)
|
|
, ellipse_(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(unit_ctor_t)
|
|
: std::enable_shared_from_this<unit>()
|
|
, 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_()
|
|
, trait_nonhidden_ids_()
|
|
, 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_()
|
|
, special_notes_()
|
|
, 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();
|
|
variation_ = cfg["variation"].empty() ? type_->default_variation() : cfg["variation"].str();
|
|
canrecruit_ = cfg["canrecruit"].to_bool();
|
|
gender_ = generate_gender(*type_, cfg);
|
|
name_ = gender_value(cfg, gender_, "male_name", "female_name", "name").t_str();
|
|
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::lua_kernel) {
|
|
resources::game_events->add_events(events_.child_range("event"), *resources::lua_kernel);
|
|
}
|
|
|
|
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));
|
|
|
|
for(const config& mods : cfg.child_range("modifications")) {
|
|
modifications_.append_children(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(auto variables = cfg.optional_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(0, 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(auto ai = cfg.optional_child("ai")) {
|
|
formula_man_->read(*ai);
|
|
config ai_events;
|
|
for(config mai : ai->child_range("micro_ai")) {
|
|
mai.clear_children("filter");
|
|
mai.add_child("filter")["id"] = id();
|
|
mai["side"] = side();
|
|
mai["action"] = "add";
|
|
ai_events.add_child("micro_ai", mai);
|
|
}
|
|
for(config ca : ai->child_range("candidate_action")) {
|
|
ca.clear_children("filter_own");
|
|
ca.add_child("filter_own")["id"] = id();
|
|
// Sticky candidate actions not supported here (they cause a crash because the unit isn't on the map yet)
|
|
ca.remove_attribute("sticky");
|
|
std::string stage = "main_loop";
|
|
if(ca.has_attribute("stage")) {
|
|
stage = ca["stage"].str();
|
|
ca.remove_attribute("stage");
|
|
}
|
|
config mod{
|
|
"action", "add",
|
|
"side", side(),
|
|
"path", "stage[" + stage + "].candidate_action[]",
|
|
"candidate_action", ca,
|
|
};
|
|
ai_events.add_child("modify_ai", mod);
|
|
}
|
|
if(ai_events.all_children_count() > 0) {
|
|
ai::manager::get_singleton().append_active_ai_for_side(side(), ai_events);
|
|
}
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
}
|
|
|
|
// Don't use the unit_type's special notes if this config has its own defined
|
|
if(config::const_child_itors cfg_range = cfg.child_range("special_note")) {
|
|
set_attr_changed(UA_NOTES);
|
|
special_notes_.clear();
|
|
for(const config& c : cfg_range) {
|
|
special_notes_.emplace_back(c["note"].t_str());
|
|
}
|
|
}
|
|
|
|
// If cfg specifies [advancement]s, replace this [advancement]s with them.
|
|
if(cfg.has_child("advancement")) {
|
|
set_attr_changed(UA_ADVANCEMENTS);
|
|
advancements_.clear();
|
|
for(const config& adv : cfg.child_range("advancement")) {
|
|
advancements_.push_back(adv);
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
abilities_.append(abilities);
|
|
}
|
|
}
|
|
|
|
if(const config::attribute_value* v = cfg.get("alignment")) {
|
|
set_attr_changed(UA_ALIGNMENT);
|
|
auto new_align = unit_alignments::get_enum(v->str());
|
|
if(new_align) {
|
|
alignment_ = *new_align;
|
|
}
|
|
}
|
|
|
|
// 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(auto status_flags = cfg.optional_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(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"]));
|
|
|
|
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, const std::string& variation)
|
|
{
|
|
type_ = &u_type;
|
|
race_ = &unit_race::null_race;
|
|
variation_ = variation.empty() ? type_->default_variation() : 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();
|
|
} catch(...) {
|
|
DBG_UT << "Caught general exception when destroying unit: " << utils::get_unknown_exception_type();
|
|
}
|
|
}
|
|
|
|
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;
|
|
const unit_type& u_type = type();
|
|
|
|
config::const_child_itors current_traits = modifications_.child_range("trait");
|
|
|
|
// Handle must-have only at the beginning
|
|
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;
|
|
}
|
|
}
|
|
|
|
if(must_have_only) {
|
|
return;
|
|
}
|
|
|
|
std::vector<const config*> candidate_traits;
|
|
std::vector<std::string> temp_require_traits;
|
|
std::vector<std::string> temp_exclude_traits;
|
|
|
|
// 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; ++nb_traits)
|
|
{
|
|
current_traits = modifications_.child_range("trait");
|
|
candidate_traits.clear();
|
|
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;
|
|
}
|
|
// Skip trait if trait requirements are not met
|
|
// or trait exclusions are present
|
|
temp_require_traits = utils::split(t["require_traits"]);
|
|
temp_exclude_traits = utils::split(t["exclude_traits"]);
|
|
|
|
// See if the unit already has a trait that excludes the current one
|
|
for(const config& mod : current_traits) {
|
|
if (mod["exclude_traits"] != "") {
|
|
for (const auto& c: utils::split(mod["exclude_traits"])) {
|
|
temp_exclude_traits.push_back(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
// First check for requirements
|
|
bool trait_req_met = true;
|
|
for(const std::string& s : temp_require_traits) {
|
|
bool has_trait = false;
|
|
for(const config& mod : current_traits) {
|
|
if (mod["id"] == s)
|
|
has_trait = true;
|
|
}
|
|
if(!has_trait) {
|
|
trait_req_met = false;
|
|
break;
|
|
}
|
|
}
|
|
if(!trait_req_met) {
|
|
continue;
|
|
}
|
|
|
|
// Now check for exclusionary traits
|
|
bool trait_exc_met = true;
|
|
|
|
for(const std::string& s : temp_exclude_traits) {
|
|
bool has_exclusionary_trait = false;
|
|
for(const config& mod : current_traits) {
|
|
if (mod["id"] == s)
|
|
has_exclusionary_trait = true;
|
|
}
|
|
if (tid == s) {
|
|
has_exclusionary_trait = true;
|
|
}
|
|
if(has_exclusionary_trait) {
|
|
trait_exc_met = false;
|
|
break;
|
|
}
|
|
}
|
|
if(!trait_exc_met) {
|
|
continue;
|
|
}
|
|
|
|
const std::string& avl = t["availability"];
|
|
// 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);
|
|
}
|
|
}
|
|
// No traits available anymore? Break
|
|
if(candidate_traits.empty()) {
|
|
break;
|
|
}
|
|
|
|
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 hitpoints/movement/attacks_left is left unchanged unless it would violate their maximum.
|
|
* Assumes gender_ and variation_ are set to their correct values.
|
|
*/
|
|
void unit::advance_to(const unit_type& u_type, bool use_traits)
|
|
{
|
|
auto ss = stats_storage_resetter(*this, true);
|
|
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_);
|
|
// In case u_type was already a variation, make sure our variation is set correctly.
|
|
variation_ = new_type.variation_id();
|
|
|
|
// Reset the scalar values first
|
|
trait_names_.clear();
|
|
trait_descriptions_.clear();
|
|
trait_nonhidden_ids_.clear();
|
|
is_fearless_ = false;
|
|
is_healthy_ = false;
|
|
image_mods_.clear();
|
|
overlays_.clear();
|
|
ellipse_.reset();
|
|
|
|
// Clear modification-related caches
|
|
modification_descriptions_.clear();
|
|
|
|
|
|
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(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();
|
|
special_notes_ = new_type.direct_special_notes();
|
|
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.
|
|
// Make sure apply_modifications() didn't attempt to heal the unit (for example if the unit has a default amla.).
|
|
ss();
|
|
if(get_state("unpetrifiable")) {
|
|
set_state(STATE_PETRIFIED, false);
|
|
}
|
|
|
|
// In case the unit carries EventWML, apply it now
|
|
if(resources::game_events && resources::lua_kernel) {
|
|
resources::game_events->add_events(new_type.events(), *resources::lua_kernel, 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::hp_color_max()
|
|
{
|
|
return hp_color_impl(1, 1);
|
|
}
|
|
|
|
color_t unit::xp_color(int xp_to_advance, bool can_advance, bool has_amla)
|
|
{
|
|
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>(xp_to_advance) <= game_config::kill_experience;
|
|
const bool mid_advance = static_cast<int>(xp_to_advance) <= game_config::kill_experience*2;
|
|
const bool far_advance = static_cast<int>(xp_to_advance) <= game_config::kill_experience*3;
|
|
|
|
color_t color = normal_color;
|
|
if(can_advance){
|
|
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(has_amla){
|
|
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);
|
|
}
|
|
|
|
color_t unit::xp_color() const
|
|
{
|
|
bool major_amla = false;
|
|
bool has_amla = false;
|
|
for(const config& adv:get_modification_advances()){
|
|
major_amla |= adv["major_amla"].to_bool();
|
|
has_amla = true;
|
|
}
|
|
//TODO: calculating has_amla and major_amla can be a quite slow operation, we should cache these two values somehow.
|
|
return xp_color(experience_to_advance(), !advances_to().empty() || major_amla, has_amla);
|
|
}
|
|
|
|
void unit::set_recruits(const std::vector<std::string>& recruits)
|
|
{
|
|
unit_types.check_types(recruits);
|
|
recruit_list_ = recruits;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
// 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_.mandatory_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);
|
|
}
|
|
}
|
|
|
|
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},
|
|
{"invulnerable", STATE_INVULNERABLE},
|
|
};
|
|
|
|
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 : 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 = abilities_.ordered_begin();
|
|
while (i != abilities_.ordered_end()) {
|
|
if(i->cfg["id"] == ability) {
|
|
i = abilities_.erase(i);
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool matches_ability_filter(const config & cfg, const std::string& tag_name, const config & filter)
|
|
{
|
|
using namespace utils::config_filters;
|
|
|
|
if(!filter["affect_adjacent"].empty()){
|
|
bool adjacent = cfg.has_child("affect_adjacent");
|
|
if(filter["affect_adjacent"].to_bool() != adjacent){
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if(!bool_matches_if_present(filter, cfg, "affect_self", true))
|
|
return false;
|
|
|
|
if(!bool_or_empty(filter, cfg, "affect_allies"))
|
|
return false;
|
|
|
|
if(!bool_matches_if_present(filter, cfg, "affect_enemies", false))
|
|
return false;
|
|
|
|
if(!bool_matches_if_present(filter, cfg, "cumulative", false))
|
|
return false;
|
|
|
|
const std::vector<std::string> filter_type = utils::split(filter["tag_name"]);
|
|
if ( !filter_type.empty() && std::find(filter_type.begin(), filter_type.end(), tag_name) == filter_type.end() )
|
|
return false;
|
|
|
|
if(!string_matches_if_present(filter, cfg, "id", ""))
|
|
return false;
|
|
|
|
if(!string_matches_if_present(filter, cfg, "apply_to", "self"))
|
|
return false;
|
|
|
|
if(!string_matches_if_present(filter, cfg, "overwrite_specials", "none"))
|
|
return false;
|
|
|
|
if(!string_matches_if_present(filter, cfg, "active_on", "both"))
|
|
return false;
|
|
|
|
//for damage only
|
|
if(!string_matches_if_present(filter, cfg, "replacement_type", ""))
|
|
return false;
|
|
|
|
if(!string_matches_if_present(filter, cfg, "alternative_type", ""))
|
|
return false;
|
|
|
|
//for plague only
|
|
if(!string_matches_if_present(filter, cfg, "type", ""))
|
|
return false;
|
|
|
|
if(!filter["value"].empty()){
|
|
if(tag_name == "drains"){
|
|
if(!int_matches_if_present(filter, cfg, "value", 50)){
|
|
return false;
|
|
}
|
|
} else if(tag_name == "berserk"){
|
|
if(!int_matches_if_present(filter, cfg, "value", 1)){
|
|
return false;
|
|
}
|
|
} else if(tag_name == "heal_on_hit" || tag_name == "heals" || tag_name == "regenerate" || tag_name == "leadership"){
|
|
if(!int_matches_if_present(filter, cfg, "value" , 0)){
|
|
return false;
|
|
}
|
|
} else {
|
|
if(!int_matches_if_present(filter, cfg, "value")){
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!int_matches_if_present_or_negative(filter, cfg, "add", "sub"))
|
|
return false;
|
|
|
|
if(!int_matches_if_present_or_negative(filter, cfg, "sub", "add"))
|
|
return false;
|
|
|
|
if(!double_matches_if_present(filter, cfg, "multiply"))
|
|
return false;
|
|
|
|
if(!double_matches_if_present(filter, cfg, "divide"))
|
|
return false;
|
|
|
|
// Passed all tests.
|
|
return true;
|
|
}
|
|
|
|
bool unit::ability_matches_filter(const config & cfg, const std::string& tag_name, const config & filter) const
|
|
{
|
|
// Handle the basic filter.
|
|
bool matches = matches_ability_filter(cfg, tag_name, filter);
|
|
|
|
// Handle [and], [or], and [not] with in-order precedence
|
|
for (const config::any_child condition : filter.all_children_range() )
|
|
{
|
|
// Handle [and]
|
|
if ( condition.key == "and" )
|
|
matches = matches && ability_matches_filter(cfg, tag_name, condition.cfg);
|
|
|
|
// Handle [or]
|
|
else if ( condition.key == "or" )
|
|
matches = matches || ability_matches_filter(cfg, tag_name, condition.cfg);
|
|
|
|
// Handle [not]
|
|
else if ( condition.key == "not" )
|
|
matches = matches && !ability_matches_filter(cfg, tag_name, condition.cfg);
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
void unit::remove_ability_by_attribute(const config& filter)
|
|
{
|
|
set_attr_changed(UA_ABILITIES);
|
|
config::all_children_iterator i = abilities_.ordered_begin();
|
|
while (i != abilities_.ordered_end()) {
|
|
if(ability_matches_filter(i->cfg, i->key, filter)) {
|
|
i = 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, false);
|
|
}
|
|
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(write_all || get_attr_changed(UA_NOTES)) {
|
|
for(const t_string& note : special_notes_) {
|
|
cfg.add_child("special_note")["note"] = note;
|
|
}
|
|
}
|
|
|
|
if(halo_) {
|
|
cfg["halo"] = *halo_;
|
|
}
|
|
|
|
if(ellipse_) {
|
|
cfg["ellipse"] = *ellipse_;
|
|
}
|
|
|
|
if(usage_) {
|
|
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().parent_id()) {
|
|
cfg["parent_type"] = type().parent_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"] = unit_alignments::get_string(alignment_);
|
|
}
|
|
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 : 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 utils::visit(upkeep_value_visitor{*this}, upkeep_);
|
|
}
|
|
|
|
bool unit::loyal() const
|
|
{
|
|
return utils::holds_alternative<upkeep_loyal>(upkeep_);
|
|
}
|
|
|
|
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);
|
|
def = defense_effect.get_composite_value();
|
|
}
|
|
#endif
|
|
return def;
|
|
}
|
|
|
|
bool unit::resistance_filter_matches(const config& cfg, const std::string& damage_name, int res) const
|
|
{
|
|
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_value(unit_ability_list resistance_list, const std::string& damage_name) const
|
|
{
|
|
int res = movement_type_.resistance_against(damage_name);
|
|
utils::erase_if(resistance_list, [&](const unit_ability& i) {
|
|
return !resistance_filter_matches(*i.ability_cfg, damage_name, 100-res);
|
|
});
|
|
|
|
if(!resistance_list.empty()) {
|
|
unit_abilities::effect resist_effect(resistance_list, 100-res, nullptr, unit_abilities::EFFECT_CLAMP_MIN_MAX);
|
|
|
|
res = 100 - resist_effect.get_composite_value();
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
static bool resistance_filter_matches_base(const config& cfg, bool attacker)
|
|
{
|
|
if(!(!cfg.has_attribute("active_on") || (attacker && cfg["active_on"] == "offense") || (!attacker && cfg["active_on"] == "defense"))) {
|
|
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
|
|
{
|
|
unit_ability_list resistance_list = get_abilities_weapons("resistance",loc, weapon, opp_weapon);
|
|
utils::erase_if(resistance_list, [&](const unit_ability& i) {
|
|
return !resistance_filter_matches_base(*i.ability_cfg, attacker);
|
|
});
|
|
if(opp_weapon){
|
|
unit_ability_list damage_type_list = opp_weapon->get_specials_and_abilities("damage_type");
|
|
if(damage_type_list.empty()){
|
|
return resistance_value(resistance_list, damage_name);
|
|
}
|
|
std::string replacement_type = opp_weapon->select_damage_type(damage_type_list, "replacement_type", resistance_list);
|
|
std::string type_damage = replacement_type.empty() ? damage_name : replacement_type;
|
|
int max_res = resistance_value(resistance_list, type_damage);
|
|
for(auto& i : damage_type_list) {
|
|
if((*i.ability_cfg).has_attribute("alternative_type")){
|
|
max_res = std::max(max_res , resistance_value(resistance_list, (*i.ability_cfg)["alternative_type"].str()));
|
|
}
|
|
}
|
|
return max_res;
|
|
}
|
|
return resistance_value(resistance_list, damage_name);
|
|
}
|
|
|
|
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(auto filter = adv.optional_child("filter")) {
|
|
if(!unit_filter(vconfig(*filter)).matches(*this, loc_)) {
|
|
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);
|
|
advancements_ = advancements;
|
|
}
|
|
|
|
const std::string& unit::type_id() const
|
|
{
|
|
return type_->id();
|
|
}
|
|
|
|
void unit::set_big_profile(const std::string& value)
|
|
{
|
|
set_attr_changed(UA_PROFILE);
|
|
profile_ = value;
|
|
adjust_profile(profile_);
|
|
}
|
|
|
|
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", "level",
|
|
"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] == '-' ? "#f00" : "#0f0"}});
|
|
}
|
|
} 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] == '-' ? "#f00" : "#0f0"}});
|
|
} 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] == '-' ? "#f00" : "#0f0"}});
|
|
} 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] == '-' ? "#f00" : "#0f0"}});
|
|
} 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] == '-' ? "#0f0" : "#f00"}});
|
|
} 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] == '-' ? "#f00" : "#0f0"}});
|
|
} 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] == '-' ? "#0f0" : "#f00"}});
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
if(config::const_child_itors cfg_range = effect.child_range("special_note")) {
|
|
for(const config& c : cfg_range) {
|
|
if(!c["remove"].to_bool()) {
|
|
special_notes_.emplace_back(c["note"].t_str());
|
|
} else {
|
|
auto iter = std::find(special_notes_.begin(), special_notes_.end(), c["note"].t_str());
|
|
if(iter != special_notes_.end()) {
|
|
special_notes_.erase(iter);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} 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_;
|
|
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_;
|
|
if(hit_points_ > max_hit_points_ && !violate_max) {
|
|
LOG_UT << "resetting hp to max";
|
|
hit_points_ = max_hit_points_;
|
|
}
|
|
|
|
if(hit_points_ < 1) {
|
|
hit_points_ = 1;
|
|
}
|
|
} else if(apply_to == "movement") {
|
|
const bool apply_to_vision = effect["apply_to_vision"].to_bool(true);
|
|
|
|
// Unlink vision from movement, regardless of whether we'll increment both or not
|
|
if(vision_ < 0) {
|
|
vision_ = max_movement_;
|
|
}
|
|
|
|
const int old_max = max_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_;
|
|
}
|
|
|
|
if(apply_to_vision) {
|
|
vision_ = std::max(0, vision_ + max_movement_ - old_max);
|
|
}
|
|
} else if(apply_to == "vision") {
|
|
// Unlink vision from movement, regardless of which one we're about to change.
|
|
if(vision_ < 0) {
|
|
vision_ = max_movement_;
|
|
}
|
|
|
|
const std::string& increase = effect["increase"];
|
|
if(!increase.empty()) {
|
|
vision_ = utils::apply_modifier(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, 0);
|
|
}
|
|
} 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(auto ap = effect.optional_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(auto ab_effect = effect.optional_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);
|
|
}
|
|
}
|
|
abilities_.append(to_append);
|
|
}
|
|
} else if(apply_to == "remove_ability") {
|
|
if(auto ab_effect = effect.optional_child("abilities")) {
|
|
for(const config::any_child ab : ab_effect->all_children_range()) {
|
|
remove_ability_by_id(ab.cfg["id"]);
|
|
}
|
|
}
|
|
if(auto fab_effect = effect.optional_child("experimental_filter_ability")) {
|
|
remove_ability_by_attribute(*fab_effect);
|
|
}
|
|
} else if(apply_to == "image_mod") {
|
|
LOG_UT << "applying image_mod";
|
|
std::string mod = effect["replace"];
|
|
if(!mod.empty()){
|
|
image_mods_ = mod;
|
|
}
|
|
LOG_UT << "applying image_mod";
|
|
mod = effect["add"].str();
|
|
if(!mod.empty()){
|
|
if(!image_mods_.empty()) {
|
|
image_mods_ += '~';
|
|
}
|
|
|
|
image_mods_ += mod;
|
|
}
|
|
|
|
game_config::add_color_info(game_config_view::wrap(effect));
|
|
LOG_UT << "applying image_mod";
|
|
} 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();
|
|
}
|
|
|
|
for(const config& adv : effect.child_range("advancement")) {
|
|
advancements_.push_back(adv);
|
|
}
|
|
}
|
|
} 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") {
|
|
auto new_align = unit_alignments::get_enum(effect["set"].str());
|
|
if(new_align) {
|
|
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 team_recall_cost = resources::gameboard ? resources::gameboard->get_team(side_).recall_cost() : 20;
|
|
const int recall_cost = recall_cost_ < 0 ? team_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") {
|
|
const unit_type* base_type = unit_types.find(type().parent_id());
|
|
assert(base_type != nullptr);
|
|
const std::string& variation_id = effect["name"];
|
|
if(variation_id.empty() || base_type->get_gender_unit_type(gender_).has_variation(variation_id)) {
|
|
variation_ = variation_id;
|
|
advance_to(*base_type);
|
|
if(effect["heal_full"].to_bool(false)) {
|
|
heal_fully();
|
|
}
|
|
} else {
|
|
WRN_UT << "unknown variation '" << variation_id << "' (name=) in [effect]apply_to=variation, ignoring";
|
|
}
|
|
} else if(effect["apply_to"] == "type") {
|
|
std::string prev_type = effect["prev_type"];
|
|
if(prev_type.empty()) {
|
|
prev_type = type().parent_id();
|
|
}
|
|
const std::string& new_type_id = effect["name"];
|
|
const unit_type* new_type = unit_types.find(new_type_id);
|
|
if(new_type) {
|
|
advance_to(*new_type);
|
|
preferences::encountered_units().insert(new_type_id);
|
|
if(effect["heal_full"].to_bool(false)) {
|
|
heal_fully();
|
|
}
|
|
} else {
|
|
WRN_UT << "unknown type '" << new_type_id << "' (name=) in [effect]apply_to=type, ignoring";
|
|
}
|
|
} else if(effect["apply_to"] == "level") {
|
|
const std::string& increase = effect["increase"];
|
|
const std::string& set = effect["set"];
|
|
|
|
set_attr_changed(UA_LEVEL);
|
|
|
|
// no support for percentages, since levels are usually small numbers
|
|
|
|
if(!set.empty()) {
|
|
level_ = lexical_cast_default<int>(set);
|
|
}
|
|
|
|
if(!increase.empty()) {
|
|
level_ += lexical_cast_default<int>(increase);
|
|
}
|
|
}
|
|
}
|
|
|
|
void unit::add_modification(const std::string& mod_type, const config& mod, bool no_add)
|
|
{
|
|
bool generate_description = mod["generate_description"].to_bool(true);
|
|
|
|
config* target = nullptr;
|
|
|
|
if(no_add == false) {
|
|
target = &modifications_.add_child(mod_type, mod);
|
|
target->remove_children("effect");
|
|
}
|
|
|
|
std::vector<t_string> effects_description;
|
|
for(const config& effect : mod.child_range("effect")) {
|
|
if(target) {
|
|
//Store effects only after they are added to avoid double applying effects on advance with apply_to=variation.
|
|
target->add_child("effect", effect);
|
|
}
|
|
// Apply SUF.
|
|
if(auto afilter = effect.optional_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(no_add && (apply_to == "type" || apply_to == "variation")) {
|
|
continue;
|
|
}
|
|
|
|
if(effect["times"] == "per level") {
|
|
if(effect["apply_to"] == "level") {
|
|
WRN_UT << "[effect] times=per level is not allowed with apply_to=level, using default value of 1";
|
|
times = 1;
|
|
}
|
|
else {
|
|
times = level_;
|
|
}
|
|
}
|
|
|
|
if(times) {
|
|
while (times > 0) {
|
|
times --;
|
|
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;
|
|
}
|
|
} // 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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
trait_nonhidden_ids_.push_back(trait["id"]);
|
|
}
|
|
}
|
|
|
|
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");
|
|
if(modifications_.has_child("advance")) {
|
|
deprecated_message("[advance]", DEP_LEVEL::PREEMPTIVE, {1, 15, 0}, "Use [advancement] instead.");
|
|
}
|
|
for (const config::any_child mod : modifications_.all_children_range()) {
|
|
add_modification(mod.key, mod.cfg, 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();
|
|
}
|
|
|
|
// 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_;
|
|
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.";
|
|
}
|
|
|
|
u_.set_movement(moves_);
|
|
} catch(...) {
|
|
DBG_UT << "Caught exception when destroying unit_movement_resetter: " << utils::get_unknown_exception_type();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// TODO: this should really hide the halo, not destroy it
|
|
// 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_ = halo;
|
|
}
|
|
|
|
void unit::parse_upkeep(const config::attribute_value& upkeep)
|
|
{
|
|
if(upkeep.empty()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
upkeep_ = upkeep.apply_visitor(upkeep_parser_visitor{});
|
|
} catch(std::invalid_argument& e) {
|
|
WRN_UT << "Found invalid upkeep=\"" << e.what() << "\" in a unit";
|
|
upkeep_ = upkeep_full{};
|
|
}
|
|
}
|
|
|
|
void unit::write_upkeep(config::attribute_value& upkeep) const
|
|
{
|
|
upkeep = utils::visit(upkeep_type_visitor{}, upkeep_);
|
|
}
|
|
|
|
void unit::clear_changed_attributes()
|
|
{
|
|
changed_attributes_.reset();
|
|
for(const auto& a_ptr : attacks_) {
|
|
a_ptr->set_changed(false);
|
|
}
|
|
}
|
|
|
|
std::vector<t_string> unit::unit_special_notes() const {
|
|
return combine_special_notes(special_notes_, abilities(), attacks(), movement_type());
|
|
}
|
|
|
|
// Filters unimportant stats from the unit config and returns a checksum of
|
|
// the remaining config.
|
|
std::string get_checksum(const unit& u, backwards_compatibility::unit_checksum_version version)
|
|
{
|
|
config unit_config;
|
|
config wcfg;
|
|
u.write(unit_config);
|
|
|
|
static const std::set<std::string_view> 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(const std::string_view& main_key : main_keys) {
|
|
wcfg[main_key] = unit_config[main_key];
|
|
}
|
|
|
|
static const std::set<std::string_view> attack_keys {
|
|
"name",
|
|
"type",
|
|
"range",
|
|
"damage",
|
|
"number"
|
|
};
|
|
|
|
for(const config& att : unit_config.child_range("attack")) {
|
|
config& child = wcfg.add_child("attack");
|
|
|
|
for(const std::string_view& attack_key : attack_keys) {
|
|
child[attack_key] = att[attack_key];
|
|
}
|
|
|
|
for(const config& spec : att.child_range("specials")) {
|
|
config& child_spec = child.add_child("specials", spec);
|
|
|
|
child_spec.recursive_clear_value("description");
|
|
if(version != backwards_compatibility::unit_checksum_version::version_1_16_or_older) {
|
|
child_spec.recursive_clear_value("description_inactive");
|
|
child_spec.recursive_clear_value("name");
|
|
child_spec.recursive_clear_value("name_inactive");
|
|
}
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
static const std::set<std::string_view> child_keys {
|
|
"advance_from",
|
|
"defense",
|
|
"movement_costs",
|
|
"vision_costs",
|
|
"jamming_costs",
|
|
"resistance"
|
|
};
|
|
|
|
for(const std::string_view& child_key : child_keys) {
|
|
for(const config& c : unit_config.child_range(child_key)) {
|
|
wcfg.add_child(child_key, c);
|
|
}
|
|
}
|
|
|
|
DBG_UT << wcfg;
|
|
|
|
return wcfg.hash();
|
|
}
|