diff --git a/src/game_constants.h b/src/game_constants.h index 8ce5143e54505..2dd4c2ba40b67 100644 --- a/src/game_constants.h +++ b/src/game_constants.h @@ -1,236 +1,176 @@ #pragma once -#ifndef CATA_SRC_GAME_CONSTANTS_H -#define CATA_SRC_GAME_CONSTANTS_H +#ifndef GAME_CONSTANTS_H +#define GAME_CONSTANTS_H -#include -#include #include "calendar.h" #include "units.h" -// Fixed window sizes. -constexpr int EVEN_MINIMUM_TERM_WIDTH = 80; -constexpr int EVEN_MINIMUM_TERM_HEIGHT = 24; -constexpr int HP_HEIGHT = 14; -constexpr int HP_WIDTH = 7; -constexpr int MINIMAP_HEIGHT = 7; -constexpr int MINIMAP_WIDTH = MINIMAP_HEIGHT; -constexpr int MONINFO_HEIGHT = 12; -constexpr int MONINFO_WIDTH = 48; -constexpr int MESSAGES_HEIGHT = 8; -constexpr int MESSAGES_WIDTH = 48; -constexpr int LOCATION_HEIGHT = 1; -constexpr int LOCATION_WIDTH = 48; -constexpr int STATUS_HEIGHT = 4; -constexpr int STATUS_WIDTH = 55; - -constexpr int EXPLOSION_MULTIPLIER = 7; - -// Really just a sanity check for functions not tested beyond this. in theory 4096 works (`InvletInvlet). -constexpr int MAX_ITEM_IN_SQUARE = 4096; -// no reason to differ. -constexpr int MAX_ITEM_IN_VEHICLE_STORAGE = MAX_ITEM_IN_SQUARE; -// Sanity checks for volume -constexpr units::volume DEFAULT_TILE_VOLUME = units::from_liter( 1000 ); -constexpr units::volume MAX_ITEM_VOLUME = DEFAULT_TILE_VOLUME; -// only can wear a maximum of two of any type of clothing. -constexpr int MAX_WORN_PER_TYPE = 2; - -constexpr int MAPSIZE = 11; -constexpr int HALF_MAPSIZE = static_cast( MAPSIZE / 2 ); +// Fixed window sizes +#define HP_HEIGHT 14 +#define HP_WIDTH 7 +#define MINIMAP_HEIGHT 7 +#define MINIMAP_WIDTH 7 +#define MONINFO_HEIGHT 12 +#define MONINFO_WIDTH 48 +#define MESSAGES_HEIGHT 8 +#define MESSAGES_WIDTH 48 +#define LOCATION_HEIGHT 1 +#define LOCATION_WIDTH 48 +#define STATUS_HEIGHT 4 +#define STATUS_WIDTH 55 + +#define BLINK_SPEED 300 +#define EXPLOSION_MULTIPLIER 7 + +// Really just a sanity check for functions not tested beyond this. in theory 4096 works (`InvletInvlet) +#define MAX_ITEM_IN_SQUARE 4096 +// no reason to differ +#define MAX_ITEM_IN_VEHICLE_STORAGE MAX_ITEM_IN_SQUARE +// only can wear a maximum of two of any type of clothing +#define MAX_WORN_PER_TYPE 2 + +#define MAPSIZE 11 +#define HALF_MAPSIZE static_cast( MAPSIZE / 2 ) // SEEX/SEEY define the size of a nonant, or grid. // All map segments will need to be at least this wide. -constexpr int SEEX = 12; -constexpr int SEEY = SEEX; +#define SEEX 12 +#define SEEY SEEX -constexpr int MAPSIZE_X = SEEX * MAPSIZE; -constexpr int MAPSIZE_Y = SEEY * MAPSIZE; +#define MAPSIZE_X (SEEX * MAPSIZE) +#define MAPSIZE_Y (SEEY * MAPSIZE) -constexpr int HALF_MAPSIZE_X = SEEX * HALF_MAPSIZE; -constexpr int HALF_MAPSIZE_Y = SEEY * HALF_MAPSIZE; +#define HALF_MAPSIZE_X (SEEX * HALF_MAPSIZE) +#define HALF_MAPSIZE_Y (SEEY * HALF_MAPSIZE) -constexpr int MAX_VIEW_DISTANCE = SEEX * HALF_MAPSIZE; +#define MAX_VIEW_DISTANCE ( SEEX * HALF_MAPSIZE ) -/** - * Size of the overmap. This is the number of overmap terrain tiles per dimension in one overmap, - * it's just like SEEX/SEEY for submaps. -*/ -constexpr int OMAPX = 180; -constexpr int OMAPY = OMAPX; +// Size of the overmap. This is the number of overmap terrain tiles per dimension in one overmap, +// it's just like SEEX/SEEY for submaps. +#define OMAPX 180 +#define OMAPY 180 // Size of a square unit of terrain saved to a directory. -constexpr int SEG_SIZE = 32; - -// Size of a square unit of tile memory saved in a single file, in mm_submaps. -constexpr int MM_REG_SIZE = 8; - -/** - * Items on the map with at most this distance to the player are considered available for crafting, - * see inventory::form_from_map -*/ -static constexpr int PICKUP_RANGE = 10; - -// Number of z-levels below 0 (not including 0). -constexpr int OVERMAP_DEPTH = 10; -// Number of z-levels above 0 (not including 0). -constexpr int OVERMAP_HEIGHT = 10; -// Total number of z-levels. -constexpr int OVERMAP_LAYERS = 1 + OVERMAP_DEPTH + OVERMAP_HEIGHT; - -// Maximum move cost when handling an item. -constexpr int MAX_HANDLING_COST = 400; -// Move cost of accessing an item in inventory. -constexpr int INVENTORY_HANDLING_PENALTY = 100; -// Move cost of accessing an item lying on the map. TODO: Less if player is crouching. -constexpr int MAP_HANDLING_PENALTY = 80; -// Move cost of accessing an item lying on a vehicle. -constexpr int VEHICLE_HANDLING_PENALTY = 80; - -// Amount by which to charge an item for each unit of plutonium cell. -static constexpr int PLUTONIUM_CHARGES = 50000; - -// Temperature constants. +#define SEG_SIZE 32 + +// Items on the map with at most this distance to the player are considered available for crafting, +// see inventory::form_from_map +#define PICKUP_RANGE 10 + +/** Number of z-levels below 0 (not including 0). */ +#define OVERMAP_DEPTH 10 +/** Number of z-levels above 0 (not including 0). */ +#define OVERMAP_HEIGHT 10 +/** Total number of z-levels */ +#define OVERMAP_LAYERS (1 + OVERMAP_DEPTH + OVERMAP_HEIGHT) + +/** Maximum move cost when handling an item */ +#define MAX_HANDLING_COST 400 +/** Move cost of accessing an item in inventory. */ +#define INVENTORY_HANDLING_PENALTY 100 +/** Move cost of accessing an item lying on the map. TODO: Less if player is crouching */ +#define MAP_HANDLING_PENALTY 80 +/** Move cost of accessing an item lying on a vehicle. */ +#define VEHICLE_HANDLING_PENALTY 80 + +/** Amount by which to charge an item for each unit of plutonium cell */ +#define PLUTONIUM_CHARGES 500 + +/** Temperature constants */ namespace temperatures { -// temperature at which something starts is considered HOT. -constexpr units::temperature hot = units::from_fahrenheit( 100 ); // ~ 38 Celsius +/** temperature at which something starts is considered HOT */ +constexpr int hot = 100; // ~ 38 Celsius -// the "normal" temperature midpoint between cold and hot. -constexpr units::temperature normal = units::from_fahrenheit( 70 ); // ~ 21 Celsius +/** the "normal" temperature midpoint between cold and hot */ +constexpr int normal = 70; // ~ 21 Celsius -// Temperature inside an active fridge in Fahrenheit. -constexpr units::temperature fridge = units::from_fahrenheit( 37 ); // ~ 2.7 Celsius +/** Temperature inside an active fridge in Fahrenheit */ +constexpr int fridge = 37; // ~ 2.7 Celsius -// Temperature at which things are considered "cold". -constexpr units::temperature cold = units::from_fahrenheit( 40 ); // ~4.4 C +/** Temperature at which things are considered "cold" */ +constexpr int cold = 40; // ~4.4 C -// Temperature inside an active freezer in Fahrenheit. -constexpr units::temperature freezer = units::from_celsius( -5 ); // -5 Celsius +/** Temperature inside an active freezer in Fahrenheit */ +constexpr int freezer = 23; // -5 Celsius -// Temperature in which water freezes. -constexpr units::temperature freezing = units::from_celsius( 0 ); // 0 Celsius - -// Temperature in which water boils. -constexpr units::temperature boiling = units::from_celsius( 100 ); // 100 Celsius +/** Temperature in which water freezes in Fahrenheit */ +constexpr int freezing = 32; // 0 Celsius } // namespace temperatures -// Slowest speed at which a gun can be aimed. -constexpr int MAX_AIM_COST = 10; +// Shelf life of corpse. This should be kept same as raw flesh. +constexpr time_duration CORPSE_ROT_TIME = 24_hours; + +/** Weight per level of LIFT/JACK tool quality */ +#define TOOL_LIFT_FACTOR 500_kilogram // 500kg/level + +/** Cap JACK requirements to support arbitrarily large vehicles */ +#define JACK_LIMIT 8500_kilogram // 8500kg ( 8.5 metric tonnes ) + +/** Slowest speed at which a gun can be aimed */ +#define MAX_AIM_COST 10 -// Maximum (effective) level for a skill. -static constexpr int MAX_SKILL = 20; +/** Maximum (effective) level for a skill */ +#define MAX_SKILL 20 -// Maximum (effective) level for a stat. -static constexpr int MAX_STAT = 30; +/** Maximum (effective) level for a stat */ +#define MAX_STAT 100 -// Maximum range at which ranged attacks can be executed. -constexpr int RANGE_HARD_CAP = 60; +/** Maximum range at which ranged attacks can be executed */ +#define RANGE_HARD_CAP 60 -// Accuracy levels which a shots tangent must be below. +/** Accuracy levels which a shots tangent must be below */ constexpr double accuracy_headshot = 0.1; constexpr double accuracy_critical = 0.2; constexpr double accuracy_goodhit = 0.5; constexpr double accuracy_standard = 0.8; constexpr double accuracy_grazing = 1.0; -// The maximum level recoil will ever reach. -// This corresponds to the level of accuracy of a "snap" or "hip" shot. -constexpr double MAX_RECOIL = 3000; +/** Minimum item damage output of relevant type to allow using with relevant weapon skill */ +#define MELEE_STAT 5 -// Minimum item damage output of relevant type to allow using with relevant weapon skill. -constexpr int MELEE_STAT = 5; +/** Effective lower bound to combat skill levels when CQB bionic is active */ +#define BIO_CQB_LEVEL 5 -// Effective lower bound to combat skill levels when CQB bionic is active. -constexpr int BIO_CQB_LEVEL = 5; +/** Minimum size of a horde to show up on the minimap. */ +#define HORDE_VISIBILITY_SIZE 3 -// Minimum size of a horde to show up on the minimap. -constexpr int HORDE_VISIBILITY_SIZE = 3; +/** Average annual temperature in F used for climate, weather and temperature calculation */ +/** Average New England temperature = 43F/6C rounded to int */ +#define AVERAGE_ANNUAL_TEMPERATURE 43 -// How often a NPC can move one tile on the overmap -constexpr time_duration time_between_npc_OM_moves = 5_minutes; +/** Base starting spring temperature in F used for climate, weather and temperature calculation */ +/** New England base spring temperature = 65F/18C rounded to int */ +#define SPRING_TEMPERATURE 65 -/** - * Average annual temperature in Kelvin used for climate, weather and temperature calculation. - * Average New England temperature = 43F/6C rounded to int. -*/ -constexpr units::temperature AVERAGE_ANNUAL_TEMPERATURE = units::from_fahrenheit( 43 ); - -/** - * Base starting spring temperature in Kelvin used for climate, weather and temperature calculation. - * New England base spring temperature = 65F/18C rounded to int. -*/ -constexpr units::temperature SPRING_TEMPERATURE = units::from_fahrenheit( 65 ); - -/** - * Used to limit the random seed during noise calculation. A large value flattens the noise generator to zero. - * Windows has a rand limit of 32768, other operating systems can have higher limits. -*/ +/** Used to limit the random seed during noise calculation. A large value flattens the noise generator to zero. + Windows has a rand limit of 32768, other operating systems can have higher limits. */ constexpr int SIMPLEX_NOISE_RANDOM_SEED_LIMIT = 32768; -constexpr float MIN_MANIPULATOR_SCORE = 0.1f; -// the maximum penalty to movecost from a limb value -constexpr float MAX_MOVECOST_MODIFIER = 100.0f; - -/** - * activity levels, used for BMR. - * these levels are normally used over the length of - * days to weeks in order to calculate your total BMR - * but we are making it more granular to be able to have - * variable activity levels. - * as such, when determining your activity level - * in the json, think about what it would be if you - * did this activity for a longer period of time. -*/ -constexpr float SLEEP_EXERCISE = 0.85f; -constexpr float NO_EXERCISE = 1.0f; -constexpr float LIGHT_EXERCISE = 2.0f; -constexpr float MODERATE_EXERCISE = 4.0f; -constexpr float BRISK_EXERCISE = 6.0f; -constexpr float ACTIVE_EXERCISE = 8.0f; -constexpr float EXTRA_EXERCISE = 10.0f; - -const std::map activity_levels_map = { - { "SLEEP_EXERCISE", SLEEP_EXERCISE }, - { "NO_EXERCISE", NO_EXERCISE }, - { "LIGHT_EXERCISE", LIGHT_EXERCISE }, - { "MODERATE_EXERCISE", MODERATE_EXERCISE }, - { "BRISK_EXERCISE", BRISK_EXERCISE }, - { "ACTIVE_EXERCISE", ACTIVE_EXERCISE }, - { "EXTRA_EXERCISE", EXTRA_EXERCISE } -}; - -const std::map activity_levels_str_map = { - { SLEEP_EXERCISE, "SLEEP_EXERCISE" }, - { NO_EXERCISE, "NO_EXERCISE" }, - { LIGHT_EXERCISE, "LIGHT_EXERCISE" }, - { MODERATE_EXERCISE, "MODERATE_EXERCISE" }, - { BRISK_EXERCISE, "BRISK_EXERCISE" }, - { ACTIVE_EXERCISE, "ACTIVE_EXERCISE" }, - { EXTRA_EXERCISE, "EXTRA_EXERCISE" } -}; - -// these are the lower bounds of each of the weight classes, determined by the amount of BMI coming from stored calories (fat) +// activity levels, used for BMR +// these levels are normally used over the length of +// days to weeks in order to calculate your total BMR +// but we are making it more granular to be able to have +// variable activity levels. +// as such, when determining your activity level +// in the json, think about what it would be if you +// did this activity for a longer period of time. +constexpr float NO_EXERCISE = 1.2f; +constexpr float LIGHT_EXERCISE = 1.375f; +constexpr float MODERATE_EXERCISE = 1.55f; +constexpr float ACTIVE_EXERCISE = 1.725f; +constexpr float EXTRA_EXERCISE = 1.9f; + +// these are the lower bounds of each of the weight classes namespace character_weight_category { -constexpr float emaciated = 1.0f; -constexpr float underweight = 2.0f; -constexpr float normal = 3.0f; -constexpr float overweight = 5.0f; -constexpr float obese = 10.0f; -constexpr float very_obese = 15.0f; -constexpr float morbidly_obese = 20.0f; +constexpr float emaciated = 14.0f; +constexpr float underweight = 16.0f; +constexpr float normal = 18.5f; +constexpr float overweight = 25.0f; +constexpr float obese = 30.0f; +constexpr float very_obese = 35.0f; +constexpr float morbidly_obese = 40.0f; } // namespace character_weight_category -// these are the lower bounds of each of the health classes. -namespace character_health_category -{ -//horrible -constexpr int very_bad = -100; -constexpr int bad = -50; -constexpr int fine = -10; -constexpr int good = 10; -constexpr int very_good = 50; -constexpr int great = 100; -} // namespace character_health_category - -#endif // CATA_SRC_GAME_CONSTANTS_H +#endif diff --git a/src/magic.cpp b/src/magic.cpp index 666c9e79ab8e8..b60f2644a205f 100644 --- a/src/magic.cpp +++ b/src/magic.cpp @@ -1,244 +1,118 @@ #include "magic.h" +#include +#include #include -#include -#include +#include #include -#include +#include #include +#include #include "avatar.h" -#include "bodypart.h" #include "calendar.h" -#include "cata_utility.h" -#include "catacharset.h" -#include "character.h" #include "color.h" -#include "condition.h" -#include "creature.h" -#include "creature_tracker.h" -#include "cursesdef.h" #include "damage.h" -#include "debug.h" -#include "enum_conversions.h" -#include "enums.h" -#include "event.h" -#include "event_bus.h" #include "field.h" +#include "game.h" #include "generic_factory.h" -#include "input_context.h" #include "inventory.h" -#include "item.h" #include "json.h" -#include "line.h" -#include "localized_comparator.h" -#include "magic_enchantment.h" #include "map.h" -#include "map_iterator.h" #include "messages.h" -#include "mongroup.h" #include "monster.h" -#include "mtype.h" #include "mutation.h" -#include "npc.h" #include "output.h" -#include "pimpl.h" -#include "point.h" -#include "projectile.h" -#include "requirements.h" -#include "rng.h" +#include "player.h" #include "sounds.h" -#include "string_formatter.h" #include "translations.h" #include "ui.h" -#include "units.h" - -static const ammo_effect_str_id ammo_effect_MAGIC( "MAGIC" ); - -static const json_character_flag json_flag_NO_PSIONICS( "NO_PSIONICS" ); -static const json_character_flag json_flag_NO_SPELLCASTING( "NO_SPELLCASTING" ); -static const json_character_flag json_flag_SILENT_SPELL( "SILENT_SPELL" ); -static const json_character_flag json_flag_SUBTLE_SPELL( "SUBTLE_SPELL" ); - -static const proficiency_id proficiency_prof_concentration_basic( "prof_concentration_basic" ); -static const proficiency_id -proficiency_prof_concentration_intermediate( "prof_concentration_intermediate" ); -static const proficiency_id proficiency_prof_concentration_master( "prof_concentration_master" ); - -static const skill_id skill_spellcraft( "spellcraft" ); - -static const trait_id trait_NONE( "NONE" ); - -static std::string target_to_string( spell_target data ) -{ - switch( data ) { - case spell_target::ally: - return pgettext( "Valid spell target", "ally" ); - case spell_target::hostile: - return pgettext( "Valid spell target", "hostile" ); - case spell_target::self: - return pgettext( "Valid spell target", "self" ); - case spell_target::ground: - return pgettext( "Valid spell target", "ground" ); - case spell_target::none: - return pgettext( "Valid spell target", "none" ); - case spell_target::item: - return pgettext( "Valid spell target", "item" ); - case spell_target::field: - return pgettext( "Valid spell target", "field" ); - case spell_target::num_spell_targets: - break; - } - debugmsg( "Invalid valid_target" ); - return "THIS IS A BUG"; -} +#include "cata_utility.h" +#include "character.h" +#include "compatibility.h" +#include "creature.h" +#include "cursesdef.h" +#include "debug.h" +#include "enums.h" +#include "input.h" +#include "item.h" +#include "pldata.h" +#include "point.h" +#include "string_formatter.h" +#include "line.h" namespace io { // *INDENT-OFF* template<> -std::string enum_to_string( spell_target data ) +std::string enum_to_string( valid_target data ) { switch( data ) { - case spell_target::ally: return "ally"; - case spell_target::hostile: return "hostile"; - case spell_target::self: return "self"; - case spell_target::ground: return "ground"; - case spell_target::none: return "none"; - case spell_target::item: return "item"; - case spell_target::field: return "field"; - case spell_target::num_spell_targets: break; + case valid_target::target_ally: return "ally"; + case valid_target::target_hostile: return "hostile"; + case valid_target::target_self: return "self"; + case valid_target::target_ground: return "ground"; + case valid_target::target_none: return "none"; + case valid_target::target_item: return "item"; + case valid_target::target_fd_fire: return "fd_fire"; + case valid_target::target_fd_blood: return "fd_blood"; + case valid_target::_LAST: break; } - cata_fatal( "Invalid valid_target" ); + debugmsg( "Invalid valid_target" ); + abort(); } template<> -std::string enum_to_string( spell_shape data ) +std::string enum_to_string( body_part data ) { switch( data ) { - case spell_shape::blast: return "blast"; - case spell_shape::cone: return "cone"; - case spell_shape::line: return "line"; - case spell_shape::num_shapes: break; - } - cata_fatal( "Invalid spell_shape" ); + case body_part::bp_torso: return "TORSO"; + case body_part::bp_head: return "HEAD"; + case body_part::bp_eyes: return "EYES"; + case body_part::bp_mouth: return "MOUTH"; + case body_part::bp_arm_l: return "ARM_L"; + case body_part::bp_arm_r: return "ARM_R"; + case body_part::bp_hand_l: return "HAND_L"; + case body_part::bp_hand_r: return "HAND_R"; + case body_part::bp_leg_l: return "LEG_L"; + case body_part::bp_leg_r: return "LEG_R"; + case body_part::bp_foot_l: return "FOOT_L"; + case body_part::bp_foot_r: return "FOOT_R"; + case body_part::num_bp: break; + } + debugmsg( "Invalid body_part" ); + abort(); } template<> std::string enum_to_string( spell_flag data ) { switch( data ) { case spell_flag::PERMANENT: return "PERMANENT"; - case spell_flag::PERMANENT_ALL_LEVELS: return "PERMANENT_ALL_LEVELS"; - case spell_flag::PERCENTAGE_DAMAGE: return "PERCENTAGE_DAMAGE"; case spell_flag::IGNORE_WALLS: return "IGNORE_WALLS"; - case spell_flag::NO_PROJECTILE: return "NO_PROJECTILE"; case spell_flag::HOSTILE_SUMMON: return "HOSTILE_SUMMON"; case spell_flag::HOSTILE_50: return "HOSTILE_50"; - case spell_flag::FRIENDLY_POLY: return "FRIENDLY_POLY"; - case spell_flag::POLYMORPH_GROUP: return "POLYMORPH_GROUP"; case spell_flag::SILENT: return "SILENT"; - case spell_flag::NO_EXPLOSION_SFX: return "NO_EXPLOSION_SFX"; case spell_flag::LOUD: return "LOUD"; case spell_flag::VERBAL: return "VERBAL"; case spell_flag::SOMATIC: return "SOMATIC"; case spell_flag::NO_HANDS: return "NO_HANDS"; case spell_flag::NO_LEGS: return "NO_LEGS"; case spell_flag::UNSAFE_TELEPORT: return "UNSAFE_TELEPORT"; - case spell_flag::TARGET_TELEPORT: return "TARGET_TELEPORT"; - case spell_flag::SWAP_POS: return "SWAP_POS"; case spell_flag::CONCENTRATE: return "CONCENTRATE"; case spell_flag::RANDOM_AOE: return "RANDOM_AOE"; case spell_flag::RANDOM_DAMAGE: return "RANDOM_DAMAGE"; case spell_flag::RANDOM_DURATION: return "RANDOM_DURATION"; case spell_flag::RANDOM_TARGET: return "RANDOM_TARGET"; - case spell_flag::RANDOM_CRITTER: return "RANDOM_CRITTER"; case spell_flag::MUTATE_TRAIT: return "MUTATE_TRAIT"; - case spell_flag::PAIN_NORESIST: return "PAIN_NORESIST"; - case spell_flag::SPAWN_GROUP: return "SPAWN_GROUP"; - case spell_flag::IGNITE_FLAMMABLE: return "IGNITE_FLAMMABLE"; - case spell_flag::NO_FAIL: return "NO_FAIL"; case spell_flag::WONDER: return "WONDER"; - case spell_flag::EXTRA_EFFECTS_FIRST: return "EXTRA_EFFECTS_FIRST"; - case spell_flag::MUST_HAVE_CLASS_TO_LEARN: return "MUST_HAVE_CLASS_TO_LEARN"; - case spell_flag::SPAWN_WITH_DEATH_DROPS: return "SPAWN_WITH_DEATH_DROPS"; - case spell_flag::NO_CORPSE_QUIET: return "NO_CORPSE_QUIET"; - case spell_flag::NON_MAGICAL: return "NON_MAGICAL"; - case spell_flag::PSIONIC: return "PSIONIC"; - case spell_flag::RECHARM: return "RECHARM"; case spell_flag::LAST: break; } - cata_fatal( "Invalid spell_flag" ); -} -template<> -std::string enum_to_string( magic_energy_type data ) -{ - switch( data ) { - case magic_energy_type::bionic: return "BIONIC"; - case magic_energy_type::hp: return "HP"; - case magic_energy_type::mana: return "MANA"; - case magic_energy_type::none: return "NONE"; - case magic_energy_type::stamina: return "STAMINA"; - case magic_energy_type::last: break; - } - cata_fatal( "Invalid magic_energy_type" ); + debugmsg( "Invalid spell_flag" ); + abort(); } // *INDENT-ON* } // namespace io -const std::optional fake_spell::max_level_default = std::nullopt; -const int fake_spell::level_default = 0; -const bool fake_spell::self_default = false; -const int fake_spell::trigger_once_in_default = 1; - -const skill_id spell_type::skill_default = skill_spellcraft; -// empty string -const requirement_id spell_type::spell_components_default; -const translation spell_type::message_default = to_translation( "You cast %s!" ); -const translation spell_type::sound_description_default = to_translation( "an explosion." ); -const sounds::sound_t spell_type::sound_type_default = sounds::sound_t::combat; -const bool spell_type::sound_ambient_default = false; -// empty string -const std::string spell_type::sound_id_default; -const std::string spell_type::sound_variant_default = "default"; -// empty string -const std::string spell_type::effect_str_default; -const std::optional spell_type::field_default = std::nullopt; -const int spell_type::field_chance_default = 1; -const int spell_type::min_field_intensity_default = 0; -const int spell_type::max_field_intensity_default = 0; -const float spell_type::field_intensity_increment_default = 0.0f; -const float spell_type::field_intensity_variance_default = 0.0f; -const int spell_type::min_accuracy_default = 20; -const float spell_type::accuracy_increment_default = 0.0f; -const int spell_type::max_accuracy_default = 20; -const int spell_type::min_damage_default = 0; -const float spell_type::damage_increment_default = 0.0f; -const int spell_type::max_damage_default = 0; -const int spell_type::min_range_default = 0; -const float spell_type::range_increment_default = 0.0f; -const int spell_type::max_range_default = 0; -const int spell_type::min_aoe_default = 0; -const float spell_type::aoe_increment_default = 0.0f; -const int spell_type::max_aoe_default = 0; -const int spell_type::min_dot_default = 0; -const float spell_type::dot_increment_default = 0.0f; -const int spell_type::max_dot_default = 0; -const int spell_type::min_duration_default = 0; -const int spell_type::duration_increment_default = 0; -const int spell_type::max_duration_default = 0; -const int spell_type::min_pierce_default = 0; -const float spell_type::pierce_increment_default = 0.0f; -const int spell_type::max_pierce_default = 0; -const int spell_type::base_energy_cost_default = 0; -const float spell_type::energy_increment_default = 0.0f; -const trait_id spell_type::spell_class_default = trait_NONE; -const magic_energy_type spell_type::energy_source_default = magic_energy_type::none; -const damage_type_id spell_type::dmg_type_default = damage_type_id::NULL_ID(); -const int spell_type::difficulty_default = 0; -const int spell_type::max_level_default = 0; -const int spell_type::base_casting_time_default = 0; -const float spell_type::casting_time_increment_default = 0.0f; - // LOADING // spell_type @@ -259,344 +133,197 @@ bool string_id::is_valid() const return spell_factory.is_valid( *this ); } -void spell_type::load_spell( const JsonObject &jo, const std::string &src ) +void spell_type::load_spell( JsonObject &jo, const std::string &src ) { spell_factory.load( jo, src ); } -static std::string moves_to_string( const int moves ) -{ - if( moves < to_moves( 2_seconds ) ) { - return string_format( n_gettext( "%d move", "%d moves", moves ), moves ); +static energy_type energy_source_from_string( const std::string &str ) +{ + if( str == "MANA" ) { + return mana_energy; + } else if( str == "HP" ) { + return hp_energy; + } else if( str == "BIONIC" ) { + return bionic_energy; + } else if( str == "STAMINA" ) { + return stamina_energy; + } else if( str == "FATIGUE" ) { + return fatigue_energy; + } else if( str == "NONE" ) { + return none_energy; + } else { + debugmsg( _( "ERROR: Invalid energy string. Defaulting to NONE" ) ); + return none_energy; + } +} + +static damage_type damage_type_from_string( const std::string &str ) +{ + if( str == "fire" ) { + return DT_HEAT; + } else if( str == "acid" ) { + return DT_ACID; + } else if( str == "bash" ) { + return DT_BASH; + } else if( str == "bio" ) { + return DT_BIOLOGICAL; + } else if( str == "cold" ) { + return DT_COLD; + } else if( str == "cut" ) { + return DT_CUT; + } else if( str == "electric" ) { + return DT_ELECTRIC; + } else if( str == "stab" ) { + return DT_STAB; + } else if( str == "none" || str == "NONE" ) { + return DT_TRUE; } else { - return to_string( time_duration::from_moves( moves ) ); + debugmsg( _( "ERROR: Invalid damage type string. Defaulting to none" ) ); + return DT_TRUE; } } -void spell_type::load( const JsonObject &jo, const std::string_view src ) +static std::string moves_to_string( const int moves ) { - src_mod = mod_id( src ); + if( moves < to_moves( 2_seconds ) ) { + return string_format( _( "%d moves" ), moves ); + } else { + return to_string( time_duration::from_turns( moves / 100 ) ); + } +} + +void spell_type::load( JsonObject &jo, const std::string & ) +{ + static const + std::map> + effect_map{ + { "pain_split", spell_effect::pain_split }, + { "target_attack", spell_effect::target_attack }, + { "projectile_attack", spell_effect::projectile_attack }, + { "cone_attack", spell_effect::cone_attack }, + { "line_attack", spell_effect::line_attack }, + { "teleport_random", spell_effect::teleport_random }, + { "spawn_item", spell_effect::spawn_ethereal_item }, + { "recover_energy", spell_effect::recover_energy }, + { "summon", spell_effect::spawn_summoned_monster }, + { "translocate", spell_effect::translocate }, + { "area_pull", spell_effect::area_pull }, + { "area_push", spell_effect::area_push }, + { "timed_event", spell_effect::timed_event }, + { "ter_transform", spell_effect::transform_blast }, + { "noise", spell_effect::noise }, + { "vomit", spell_effect::vomit }, + { "explosion", spell_effect::explosion }, + { "flashbang", spell_effect::flashbang }, + { "mod_moves", spell_effect::mod_moves }, + { "map", spell_effect::map }, + { "morale", spell_effect::morale }, + { "charm_monster", spell_effect::charm_monster }, + { "mutate", spell_effect::mutate }, + { "bash", spell_effect::bash }, + { "none", spell_effect::none } + }; + + mandatory( jo, was_loaded, "id", id ); mandatory( jo, was_loaded, "name", name ); mandatory( jo, was_loaded, "description", description ); - optional( jo, was_loaded, "skill", skill, skill_default ); - optional( jo, was_loaded, "teachable", teachable, true ); - optional( jo, was_loaded, "components", spell_components, spell_components_default ); - optional( jo, was_loaded, "message", message, message_default ); - optional( jo, was_loaded, "sound_description", sound_description, sound_description_default ); - optional( jo, was_loaded, "sound_type", sound_type, sound_type_default ); - optional( jo, was_loaded, "sound_ambient", sound_ambient, sound_ambient_default ); - optional( jo, was_loaded, "sound_id", sound_id, sound_id_default ); - optional( jo, was_loaded, "sound_variant", sound_variant, sound_variant_default ); + optional( jo, was_loaded, "message", message, to_translation( "You cast %s!" ) ); + optional( jo, was_loaded, "sound_description", sound_description, + to_translation( "an explosion" ) ); + optional( jo, was_loaded, "sound_type", sound_type, sounds::sound_t::combat ); + optional( jo, was_loaded, "sound_ambient", sound_ambient, false ); + optional( jo, was_loaded, "sound_id", sound_id, "" ); + optional( jo, was_loaded, "sound_variant", sound_variant, "default" ); mandatory( jo, was_loaded, "effect", effect_name ); - const auto found_effect = spell_effect::effect_map.find( effect_name ); - if( found_effect == spell_effect::effect_map.cend() ) { + const auto found_effect = effect_map.find( effect_name ); + if( found_effect == effect_map.cend() ) { effect = spell_effect::none; debugmsg( "ERROR: spell %s has invalid effect %s", id.c_str(), effect_name ); } else { effect = found_effect->second; } - mandatory( jo, was_loaded, "shape", spell_area ); - spell_area_function = spell_effect::shape_map.at( spell_area ); - - const auto targeted_monster_ids_reader = string_id_reader<::mtype> {}; - optional( jo, was_loaded, "targeted_monster_ids", targeted_monster_ids, - targeted_monster_ids_reader ); - const auto targeted_monster_species_reader = string_id_reader<::species_type> {}; - optional( jo, was_loaded, "targeted_monster_species", targeted_species_ids, - targeted_monster_species_reader ); + const auto effect_targets_reader = enum_flags_reader { "effect_targets" }; + optional( jo, was_loaded, "effect_filter", effect_targets, effect_targets_reader ); - const auto ignored_monster_species_reader = string_id_reader<::species_type> {}; - optional( jo, was_loaded, "ignored_monster_species", ignored_species_ids, - ignored_monster_species_reader ); - - - - - const auto trigger_reader = enum_flags_reader { "valid_targets" }; + const auto trigger_reader = enum_flags_reader { "valid_targets" }; mandatory( jo, was_loaded, "valid_targets", valid_targets, trigger_reader ); - optional( jo, was_loaded, "extra_effects", additional_spells ); - - optional( jo, was_loaded, "affected_body_parts", affected_bps ); - - if( jo.has_array( "flags" ) ) { - for( auto &flag : jo.get_string_array( "flags" ) ) { - // Save all provided flags as strings in spell_type.flags - // If the flag is listed as a possible enum of type spell_flag, we also save it to spell_type.spell_tags - flags.insert( flag ); - std::optional f = io::string_to_enum_optional( flag ); - if( f.has_value() ) { - spell_tags.set( f.value() ); - } - } - } else if( jo.has_string( "flags" ) ) { - const std::string flag = jo.get_string( "flags" ); - flags.insert( flag ); - std::optional f = io::string_to_enum_optional( flag ); - if( f.has_value() ) { - spell_tags.set( f.value() ); + if( jo.has_array( "extra_effects" ) ) { + JsonArray jarray = jo.get_array( "extra_effects" ); + while( jarray.has_more() ) { + fake_spell temp; + JsonObject fake_spell_obj = jarray.next_object(); + temp.load( fake_spell_obj ); + additional_spells.emplace_back( temp ); } } - optional( jo, was_loaded, "effect_str", effect_str, effect_str_default ); + const auto bp_reader = enum_flags_reader { "affected_bps" }; + optional( jo, was_loaded, "affected_body_parts", affected_bps, bp_reader ); + const auto flag_reader = enum_flags_reader { "flags" }; + optional( jo, was_loaded, "flags", spell_tags, flag_reader ); + + optional( jo, was_loaded, "effect_str", effect_str, "" ); std::string field_input; - // Because the field this is loading into is not part of this type, - // the default value will not be supplied when using copy-from if we pass was_loaded - // So just pass false instead - optional( jo, false, "field_id", field_input, "none" ); + optional( jo, was_loaded, "field_id", field_input, "none" ); if( field_input != "none" ) { field = field_type_id( field_input ); } - if( !was_loaded || jo.has_member( "field_chance" ) ) { - field_chance = get_dbl_or_var( jo, "field_chance", false, field_chance_default ); - } - if( !was_loaded || jo.has_member( "min_field_intensity" ) ) { - min_field_intensity = get_dbl_or_var( jo, "min_field_intensity", false, - min_field_intensity_default ); - } - if( !was_loaded || jo.has_member( "max_field_intensity" ) ) { - max_field_intensity = get_dbl_or_var( jo, "max_field_intensity", false, - max_field_intensity_default ); - } - if( !was_loaded || jo.has_member( "field_intensity_increment" ) ) { - field_intensity_increment = get_dbl_or_var( jo, "field_intensity_increment", false, - field_intensity_increment_default ); - } - if( !was_loaded || jo.has_member( "field_intensity_variance" ) ) { - field_intensity_variance = get_dbl_or_var( jo, "field_intensity_variance", false, - field_intensity_variance_default ); - } - - if( !was_loaded || jo.has_member( "min_accuracy" ) ) { - min_accuracy = get_dbl_or_var( jo, "min_accuracy", false, min_accuracy_default ); - } - if( !was_loaded || jo.has_member( "accuracy_increment" ) ) { - accuracy_increment = get_dbl_or_var( jo, "accuracy_increment", false, - accuracy_increment_default ); - } - if( !was_loaded || jo.has_member( "max_accuracy" ) ) { - max_accuracy = get_dbl_or_var( jo, "max_accuracy", false, max_accuracy_default ); - } - if( !was_loaded || jo.has_member( "min_damage" ) ) { - min_damage = get_dbl_or_var( jo, "min_damage", false, min_damage_default ); - } - if( !was_loaded || jo.has_member( "damage_increment" ) ) { - damage_increment = get_dbl_or_var( jo, "damage_increment", false, - damage_increment_default ); - } - if( !was_loaded || jo.has_member( "max_damage" ) ) { - max_damage = get_dbl_or_var( jo, "max_damage", false, max_damage_default ); - } + optional( jo, was_loaded, "field_chance", field_chance, 1 ); + optional( jo, was_loaded, "min_field_intensity", min_field_intensity, 0 ); + optional( jo, was_loaded, "max_field_intensity", max_field_intensity, 0 ); + optional( jo, was_loaded, "field_intensity_increment", field_intensity_increment, 0.0f ); + optional( jo, was_loaded, "field_intensity_variance", field_intensity_variance, 0.0f ); - if( !was_loaded || jo.has_member( "min_range" ) ) { - min_range = get_dbl_or_var( jo, "min_range", false, min_range_default ); - } - if( !was_loaded || jo.has_member( "range_increment" ) ) { - range_increment = get_dbl_or_var( jo, "range_increment", false, range_increment_default ); - } - if( !was_loaded || jo.has_member( "max_range" ) ) { - max_range = get_dbl_or_var( jo, "max_range", false, max_range_default ); - } - - if( !was_loaded || jo.has_member( "min_aoe" ) ) { - min_aoe = get_dbl_or_var( jo, "min_aoe", false, min_aoe_default ); - } - if( !was_loaded || jo.has_member( "aoe_increment" ) ) { - aoe_increment = get_dbl_or_var( jo, "aoe_increment", false, aoe_increment_default ); - } - if( !was_loaded || jo.has_member( "max_aoe" ) ) { - max_aoe = get_dbl_or_var( jo, "max_aoe", false, max_aoe_default ); - } - if( !was_loaded || jo.has_member( "min_dot" ) ) { - min_dot = get_dbl_or_var( jo, "min_dot", false, min_dot_default ); - } - if( !was_loaded || jo.has_member( "dot_increment" ) ) { - dot_increment = get_dbl_or_var( jo, "dot_increment", false, dot_increment_default ); - } - if( !was_loaded || jo.has_member( "max_dot" ) ) { - max_dot = get_dbl_or_var( jo, "max_dot", false, max_dot_default ); - } + optional( jo, was_loaded, "min_damage", min_damage, 0 ); + optional( jo, was_loaded, "damage_increment", damage_increment, 0.0f ); + optional( jo, was_loaded, "max_damage", max_damage, 0 ); - if( !was_loaded || jo.has_member( "min_duration" ) ) { - min_duration = get_dbl_or_var( jo, "min_duration", false, min_duration_default ); - } - if( !was_loaded || jo.has_member( "duration_increment" ) ) { - duration_increment = get_dbl_or_var( jo, "duration_increment", false, - duration_increment_default ); - } - if( !was_loaded || jo.has_member( "max_duration" ) ) { - max_duration = get_dbl_or_var( jo, "max_duration", false, max_duration_default ); - } + optional( jo, was_loaded, "min_range", min_range, 0 ); + optional( jo, was_loaded, "range_increment", range_increment, 0.0f ); + optional( jo, was_loaded, "max_range", max_range, 0 ); - if( !was_loaded || jo.has_member( "min_pierce" ) ) { - min_pierce = get_dbl_or_var( jo, "min_pierce", false, min_pierce_default ); - } - if( !was_loaded || jo.has_member( "pierce_increment" ) ) { - pierce_increment = get_dbl_or_var( jo, "pierce_increment", false, - pierce_increment_default ); - } - if( !was_loaded || jo.has_member( "max_pierce" ) ) { - max_pierce = get_dbl_or_var( jo, "max_pierce", false, max_pierce_default ); - } + optional( jo, was_loaded, "min_aoe", min_aoe, 0 ); + optional( jo, was_loaded, "aoe_increment", aoe_increment, 0.0f ); + optional( jo, was_loaded, "max_aoe", max_aoe, 0 ); - if( !was_loaded || jo.has_member( "base_energy_cost" ) ) { - base_energy_cost = get_dbl_or_var( jo, "base_energy_cost", false, - base_energy_cost_default ); - } - if( jo.has_member( "final_energy_cost" ) ) { - final_energy_cost = get_dbl_or_var( jo, "final_energy_cost" ); - } else if( !was_loaded ) { - final_energy_cost = base_energy_cost; - } - if( !was_loaded || jo.has_member( "energy_increment" ) ) { - energy_increment = get_dbl_or_var( jo, "energy_increment", false, - energy_increment_default ); - } + optional( jo, was_loaded, "min_dot", min_dot, 0 ); + optional( jo, was_loaded, "dot_increment", dot_increment, 0.0f ); + optional( jo, was_loaded, "max_dot", max_dot, 0 ); - optional( jo, was_loaded, "spell_class", spell_class, spell_class_default ); - optional( jo, was_loaded, "energy_source", energy_source, energy_source_default ); - optional( jo, was_loaded, "damage_type", dmg_type, dmg_type_default ); - if( !was_loaded || jo.has_member( "difficulty" ) ) { - difficulty = get_dbl_or_var( jo, "difficulty", false, difficulty_default ); - } - if( !was_loaded || jo.has_member( "max_level" ) ) { - max_level = get_dbl_or_var( jo, "max_level", false, max_level_default ); - } + optional( jo, was_loaded, "min_duration", min_duration, 0 ); + optional( jo, was_loaded, "duration_increment", duration_increment, 0.0f ); + optional( jo, was_loaded, "max_duration", max_duration, 0 ); - if( !was_loaded || jo.has_member( "base_casting_time" ) ) { - base_casting_time = get_dbl_or_var( jo, "base_casting_time", false, - base_casting_time_default ); - } - if( jo.has_member( "final_casting_time" ) ) { - final_casting_time = get_dbl_or_var( jo, "final_casting_time" ); - } else if( !was_loaded ) { - final_casting_time = base_casting_time; - } - if( !was_loaded || jo.has_member( "max_damage" ) ) { - max_damage = get_dbl_or_var( jo, "max_damage", false, max_damage_default ); - } - if( !was_loaded || jo.has_member( "casting_time_increment" ) ) { - casting_time_increment = get_dbl_or_var( jo, "casting_time_increment", false, - casting_time_increment_default ); - } + optional( jo, was_loaded, "min_pierce", min_pierce, 0 ); + optional( jo, was_loaded, "pierce_increment", pierce_increment, 0.0f ); + optional( jo, was_loaded, "max_pierce", max_pierce, 0 ); - for( const JsonMember member : jo.get_object( "learn_spells" ) ) { - learn_spells.insert( std::pair( member.name(), member.get_int() ) ); - } -} + optional( jo, was_loaded, "base_energy_cost", base_energy_cost, 0 ); + optional( jo, was_loaded, "final_energy_cost", final_energy_cost, base_energy_cost ); + optional( jo, was_loaded, "energy_increment", energy_increment, 0.0f ); -void spell_type::serialize( JsonOut &json ) const -{ - json.start_object(); + std::string temp_string; + optional( jo, was_loaded, "spell_class", temp_string, "NONE" ); + spell_class = trait_id( temp_string ); + optional( jo, was_loaded, "energy_source", temp_string, "NONE" ); + energy_source = energy_source_from_string( temp_string ); + optional( jo, was_loaded, "damage_type", temp_string, "NONE" ); + dmg_type = damage_type_from_string( temp_string ); + optional( jo, was_loaded, "difficulty", difficulty, 0 ); + optional( jo, was_loaded, "max_level", max_level, 0 ); - json.member( "type", "SPELL" ); - json.member( "id", id ); - json.member( "src_mod", src_mod ); - json.member( "name", name.translated() ); - json.member( "description", description.translated() ); - json.member( "effect", effect_name ); - json.member( "shape", io::enum_to_string( spell_area ) ); - json.member( "valid_targets", valid_targets, enum_bitset {} ); - json.member( "effect_str", effect_str, effect_str_default ); - json.member( "skill", skill, skill_default ); - json.member( "teachable", teachable, true ); - json.member( "components", spell_components, spell_components_default ); - json.member( "message", message.translated(), message_default.translated() ); - json.member( "sound_description", sound_description.translated(), - sound_description_default.translated() ); - json.member( "sound_type", io::enum_to_string( sound_type ), - io::enum_to_string( sound_type_default ) ); - json.member( "sound_ambient", sound_ambient, sound_ambient_default ); - json.member( "sound_id", sound_id, sound_id_default ); - json.member( "sound_variant", sound_variant, sound_variant_default ); - json.member( "targeted_monster_ids", targeted_monster_ids, std::set {} ); - json.member( "targeted_monster_species", targeted_species_ids, std::set {} ); - json.member( "ignored_monster_species", ignored_species_ids, std::set {} ); - json.member( "extra_effects", additional_spells, std::vector {} ); - if( !affected_bps.none() ) { - json.member( "affected_body_parts", affected_bps ); - } - json.member( "flags", flags, std::set {} ); - if( field ) { - json.member( "field_id", field->id().str() ); - json.member( "field_chance", static_cast( field_chance.min.dbl_val.value() ), - field_chance_default ); - json.member( "max_field_intensity", static_cast( max_field_intensity.min.dbl_val.value() ), - max_field_intensity_default ); - json.member( "min_field_intensity", static_cast( min_field_intensity.min.dbl_val.value() ), - min_field_intensity_default ); - json.member( "field_intensity_increment", - static_cast( field_intensity_increment.min.dbl_val.value() ), - field_intensity_increment_default ); - json.member( "field_intensity_variance", - static_cast( field_intensity_variance.min.dbl_val.value() ), - field_intensity_variance_default ); - } - json.member( "min_damage", static_cast( min_damage.min.dbl_val.value() ), min_damage_default ); - json.member( "max_damage", static_cast( max_damage.min.dbl_val.value() ), max_damage_default ); - json.member( "damage_increment", static_cast( damage_increment.min.dbl_val.value() ), - damage_increment_default ); - json.member( "min_accuracy", static_cast( min_accuracy.min.dbl_val.value() ), - min_accuracy_default ); - json.member( "accuracy_increment", static_cast( accuracy_increment.min.dbl_val.value() ), - accuracy_increment_default ); - json.member( "max_accuracy", static_cast( max_accuracy.min.dbl_val.value() ), - max_accuracy_default ); - json.member( "min_range", static_cast( min_range.min.dbl_val.value() ), min_range_default ); - json.member( "max_range", static_cast( max_range.min.dbl_val.value() ), min_range_default ); - json.member( "range_increment", static_cast( range_increment.min.dbl_val.value() ), - range_increment_default ); - json.member( "min_aoe", static_cast( min_aoe.min.dbl_val.value() ), min_aoe_default ); - json.member( "max_aoe", static_cast( max_aoe.min.dbl_val.value() ), max_aoe_default ); - json.member( "aoe_increment", static_cast( aoe_increment.min.dbl_val.value() ), - aoe_increment_default ); - json.member( "min_dot", static_cast( min_dot.min.dbl_val.value() ), min_dot_default ); - json.member( "max_dot", static_cast( max_dot.min.dbl_val.value() ), max_dot_default ); - json.member( "dot_increment", static_cast( dot_increment.min.dbl_val.value() ), - dot_increment_default ); - json.member( "min_duration", static_cast( min_duration.min.dbl_val.value() ), - min_duration_default ); - json.member( "max_duration", static_cast( max_duration.min.dbl_val.value() ), - max_duration_default ); - json.member( "duration_increment", static_cast( duration_increment.min.dbl_val.value() ), - duration_increment_default ); - json.member( "min_pierce", static_cast( min_pierce.min.dbl_val.value() ), min_pierce_default ); - json.member( "max_pierce", static_cast( max_pierce.min.dbl_val.value() ), max_pierce_default ); - json.member( "pierce_increment", static_cast( pierce_increment.min.dbl_val.value() ), - pierce_increment_default ); - json.member( "base_energy_cost", static_cast( base_energy_cost.min.dbl_val.value() ), - base_energy_cost_default ); - json.member( "final_energy_cost", static_cast( final_energy_cost.min.dbl_val.value() ), - static_cast( base_energy_cost.min.dbl_val.value() ) ); - json.member( "energy_increment", static_cast( energy_increment.min.dbl_val.value() ), - energy_increment_default ); - json.member( "spell_class", spell_class, spell_class_default ); - json.member( "energy_source", io::enum_to_string( energy_source ), - io::enum_to_string( energy_source_default ) ); - json.member( "damage_type", dmg_type, dmg_type_default ); - json.member( "difficulty", static_cast( difficulty.min.dbl_val.value() ), difficulty_default ); - json.member( "max_level", static_cast( max_level.min.dbl_val.value() ), max_level_default ); - json.member( "base_casting_time", static_cast( base_casting_time.min.dbl_val.value() ), - base_casting_time_default ); - json.member( "final_casting_time", static_cast( final_casting_time.min.dbl_val.value() ), - static_cast( base_casting_time.min.dbl_val.value() ) ); - json.member( "casting_time_increment", - static_cast( casting_time_increment.min.dbl_val.value() ), casting_time_increment_default ); - - if( !learn_spells.empty() ) { - json.member( "learn_spells" ); - json.start_object(); + optional( jo, was_loaded, "base_casting_time", base_casting_time, 0 ); + optional( jo, was_loaded, "final_casting_time", final_casting_time, base_casting_time ); + optional( jo, was_loaded, "casting_time_increment", casting_time_increment, 0.0f ); - for( const std::pair &sp : learn_spells ) { - json.member( sp.first, sp.second ); - } - - json.end_object(); + JsonObject learning = jo.get_object( "learn_spells" ); + for( std::string n : learning.get_member_names() ) { + learn_spells.insert( std::pair( n, learning.get_int( n ) ) ); } - - json.end_object(); } static bool spell_infinite_loop_check( std::set spell_effects, const spell_id &sp ) @@ -611,8 +338,8 @@ static bool spell_infinite_loop_check( std::set spell_effects, const s unique_spell_list.emplace( fake_sp.id ); } - for( const spell_id &other_sp : unique_spell_list ) { - if( spell_infinite_loop_check( spell_effects, other_sp ) ) { + for( const spell_id &sp : unique_spell_list ) { + if( spell_infinite_loop_check( spell_effects, sp ) ) { return true; } } @@ -622,16 +349,55 @@ static bool spell_infinite_loop_check( std::set spell_effects, const s void spell_type::check_consistency() { for( const spell_type &sp_t : get_all() ) { - if( sp_t.effect_name == "summon_vehicle" ) { - if( !sp_t.effect_str.empty() && !vproto_id( sp_t.effect_str ).is_valid() ) { - debugmsg( "ERROR %s specifies a vehicle to summon, but vehicle %s is not valid", sp_t.id.c_str(), - sp_t.effect_str ); - } + if( ( sp_t.min_aoe > sp_t.max_aoe && sp_t.aoe_increment > 0 ) || + ( sp_t.min_aoe < sp_t.max_aoe && sp_t.aoe_increment < 0 ) ) { + debugmsg( "ERROR: %s has higher min_aoe than max_aoe", sp_t.id.c_str() ); + } + if( ( sp_t.min_damage > sp_t.max_damage && sp_t.damage_increment > 0 ) || + ( sp_t.min_damage < sp_t.max_damage && sp_t.damage_increment < 0 ) ) { + debugmsg( "ERROR: %s has higher min_damage than max_damage", sp_t.id.c_str() ); + } + if( ( sp_t.min_range > sp_t.max_range && sp_t.range_increment > 0 ) || + ( sp_t.min_range < sp_t.max_range && sp_t.range_increment < 0 ) ) { + debugmsg( "ERROR: %s has higher min_range than max_range", sp_t.id.c_str() ); + } + if( ( sp_t.min_dot > sp_t.max_dot && sp_t.dot_increment > 0 ) || + ( sp_t.min_dot < sp_t.max_dot && sp_t.dot_increment < 0 ) ) { + debugmsg( "ERROR: %s has higher min_dot than max_dot", sp_t.id.c_str() ); + } + if( ( sp_t.min_duration > sp_t.max_duration && sp_t.duration_increment > 0 ) || + ( sp_t.min_duration < sp_t.max_duration && sp_t.duration_increment < 0 ) ) { + debugmsg( "ERROR: %s has higher min_dot_time than max_dot_time", sp_t.id.c_str() ); + } + if( ( sp_t.min_pierce > sp_t.max_pierce && sp_t.pierce_increment > 0 ) || + ( sp_t.min_pierce < sp_t.max_pierce && sp_t.pierce_increment < 0 ) ) { + debugmsg( "ERROR: %s has higher min_pierce than max_pierce", sp_t.id.c_str() ); + } + if( sp_t.casting_time_increment < 0.0f && sp_t.base_casting_time < sp_t.final_casting_time ) { + debugmsg( "ERROR: %s has negative increment and base_casting_time < final_casting_time", + sp_t.id.c_str() ); + } + if( sp_t.casting_time_increment > 0.0f && sp_t.base_casting_time > sp_t.final_casting_time ) { + debugmsg( "ERROR: %s has positive increment and base_casting_time > final_casting_time", + sp_t.id.c_str() ); } std::set spell_effect_list; if( spell_infinite_loop_check( spell_effect_list, sp_t.id ) ) { debugmsg( "ERROR: %s has infinite loop in extra_effects", sp_t.id.c_str() ); } + if( sp_t.field ) { + if( sp_t.field_chance <= 0 ) { + debugmsg( "ERROR: %s must have a positive field chance.", sp_t.id.c_str() ); + } + if( sp_t.field_intensity_increment > 0 && sp_t.max_field_intensity < sp_t.min_field_intensity ) { + debugmsg( "ERROR: max_field_intensity must be greater than min_field_intensity with positive increment: %s", + sp_t.id.c_str() ); + } else if( sp_t.field_intensity_increment < 0 && + sp_t.max_field_intensity > sp_t.min_field_intensity ) { + debugmsg( "ERROR: min_field_intensity must be greater than max_field_intensity with negative increment: %s", + sp_t.id.c_str() ); + } + } if( sp_t.spell_tags[spell_flag::WONDER] && sp_t.additional_spells.empty() ) { debugmsg( "ERROR: %s has WONDER flag but no spells to choose from!", sp_t.id.c_str() ); } @@ -655,16 +421,15 @@ bool spell_type::is_valid() const // spell -spell::spell( spell_id sp, int xp, int level_adjustment ) : +spell::spell( spell_id sp, int xp ) : type( sp ), - experience( xp ), - temp_level_adjustment( level_adjustment ) + experience( xp ) {} -void spell::set_message( const translation &msg ) -{ - alt_message = msg; -} +spell::spell( spell_id sp, translation alt_msg ) : + type( sp ), + alt_message( alt_msg ) +{} spell_id spell::id() const { @@ -676,406 +441,168 @@ trait_id spell::spell_class() const return type->spell_class; } -skill_id spell::skill() const +int spell::field_intensity() const { - return type->skill; + return std::min( type->max_field_intensity, + static_cast( type->min_field_intensity + round( get_level() * + type->field_intensity_increment ) ) ); } -int spell::field_intensity( const Creature &caster ) const +int spell::min_leveled_damage() const { - dialogue d( get_talker_for( caster ), nullptr ); - return std::min( static_cast( type->max_field_intensity.evaluate( d ) ), - static_cast( type->min_field_intensity.evaluate( d ) + std::round( get_effective_level() * - type->field_intensity_increment.evaluate( d ) ) ) ); -} - -int spell::min_leveled_damage( const Creature &caster ) const -{ - dialogue d( get_talker_for( caster ), nullptr ); - return type->min_damage.evaluate( d ) + std::round( get_effective_level() * - type->damage_increment.evaluate( - d ) ); -} - -float spell::dps( const Character &caster, const Creature & ) const -{ - if( type->effect_name != "attack" ) { - return 0.0f; - } - const float time_modifier = 100.0f / casting_time( caster ); - const float failure_modifier = 1.0f - spell_fail( caster ); - const float raw_dps = damage( caster ) + damage_dot( caster ) * duration_turns( caster ) / 1_turns; - // TODO: calculate true dps with armor and resistances and any caster bonuses - return raw_dps * time_modifier * failure_modifier; + return type->min_damage + round( get_level() * type->damage_increment ); } -int spell::damage( const Creature &caster ) const +int spell::damage() const { - dialogue d( get_talker_for( caster ), nullptr ); - const int leveled_damage = min_leveled_damage( caster ); + const int leveled_damage = min_leveled_damage(); if( has_flag( spell_flag::RANDOM_DAMAGE ) ) { - return rng( std::min( leveled_damage, static_cast( type->max_damage.evaluate( d ) ) ), - std::max( leveled_damage, - static_cast( type->max_damage.evaluate( d ) ) ) ); + return rng( std::min( leveled_damage, type->max_damage ), std::max( leveled_damage, + type->max_damage ) ); } else { - if( type->min_damage.evaluate( d ) >= 0 || - type->max_damage.evaluate( d ) >= type->min_damage.evaluate( d ) ) { - return std::min( leveled_damage, static_cast( type->max_damage.evaluate( d ) ) ); + if( type->min_damage >= 0 || type->max_damage >= type->min_damage ) { + return std::min( leveled_damage, type->max_damage ); } else { // if it's negative, min and max work differently - return std::max( leveled_damage, static_cast( type->max_damage.evaluate( d ) ) ); + return std::max( leveled_damage, type->max_damage ); } } } -int spell::min_leveled_accuracy( const Creature &caster ) const +std::string spell::damage_string() const { - dialogue d( get_talker_for( caster ), nullptr ); - return type->min_accuracy.evaluate( d ) + std::round( get_effective_level() * - type->accuracy_increment.evaluate( d ) ); -} - -int spell::accuracy( Creature &caster ) const -{ - dialogue d( get_talker_for( caster ), nullptr ); - const int leveled_accuracy = min_leveled_accuracy( caster ); - if( type->min_accuracy.evaluate( d ) >= 0 || - type->max_accuracy.evaluate( d ) >= type->min_accuracy.evaluate( d ) ) { - return std::min( leveled_accuracy, static_cast( type->max_accuracy.evaluate( d ) ) ); - } else { // if it's negative, min and max work differently - return std::max( leveled_accuracy, static_cast( type->max_accuracy.evaluate( d ) ) ); - } -} - -int spell::min_leveled_dot( const Creature &caster ) const -{ - dialogue d( get_talker_for( caster ), nullptr ); - return type->min_dot.evaluate( d ) + std::round( get_effective_level() * - type->dot_increment.evaluate( d ) ); -} - -int spell::damage_dot( const Creature &caster ) const -{ - dialogue d( get_talker_for( caster ), nullptr ); - const int leveled_dot = min_leveled_dot( caster ); - if( type->min_dot.evaluate( d ) >= 0 || - type->max_dot.evaluate( d ) >= type->min_dot.evaluate( d ) ) { - return std::min( leveled_dot, static_cast( type->max_dot.evaluate( d ) ) ); - } else { // if it's negative, min and max work differently - return std::max( leveled_dot, static_cast( type->max_dot.evaluate( d ) ) ); - } -} - -damage_over_time_data spell::damage_over_time( const std::vector &bps, - const Creature &caster ) const -{ - damage_over_time_data temp; - temp.bps = bps; - temp.duration = duration_turns( caster ); - temp.amount = damage_dot( caster ); - temp.type = dmg_type(); - return temp; -} - -std::string spell::damage_string( const Character &caster ) const -{ - std::string damage_string; - dialogue d( get_talker_for( caster ), nullptr ); if( has_flag( spell_flag::RANDOM_DAMAGE ) ) { - damage_string = string_format( "%d-%d", min_leveled_damage( caster ), - static_cast( type->max_damage.evaluate( d ) ) ); + return string_format( "%d-%d", min_leveled_damage(), type->max_damage ); } else { - const int dmg = damage( caster ); + const int dmg = damage(); if( dmg >= 0 ) { - damage_string = string_format( "%d", dmg ); + return string_format( "%d", dmg ); } else { - damage_string = string_format( "+%d", std::abs( dmg ) ); - } - } - if( has_flag( spell_flag::PERCENTAGE_DAMAGE ) ) { - damage_string = string_format( "%s%% %s", damage_string, _( "of current HP" ) ); - } - return damage_string; -} - -std::optional spell::select_target( Creature *source ) -{ - tripoint target = source->pos(); - bool target_is_valid = false; - if( range( *source ) > 0 && !is_valid_target( spell_target::none ) && - !has_flag( spell_flag::RANDOM_TARGET ) ) { - if( source->is_avatar() ) { - do { - avatar &source_avatar = *source->as_avatar(); - std::vector trajectory = target_handler::mode_spell( source_avatar, *this, - true, - true ); - if( !trajectory.empty() ) { - target = trajectory.back(); - target_is_valid = is_valid_target( source_avatar, target ); - if( !( is_valid_target( spell_target::ground ) || source_avatar.sees( target ) ) ) { - target_is_valid = false; - } - } else { - target_is_valid = false; - } - if( !target_is_valid ) { - if( query_yn( _( "Stop targeting? Time spent will be lost." ) ) ) { - return std::nullopt; - } - } - } while( !target_is_valid ); - } else if( source->is_npc() ) { - npc &source_npc = *source->as_npc(); - npc_attack_spell npc_spell( id() ); - // recalculate effectiveness because it's been a few turns since the npc started casting. - const npc_attack_rating effectiveness = npc_spell.evaluate( source_npc, - source_npc.last_target.lock().get() ); - if( effectiveness < 0 ) { - add_msg_debug( debugmode::debug_filter::DF_NPC, "%s cancels casting %s, target lost", - source_npc.disp_name(), name() ); - return std::nullopt; - } else { - target = effectiveness.target(); - } - } // TODO: move monster spell attack targeting here - } else if( has_flag( spell_flag::RANDOM_TARGET ) ) { - const std::optional target_ = random_valid_target( *source, source->pos() ); - if( !target_ ) { - source->add_msg_if_player( game_message_params{ m_bad, gmf_bypass_cooldown }, - _( "You can't find a suitable target." ) ); - return std::nullopt; + return string_format( "+%d", abs( dmg ) ); } - target = *target_; } - return target; } -int spell::min_leveled_aoe( const Creature &caster ) const +int spell::min_leveled_aoe() const { - dialogue d( get_talker_for( caster ), nullptr ); - return type->min_aoe.evaluate( d ) + std::round( get_effective_level() * - type->aoe_increment.evaluate( d ) ); + return type->min_aoe + round( get_level() * type->aoe_increment ); } -int spell::aoe( const Creature &caster ) const +int spell::aoe() const { - dialogue d( get_talker_for( caster ), nullptr ); - const int leveled_aoe = min_leveled_aoe( caster ); - int return_value; + const int leveled_aoe = min_leveled_aoe(); if( has_flag( spell_flag::RANDOM_AOE ) ) { - return_value = rng( std::min( leveled_aoe, static_cast( type->max_aoe.evaluate( d ) ) ), - std::max( leveled_aoe, static_cast( type->max_aoe.evaluate( d ) ) ) ); + return rng( std::min( leveled_aoe, type->max_aoe ), std::max( leveled_aoe, type->max_aoe ) ); } else { - if( type->max_aoe.evaluate( d ) >= type->min_aoe.evaluate( d ) ) { - return_value = std::min( leveled_aoe, static_cast( type->max_aoe.evaluate( d ) ) ); + if( type->max_aoe >= type->min_aoe ) { + return std::min( leveled_aoe, type->max_aoe ); } else { - return_value = std::max( leveled_aoe, static_cast( type->max_aoe.evaluate( d ) ) ); + return std::max( leveled_aoe, type->max_aoe ); } } - return return_value * temp_aoe_multiplyer; } -std::set spell::effect_area( const spell_effect::override_parameters ¶ms, - const tripoint &source, const tripoint &target ) const +bool spell::in_aoe( const tripoint &source, const tripoint &target ) const { - return type->spell_area_function( params, source, target ); -} - -std::set spell::effect_area( const tripoint &source, const tripoint &target, - const Creature &caster ) const -{ - return effect_area( spell_effect::override_parameters( *this, caster ), source, target ); -} - -bool spell::in_aoe( const tripoint &source, const tripoint &target, const Creature &caster ) const -{ - dialogue d( get_talker_for( caster ), nullptr ); if( has_flag( spell_flag::RANDOM_AOE ) ) { - return rl_dist( source, target ) <= type->max_aoe.evaluate( d ); + return rl_dist( source, target ) <= type->max_aoe; } else { - return rl_dist( source, target ) <= aoe( caster ); + return rl_dist( source, target ) <= aoe(); } } -std::string spell::aoe_string( const Creature &caster ) const +std::string spell::aoe_string() const { - dialogue d( get_talker_for( caster ), nullptr ); if( has_flag( spell_flag::RANDOM_AOE ) ) { - return string_format( "%d-%d", min_leveled_aoe( caster ), type->max_aoe.evaluate( d ) ); + return string_format( "%d-%d", min_leveled_aoe(), type->max_aoe ); } else { - return string_format( "%d", aoe( caster ) ); + return string_format( "%d", aoe() ); } } -int spell::range( const Creature &caster ) const +int spell::range() const { - dialogue d( get_talker_for( caster ), nullptr ); - const int leveled_range = type->min_range.evaluate( d ) + std::round( get_effective_level() * - type->range_increment.evaluate( d ) ); - float range; - if( type->max_range.evaluate( d ) >= type->min_range.evaluate( d ) ) { - range = std::min( leveled_range, static_cast( type->max_range.evaluate( d ) ) ); + const int leveled_range = type->min_range + round( get_level() * type->range_increment ); + if( type->max_range >= type->min_range ) { + return std::min( leveled_range, type->max_range ); } else { - range = std::max( leveled_range, static_cast( type->max_range.evaluate( d ) ) ); + return std::max( leveled_range, type->max_range ); } - return std::max( range * temp_range_multiplyer, 0.0f ); } -std::vector spell::targetable_locations( const Character &source ) const +int spell::min_leveled_duration() const { - - const tripoint char_pos = source.pos(); - const bool select_ground = is_valid_target( spell_target::ground ); - const bool ignore_walls = has_flag( spell_flag::NO_PROJECTILE ); - map &here = get_map(); - - // TODO: put this in a namespace for reuse - const auto has_obstruction = [&]( const tripoint & at ) { - for( const tripoint &line_point : line_to( char_pos, at ) ) { - if( here.impassable( line_point ) ) { - return true; - } - } - return false; - }; - - std::vector selectable_targets; - for( const tripoint &query : here.points_in_radius( char_pos, range( source ) ) ) { - if( !ignore_walls && has_obstruction( query ) ) { - // it's blocked somewhere! - continue; - } - - if( !select_ground ) { - if( !source.sees( query ) ) { - // can't target a critter you can't see - continue; - } - } - - if( is_valid_target( source, query ) ) { - selectable_targets.push_back( query ); - } - } - return selectable_targets; + return type->min_duration + round( get_level() * type->duration_increment ); } -int spell::min_leveled_duration( const Creature &caster ) const +int spell::duration() const { - dialogue d( get_talker_for( caster ), nullptr ); - return type->min_duration.evaluate( d ) + std::round( get_effective_level() * - type->duration_increment.evaluate( d ) ); -} - -int spell::duration( const Creature &caster ) const -{ - dialogue d( get_talker_for( caster ), nullptr ); - const int leveled_duration = min_leveled_duration( caster ); - float duration; + const int leveled_duration = min_leveled_duration(); if( has_flag( spell_flag::RANDOM_DURATION ) ) { - return rng( std::min( leveled_duration, static_cast( type->max_duration.evaluate( d ) ) ), - std::max( leveled_duration, - static_cast( type->max_duration.evaluate( d ) ) ) ); + return rng( std::min( leveled_duration, type->max_duration ), std::max( leveled_duration, + type->max_duration ) ); } else { - if( type->max_duration.evaluate( d ) >= type->min_duration.evaluate( d ) ) { - return std::min( leveled_duration, static_cast( type->max_duration.evaluate( d ) ) ); + if( type->max_duration >= type->min_duration ) { + return std::min( leveled_duration, type->max_duration ); } else { - return std::max( leveled_duration, static_cast( type->max_duration.evaluate( d ) ) ); + return std::max( leveled_duration, type->max_duration ); } } - return std::max( duration * temp_duration_multiplyer, 0.0f ); } -std::string spell::duration_string( const Creature &caster ) const +std::string spell::duration_string() const { - dialogue d( get_talker_for( caster ), nullptr ); if( has_flag( spell_flag::RANDOM_DURATION ) ) { - return string_format( "%s - %s", moves_to_string( min_leveled_duration( caster ) ), - moves_to_string( type->max_duration.evaluate( d ) ) ); - } else if( ( has_flag( spell_flag::PERMANENT ) && ( is_max_level( caster ) || - effect() == "summon" ) ) || - has_flag( spell_flag::PERMANENT_ALL_LEVELS ) ) { - return _( "Permanent" ); + return string_format( "%s - %s", moves_to_string( min_leveled_duration() ), + moves_to_string( type->max_duration ) ); } else { - return moves_to_string( duration( caster ) ); + return moves_to_string( duration() ); } } -time_duration spell::duration_turns( const Creature &caster ) const -{ - return time_duration::from_moves( duration( caster ) ); -} - -void spell::gain_level( const Character &guy ) +time_duration spell::duration_turns() const { - gain_exp( guy, exp_to_next_level() ); + return 1_turns * duration() / 100; } -void spell::gain_levels( const Character &guy, int gains ) +void spell::gain_level() { - if( gains < 1 ) { - return; - } - for( int gained = 0; gained < gains && !is_max_level( guy ); gained++ ) { - gain_level( guy ); - } + gain_exp( exp_to_next_level() ); } -void spell::set_level( const Character &guy, int nlevel ) +bool spell::is_max_level() const { - experience = 0; - gain_levels( guy, nlevel ); + return get_level() >= type->max_level; } -bool spell::is_max_level( const Creature &caster ) const +bool spell::can_learn( const player &p ) const { - dialogue d( get_talker_for( caster ), nullptr ); - return get_level() >= type->max_level.evaluate( d ); -} - -bool spell::can_learn( const Character &guy ) const -{ - if( type->spell_class == trait_NONE ) { + if( type->spell_class == trait_id( "NONE" ) ) { return true; } - return guy.has_trait( type->spell_class ); + return p.has_trait( type->spell_class ); } -int spell::energy_cost( const Character &guy ) const +int spell::energy_cost( const player &p ) const { int cost; - dialogue d( get_talker_for( guy ), nullptr ); - if( type->base_energy_cost.evaluate( d ) < type->final_energy_cost.evaluate( d ) ) { - cost = std::min( static_cast( type->final_energy_cost.evaluate( d ) ), - static_cast( std::round( type->base_energy_cost.evaluate( d ) + - type->energy_increment.evaluate( d ) * get_effective_level() ) ) ); - } else if( type->base_energy_cost.evaluate( d ) > type->final_energy_cost.evaluate( d ) ) { - cost = std::max( static_cast( type->final_energy_cost.evaluate( d ) ), - static_cast( std::round( type->base_energy_cost.evaluate( d ) + - type->energy_increment.evaluate( d ) * get_effective_level() ) ) ); + if( type->base_energy_cost < type->final_energy_cost ) { + cost = std::min( type->final_energy_cost, + static_cast( round( type->base_energy_cost + type->energy_increment * get_level() ) ) ); + } else if( type->base_energy_cost > type->final_energy_cost ) { + cost = std::max( type->final_energy_cost, + static_cast( round( type->base_energy_cost + type->energy_increment * get_level() ) ) ); } else { - cost = type->base_energy_cost.evaluate( d ); + cost = type->base_energy_cost; } - if( !no_hands() && !guy.has_flag( json_flag_SUBTLE_SPELL ) ) { + if( !has_flag( spell_flag::NO_HANDS ) ) { // the first 10 points of combined encumbrance is ignored, but quickly adds up - const int hands_encumb = std::max( 0, - guy.avg_encumb_of_limb_type( body_part_type::type::hand ) - 5 ); - switch( type->energy_source ) { - default: - cost += 10 * hands_encumb * temp_somatic_difficulty_multiplyer; - break; - case magic_energy_type::hp: - cost += hands_encumb * temp_somatic_difficulty_multiplyer; - break; - case magic_energy_type::stamina: - cost += 100 * hands_encumb * temp_somatic_difficulty_multiplyer; - break; - } + const int hands_encumb = std::max( 0, p.encumb( bp_hand_l ) + p.encumb( bp_hand_r ) - 10 ); + cost += 10 * hands_encumb; } - return std::max( cost * temp_spell_cost_multiplyer, 0.0f ); + return cost; } bool spell::has_flag( const spell_flag &flag ) const @@ -1083,140 +610,65 @@ bool spell::has_flag( const spell_flag &flag ) const return type->spell_tags[flag]; } -bool spell::has_flag( const std::string &flag ) const -{ - return type->flags.count( flag ) > 0; -} -bool spell::no_hands() const -{ - return ( has_flag( spell_flag::NO_HANDS ) || temp_somatic_difficulty_multiplyer <= 0 ); -} - bool spell::is_spell_class( const trait_id &mid ) const { return mid == type->spell_class; } -bool spell::can_cast( const Character &guy ) const +bool spell::can_cast( const player &p ) const { - if( has_flag( spell_flag::NON_MAGICAL ) ) { - return true; - }; - - if( guy.has_flag( json_flag_NO_SPELLCASTING ) && !has_flag( spell_flag::PSIONIC ) ) { - return false; - } - - if( guy.has_flag( json_flag_NO_PSIONICS ) && has_flag( spell_flag::PSIONIC ) ) { - return false; - } - - if( guy.is_mute() && !guy.has_flag( json_flag_SILENT_SPELL ) && has_flag( spell_flag::VERBAL ) ) { - return false; - } - - if( !type->spell_components.is_empty() && - !type->spell_components->can_make_with_inventory( guy.crafting_inventory( guy.pos(), 0, false ), - return_true ) ) { - return false; - } - - return guy.magic->has_enough_energy( guy, *this ); -} - -void spell::use_components( Character &guy ) const -{ - if( type->spell_components.is_empty() ) { - return; - } - const requirement_data &spell_components = type->spell_components.obj(); - // if we're here, we're assuming the Character has the correct components (using can_cast()) - inventory map_inv; - map_inv.form_from_map( guy.pos(), 0, &guy, true, false ); - for( const std::vector &comp_vec : spell_components.get_components() ) { - guy.consume_items( guy.select_item_component( comp_vec, 1, map_inv ), 1 ); - } - for( const std::vector &tool_vec : spell_components.get_tools() ) { - guy.consume_tools( guy.select_tool_component( tool_vec, 1, map_inv ), 1 ); - } -} - -bool spell::check_if_component_in_hand( Character &guy ) const -{ - if( type->spell_components.is_empty() ) { - return false; - } - - const requirement_data &spell_components = type->spell_components.obj(); - - if( guy.has_weapon() ) { - if( spell_components.can_make_with_inventory( *guy.get_wielded_item(), return_true ) ) { - return true; + switch( type->energy_source ) { + case mana_energy: + return p.magic.available_mana() >= energy_cost( p ); + case stamina_energy: + return p.get_stamina() >= energy_cost( p ); + case hp_energy: { + for( int i = 0; i < num_hp_parts; i++ ) { + if( energy_cost( p ) < p.hp_cur[i] ) { + return true; + } + } + return false; } + case bionic_energy: + return p.get_power_level() >= units::from_kilojoule( energy_cost( p ) ); + case fatigue_energy: + return p.get_fatigue() < EXHAUSTED; + case none_energy: + default: + return true; } - - // if it isn't in hand, return false - return false; } -int spell::get_difficulty( const Creature &caster ) const +int spell::get_difficulty() const { - dialogue d( get_talker_for( caster ), nullptr ); - return type->difficulty.evaluate( d ) + temp_difficulty_adjustment; + return type->difficulty; } -mod_id spell::get_src() const -{ - return type->src_mod; -} - -int spell::casting_time( const Character &guy, bool ignore_encumb ) const +int spell::casting_time( const player &p ) const { // casting time in moves int casting_time = 0; - dialogue d( get_talker_for( guy ), nullptr ); - if( type->base_casting_time.evaluate( d ) < type->final_casting_time.evaluate( d ) ) { - casting_time = std::min( static_cast( type->final_casting_time.evaluate( d ) ), - static_cast( std::round( type->base_casting_time.evaluate( d ) + - type->casting_time_increment.evaluate( d ) * - get_effective_level() ) ) ); - } else if( type->base_casting_time.evaluate( d ) > type->final_casting_time.evaluate( d ) ) { - casting_time = std::max( static_cast( type->final_casting_time.evaluate( d ) ), - static_cast( std::round( type->base_casting_time.evaluate( d ) + - type->casting_time_increment.evaluate( d ) * - get_effective_level() ) ) ); + if( type->base_casting_time < type->final_casting_time ) { + casting_time = std::min( type->final_casting_time, + static_cast( round( type->base_casting_time + type->casting_time_increment * get_level() ) ) ); + } else if( type->base_casting_time > type->final_casting_time ) { + casting_time = std::max( type->final_casting_time, + static_cast( round( type->base_casting_time + type->casting_time_increment * get_level() ) ) ); } else { - casting_time = type->base_casting_time.evaluate( d ); + casting_time = type->base_casting_time; } - - casting_time = guy.enchantment_cache->modify_value( enchant_vals::mod::CASTING_TIME_MULTIPLIER, - casting_time ); - - if( !ignore_encumb && temp_somatic_difficulty_multiplyer > 0 ) { - if( !has_flag( spell_flag::NO_LEGS ) ) { - // the first 20 points of encumbrance combined is ignored - const int legs_encumb = std::max( 0, - guy.avg_encumb_of_limb_type( body_part_type::type::leg ) - 10 ); - casting_time += legs_encumb * 3 * temp_somatic_difficulty_multiplyer; - } - if( has_flag( spell_flag::SOMATIC ) && !guy.has_flag( json_flag_SUBTLE_SPELL ) ) { - // the first 20 points of encumbrance combined is ignored - const int arms_encumb = std::max( 0, - guy.avg_encumb_of_limb_type( body_part_type::type::arm ) - 10 ); - casting_time += arms_encumb * 2 * temp_somatic_difficulty_multiplyer; - } + if( !has_flag( spell_flag::NO_LEGS ) ) { + // the first 20 points of encumbrance combined is ignored + const int legs_encumb = std::max( 0, p.encumb( bp_leg_l ) + p.encumb( bp_leg_r ) - 20 ); + casting_time += legs_encumb * 3; } - return std::max( casting_time * temp_cast_time_multiplyer, 0.0f ); -} - -const requirement_data &spell::components() const -{ - return type->spell_components.obj(); -} - -bool spell::has_components() const -{ - return !type->spell_components.is_empty(); + if( has_flag( spell_flag::SOMATIC ) ) { + // the first 20 points of encumbrance combined is ignored + const int arms_encumb = std::max( 0, p.encumb( bp_arm_l ) + p.encumb( bp_arm_r ) - 20 ); + casting_time += arms_encumb * 2; + } + return casting_time; } std::string spell::name() const @@ -1232,100 +684,46 @@ std::string spell::message() const return type->message.translated(); } -float spell::spell_fail( const Character &guy ) const +float spell::spell_fail( const player &p ) const { - if( has_flag( spell_flag::NO_FAIL ) ) { - return 0.0f; - } - const bool is_psi = has_flag( spell_flag::PSIONIC ); - // formula is based on the following: // exponential curve // effective skill of 0 or less is 100% failure // effective skill of 8 (8 int, 0 spellcraft, 0 spell level, spell difficulty 0) is ~50% failure // effective skill of 30 is 0% failure - const float effective_skill = 2 * ( get_effective_level() - get_difficulty( - guy ) ) + guy.get_int() + - guy.get_skill_level( skill() ); - - // skill for psi powers downplays power level and is much more based on level and intelligence - // and goes up to 40 max--effective skill of 10 is 50% failure, effective skill of 40 is 0% - // Int 8, Metaphysics 2, level 1, difficulty 1 is effective level 26.5 - // Int 10, Metaphysics 5, level 4, difficulty 5 is effective level 27 - // Int 12, Metaphysics 8, level 7, difficulty 10 is effective level 33.5 - const float two_thirds_power_level = static_cast( get_effective_level() ) / - static_cast - ( 1.5 ); - float psi_effective_skill = 0; - if( is_psi ) { - const float psi_effective_skill_initial = 2 * ( ( guy.get_skill_level( - skill() ) * 2 ) - get_difficulty( - guy ) ) + ( guy.get_int() * 1.5 ) + two_thirds_power_level; - - if( !guy.has_proficiency( proficiency_prof_concentration_basic ) ) { - psi_effective_skill = clamp( psi_effective_skill_initial, static_cast( 0 ), - static_cast( 24 ) ); - } else if( guy.has_proficiency( proficiency_prof_concentration_basic ) && - !guy.has_proficiency( proficiency_prof_concentration_intermediate ) ) { - psi_effective_skill = clamp( psi_effective_skill_initial, static_cast( 0 ), - static_cast( 31 ) ); - } else if( guy.has_proficiency( proficiency_prof_concentration_intermediate ) && - !guy.has_proficiency( proficiency_prof_concentration_master ) ) { - psi_effective_skill = clamp( psi_effective_skill_initial, static_cast( 0 ), - static_cast( 37 ) ); - } else { - psi_effective_skill = clamp( psi_effective_skill_initial, static_cast( 0 ), - static_cast( 45 ) ); - } - } + const float effective_skill = 2 * ( get_level() - get_difficulty() ) + p.get_int() + + p.get_skill_level( skill_id( "spellcraft" ) ); // add an if statement in here because sufficiently large numbers will definitely overflow because of exponents - if( ( effective_skill > 30.0f && !is_psi ) || ( psi_effective_skill > 40.0f && is_psi ) ) { + if( effective_skill > 30.0f ) { return 0.0f; - } else if( ( effective_skill < 0.0f && !is_psi ) || ( psi_effective_skill < 0.0f && is_psi ) ) { + } else if( effective_skill < 0.0f ) { return 1.0f; } - - float fail_chance = std::pow( ( effective_skill - 30.0f ) / 30.0f, 2 ); - float psi_fail_chance = std::pow( ( psi_effective_skill - 40.0f ) / 40.0f, 2 ); - - if( !is_psi ) { - if( has_flag( spell_flag::SOMATIC ) && - !guy.has_flag( json_flag_SUBTLE_SPELL ) && temp_somatic_difficulty_multiplyer > 0 ) { - // the first 20 points of encumbrance combined is ignored - const int arms_encumb = std::max( 0, - guy.avg_encumb_of_limb_type( body_part_type::type::arm ) - 10 ); - // each encumbrance point beyond the "gray" color counts as half an additional fail % - fail_chance += ( arms_encumb / 200.0f ) * temp_somatic_difficulty_multiplyer; - } - if( has_flag( spell_flag::VERBAL ) && - !guy.has_flag( json_flag_SILENT_SPELL ) && temp_sound_multiplyer > 0 ) { - // a little bit of mouth encumbrance is allowed, but not much - const int mouth_encumb = std::max( 0, - guy.avg_encumb_of_limb_type( body_part_type::type::mouth ) - 5 ); - fail_chance += ( mouth_encumb / 100.0f ) * temp_sound_multiplyer; - } + float fail_chance = pow( ( effective_skill - 30.0f ) / 30.0f, 2 ); + if( has_flag( spell_flag::SOMATIC ) ) { + // the first 20 points of encumbrance combined is ignored + const int arms_encumb = std::max( 0, p.encumb( bp_arm_l ) + p.encumb( bp_arm_r ) - 20 ); + // each encumbrance point beyond the "gray" color counts as half an additional fail % + fail_chance += arms_encumb / 200.0f; + } + if( has_flag( spell_flag::VERBAL ) ) { + // a little bit of mouth encumbrance is allowed, but not much + const int mouth_encumb = std::max( 0, p.encumb( bp_mouth ) - 5 ); + fail_chance += mouth_encumb / 100.0f; } - // concentration spells work better than you'd expect with a higher focus pool - if( has_flag( spell_flag::CONCENTRATE ) && temp_concentration_difficulty_multiplyer > 0 ) { - if( guy.get_focus() <= 0 ) { + if( has_flag( spell_flag::CONCENTRATE ) ) { + if( p.focus_pool <= 0 ) { return 0.0f; } - float concentration_loss = ( 1.0f - ( guy.get_focus() / 100.0f ) ) * - temp_concentration_difficulty_multiplyer; - if( concentration_loss >= 1.0f ) { - return 1.0f; - } - fail_chance /= 1.0f - concentration_loss; - psi_fail_chance /= 1.0f - concentration_loss; + fail_chance /= p.focus_pool / 100.0f; } - - return clamp( is_psi ? psi_fail_chance : fail_chance, 0.0f, 1.0f ); + return clamp( fail_chance, 0.0f, 1.0f ); } -std::string spell::colorized_fail_percent( const Character &guy ) const +std::string spell::colorized_fail_percent( const player &p ) const { - const float fail_fl = spell_fail( guy ) * 100.0f; + const float fail_fl = spell_fail( p ) * 100.0f; std::string fail_str; fail_fl == 100.0f ? fail_str = _( "Too Difficult!" ) : fail_str = string_format( "%.1f %% %s", fail_fl, _( "Failure Chance" ) ); @@ -1346,24 +744,14 @@ std::string spell::colorized_fail_percent( const Character &guy ) const return colorize( fail_str, color ); } -spell_shape spell::shape() const -{ - return type->spell_area; -} - int spell::xp() const { return experience; } -void spell::gain_exp( const Character &guy, int nxp ) +void spell::gain_exp( int nxp ) { - int oldLevel = get_level(); experience += nxp; - if( guy.is_avatar() && oldLevel != get_level() ) { - get_event_bus().send( guy.getID(), id(), get_level(), - spell_class() ); - } } void spell::set_exp( int nxp ) @@ -1374,57 +762,66 @@ void spell::set_exp( int nxp ) std::string spell::energy_string() const { switch( type->energy_source ) { - case magic_energy_type::hp: + case hp_energy: return _( "health" ); - case magic_energy_type::mana: + case mana_energy: return _( "mana" ); - case magic_energy_type::stamina: + case stamina_energy: return _( "stamina" ); - case magic_energy_type::bionic: - return _( "kJ" ); + case bionic_energy: + return _( "bionic power" ); + case fatigue_energy: + return _( "fatigue" ); default: return ""; } } -std::string spell::energy_cost_string( const Character &guy ) const +std::string spell::energy_cost_string( const player &p ) const { - if( energy_source() == magic_energy_type::none ) { + if( energy_source() == none_energy ) { return _( "none" ); } - if( energy_source() == magic_energy_type::bionic || energy_source() == magic_energy_type::mana ) { - return colorize( std::to_string( energy_cost( guy ) ), c_light_blue ); + if( energy_source() == bionic_energy || energy_source() == mana_energy ) { + return colorize( to_string( energy_cost( p ) ), c_light_blue ); } - if( energy_source() == magic_energy_type::hp ) { - auto pair = get_hp_bar( energy_cost( guy ), guy.get_hp_max() / 6 ); + if( energy_source() == hp_energy ) { + auto pair = get_hp_bar( energy_cost( p ), p.get_hp_max() / num_hp_parts ); return colorize( pair.first, pair.second ); } - if( energy_source() == magic_energy_type::stamina ) { - auto pair = get_hp_bar( energy_cost( guy ), guy.get_stamina_max() ); + if( energy_source() == stamina_energy ) { + auto pair = get_hp_bar( energy_cost( p ), p.get_stamina_max() ); return colorize( pair.first, pair.second ); } + if( energy_source() == fatigue_energy ) { + return colorize( to_string( energy_cost( p ) ), c_cyan ); + } debugmsg( "ERROR: Spell %s has invalid energy source.", id().c_str() ); return _( "error: energy_type" ); } -std::string spell::energy_cur_string( const Character &guy ) const +std::string spell::energy_cur_string( const player &p ) const { - if( energy_source() == magic_energy_type::none ) { + if( energy_source() == none_energy ) { return _( "infinite" ); } - if( energy_source() == magic_energy_type::bionic ) { - return colorize( std::to_string( units::to_kilojoule( guy.get_power_level() ) ), c_light_blue ); + if( energy_source() == bionic_energy ) { + return colorize( to_string( units::to_kilojoule( p.get_power_level() ) ), c_light_blue ); } - if( energy_source() == magic_energy_type::mana ) { - return colorize( std::to_string( guy.magic->available_mana() ), c_light_blue ); + if( energy_source() == mana_energy ) { + return colorize( to_string( p.magic.available_mana() ), c_light_blue ); } - if( energy_source() == magic_energy_type::stamina ) { - auto pair = get_hp_bar( guy.get_stamina(), guy.get_stamina_max() ); + if( energy_source() == stamina_energy ) { + auto pair = get_hp_bar( p.get_stamina(), p.get_stamina_max() ); return colorize( pair.first, pair.second ); } - if( energy_source() == magic_energy_type::hp ) { + if( energy_source() == hp_energy ) { return ""; } + if( energy_source() == fatigue_energy ) { + const std::pair pair = p.get_fatigue_description(); + return colorize( pair.first, pair.second ); + } debugmsg( "ERROR: Spell %s has invalid energy source.", id().c_str() ); return _( "error: energy_type" ); } @@ -1434,50 +831,38 @@ bool spell::is_valid() const return type.is_valid(); } -bool spell::bp_is_affected( const bodypart_str_id &bp ) const +bool spell::bp_is_affected( body_part bp ) const { - return type->affected_bps.test( bp ); + return type->affected_bps[bp]; } -void spell::create_field( const tripoint &at, Creature &caster ) const +void spell::create_field( const tripoint &at ) const { if( !type->field ) { return; } - dialogue d( get_talker_for( caster ), nullptr ); - const int intensity = field_intensity( caster ) + rng( -type->field_intensity_variance.evaluate( - d ) * field_intensity( caster ), - type->field_intensity_variance.evaluate( d ) * field_intensity( caster ) ); + const int intensity = field_intensity() + rng( -type->field_intensity_variance * field_intensity(), + type->field_intensity_variance * field_intensity() ); if( intensity <= 0 ) { return; } - if( one_in( type->field_chance.evaluate( d ) ) ) { - map &here = get_map(); - field_entry *field = here.get_field( at, *type->field ); + if( one_in( type->field_chance ) ) { + field_entry *field = g->m.get_field( at, *type->field ); if( field ) { field->set_field_intensity( field->get_field_intensity() + intensity ); } else { - here.add_field( at, *type->field, intensity, -duration_turns( caster ) ); + g->m.add_field( at, *type->field, intensity, -duration_turns() ); } } } -int spell::sound_volume( const Creature &caster ) const +void spell::make_sound( const tripoint &target ) const { - int loudness = 0; if( !has_flag( spell_flag::SILENT ) ) { - loudness = std::abs( damage( caster ) ) / 3; - if( has_flag( spell_flag::LOUD ) ) { - loudness += 1 + damage( caster ) / 3; - } - } - return std::max( loudness * temp_sound_multiplyer, 0.0f ); -} - -void spell::make_sound( const tripoint &target, Creature &caster ) const -{ - const int loudness = sound_volume( caster ); - if( loudness > 0 ) { + int loudness = abs( damage() ) / 3; + if( has_flag( spell_flag::LOUD ) ) { + loudness += 1 + damage() / 3; + } make_sound( target, loudness ); } } @@ -1493,17 +878,12 @@ std::string spell::effect() const return type->effect_name; } -magic_energy_type spell::energy_source() const +energy_type spell::energy_source() const { return type->energy_source; } -bool spell::is_target_in_range( const Creature &caster, const tripoint &p ) const -{ - return rl_dist( caster.pos(), p ) <= range( caster ); -} - -bool spell::is_valid_target( spell_target t ) const +bool spell::is_valid_target( valid_target t ) const { return type->valid_targets[t]; } @@ -1511,72 +891,23 @@ bool spell::is_valid_target( spell_target t ) const bool spell::is_valid_target( const Creature &caster, const tripoint &p ) const { bool valid = false; - if( Creature *const cr = get_creature_tracker().creature_at( p ) ) { + if( Creature *const cr = g->critter_at( p ) ) { Creature::Attitude cr_att = cr->attitude_to( caster ); - valid = valid || ( cr_att != Creature::Attitude::FRIENDLY && - is_valid_target( spell_target::hostile ) ); - valid = valid || ( cr_att == Creature::Attitude::FRIENDLY && - is_valid_target( spell_target::ally ) && + valid = valid || ( cr_att != Creature::A_FRIENDLY && is_valid_target( target_hostile ) ); + valid = valid || ( cr_att == Creature::A_FRIENDLY && is_valid_target( target_ally ) && p != caster.pos() ); - valid = valid || ( is_valid_target( spell_target::self ) && p == caster.pos() ); - valid = valid && target_by_monster_id( p ); - valid = valid && target_by_species_id( p ); - valid = valid && ignore_by_species_id( p ); + valid = valid || ( is_valid_target( target_self ) && p == caster.pos() ); } else { - valid = is_valid_target( spell_target::ground ); - } - return valid; -} - -bool spell::target_by_monster_id( const tripoint &p ) const -{ - if( type->targeted_monster_ids.empty() ) { - return true; - } - bool valid = false; - if( monster *const target = get_creature_tracker().creature_at( p ) ) { - if( type->targeted_monster_ids.find( target->type->id ) != type->targeted_monster_ids.end() ) { - valid = true; - } - } - return valid; -} - -bool spell::target_by_species_id( const tripoint &p ) const -{ - if( type->targeted_species_ids.empty() ) { - return true; - } - bool valid = false; - if( monster *const target = get_creature_tracker().creature_at( p ) ) { - for( const species_id &spid : type->targeted_species_ids ) { - if( target->type->in_species( spid ) ) { - valid = true; - } - } + valid = is_valid_target( target_ground ); } return valid; } -bool spell::ignore_by_species_id( const tripoint &p ) const +bool spell::is_valid_effect_target( valid_target t ) const { - if( type->ignored_species_ids.empty() ) { - return true; - } - bool valid = true; - if( monster *const target = get_creature_tracker().creature_at( p ) ) { - for( const species_id &spid : type->ignored_species_ids ) { - if( target->type->in_species( spid ) ) { - valid = false; - } - } - } - return valid; + return type->effect_targets[t]; } - - - std::string spell::description() const { return type->description.translated(); @@ -1584,104 +915,84 @@ std::string spell::description() const nc_color spell::damage_type_color() const { - if( dmg_type().is_null() ) { - return c_black; + switch( dmg_type() ) { + case DT_HEAT: + return c_red; + case DT_ACID: + return c_light_green; + case DT_BASH: + return c_magenta; + case DT_BIOLOGICAL: + return c_green; + case DT_COLD: + return c_white; + case DT_CUT: + return c_light_gray; + case DT_ELECTRIC: + return c_light_blue; + case DT_STAB: + return c_light_red; + case DT_TRUE: + return c_dark_gray; + default: + return c_black; } - return dmg_type()->magic_color; } std::string spell::damage_type_string() const { - if( dmg_type().is_null() ) { - return std::string(); + switch( dmg_type() ) { + case DT_HEAT: + return "heat"; + case DT_ACID: + return "acid"; + case DT_BASH: + return "bashing"; + case DT_BIOLOGICAL: + return "biological"; + case DT_COLD: + return "cold"; + case DT_CUT: + return "cutting"; + case DT_ELECTRIC: + return "electric"; + case DT_STAB: + return "stabbing"; + case DT_TRUE: + // not *really* force damage + return "force"; + default: + return "error"; } - return dmg_type()->name.translated(); } // constants defined below are just for the formula to be used, // in order for the inverse formula to be equivalent -static constexpr double a = 6200.0; -static constexpr double b = 0.146661; -static constexpr double c = -62.5; +constexpr float a = 6200.0; +constexpr float b = 0.146661; +constexpr float c = -62.5; int spell::get_level() const { // you aren't at the next level unless you have the requisite xp, so floor - return std::max( static_cast( std::floor( std::log( experience + a ) / b + c ) ), 0 ); -} - -int spell::get_effective_level() const -{ - return get_level() + temp_level_adjustment; -} - -int spell::get_max_level( const Creature &caster ) const -{ - dialogue d( get_talker_for( caster ), nullptr ); - return type->max_level.evaluate( d ); -} - -int spell::get_temp_level_adjustment() const -{ - return temp_level_adjustment; + return std::max( static_cast( floor( log( experience + a ) / b + c ) ), 0 ); } -void spell::set_temp_level_adjustment( int adjustment ) -{ - temp_level_adjustment = adjustment; -} - - -void spell::set_temp_adjustment( const std::string &target_property, float adjustment ) +int spell::get_max_level() const { - if( target_property == "caster_level" ) { - temp_level_adjustment += adjustment; - } else if( target_property == "casting_time" ) { - temp_cast_time_multiplyer += adjustment; - } else if( target_property == "cost" ) { - temp_spell_cost_multiplyer += adjustment; - } else if( target_property == "aoe" ) { - temp_aoe_multiplyer += adjustment; - } else if( target_property == "range" ) { - temp_range_multiplyer += adjustment; - } else if( target_property == "duration" ) { - temp_duration_multiplyer += adjustment; - } else if( target_property == "difficulty" ) { - temp_difficulty_adjustment += adjustment; - } else if( target_property == "somatic_difficulty" ) { - temp_somatic_difficulty_multiplyer += adjustment; - } else if( target_property == "sound" ) { - temp_sound_multiplyer += adjustment; - } else if( target_property == "concentration" ) { - temp_concentration_difficulty_multiplyer += adjustment; - } else { - debugmsg( "ERROR: invalid spellcasting adjustment name: %s", target_property ); - } -} -void spell::clear_temp_adjustments() -{ - temp_level_adjustment = 0; - temp_cast_time_multiplyer = 1; - temp_spell_cost_multiplyer = 1; - temp_aoe_multiplyer = 1; - temp_range_multiplyer = 1; - temp_duration_multiplyer = 1; - temp_difficulty_adjustment = 0; - temp_somatic_difficulty_multiplyer = 1; - temp_sound_multiplyer = 1; - temp_concentration_difficulty_multiplyer = 1; + return type->max_level; } // helper function to calculate xp needed to be at a certain level // pulled out as a helper function to make it easier to either be used in the future // or easier to tweak the formula -int spell::exp_for_level( int level ) +static int exp_for_level( int level ) { // level 0 never needs xp if( level == 0 ) { return 0; } - return std::ceil( std::exp( ( level - c ) * b ) ) - a; + return ceil( exp( ( level - c ) * b ) ) - a; } int spell::exp_to_next_level() const @@ -1697,41 +1008,41 @@ std::string spell::exp_progress() const const int denominator = next_level_xp - this_level_xp; const float progress = static_cast( xp() - this_level_xp ) / std::max( 1.0f, static_cast( denominator ) ); - return string_format( "%i%%", clamp( static_cast( std::round( progress * 100 ) ), 0, 99 ) ); + return string_format( "%i%%", clamp( static_cast( round( progress * 100 ) ), 0, 99 ) ); } -float spell::exp_modifier( const Character &guy ) const +float spell::exp_modifier( const player &p ) const { - const float int_modifier = ( guy.get_int() - 8.0f ) / 8.0f; - const float difficulty_modifier = get_difficulty( guy ) / 20.0f; - const float spellcraft_modifier = guy.get_skill_level( skill() ) / 10.0f; + const float int_modifier = ( p.get_int() - 8.0f ) / 8.0f; + const float difficulty_modifier = get_difficulty() / 20.0f; + const float spellcraft_modifier = p.get_skill_level( skill_id( "spellcraft" ) ) / 10.0f; return ( int_modifier + difficulty_modifier + spellcraft_modifier ) / 5.0f + 1.0f; } -int spell::casting_exp( const Character &guy ) const +int spell::casting_exp( const player &p ) const { // the amount of xp you would get with no modifiers const int base_casting_xp = 75; - return std::round( guy.adjust_for_focus( base_casting_xp * exp_modifier( guy ) ) ); + return round( p.adjust_for_focus( base_casting_xp * exp_modifier( p ) ) ); } std::string spell::enumerate_targets() const { std::vector all_valid_targets; - int last_target = static_cast( spell_target::num_spell_targets ); + int last_target = static_cast( valid_target::_LAST ); for( int i = 0; i < last_target; ++i ) { - spell_target t = static_cast( i ); - if( is_valid_target( t ) && t != spell_target::none ) { - all_valid_targets.emplace_back( target_to_string( t ) ); + valid_target t = static_cast( i ); + if( is_valid_target( t ) && t != target_none ) { + all_valid_targets.emplace_back( io::enum_to_string( t ) ); } } if( all_valid_targets.size() == 1 ) { return all_valid_targets[0]; } std::string ret; - // TODO: if only we had a function to enumerate strings and concatenate them... + // @todo if only we had a function to enumerate strings and concatenate them... for( auto iter = all_valid_targets.begin(); iter != all_valid_targets.end(); iter++ ) { if( iter + 1 == all_valid_targets.end() ) { ret = string_format( _( "%s and %s" ), ret, *iter ); @@ -1744,126 +1055,46 @@ std::string spell::enumerate_targets() const return ret; } -std::string spell::list_targeted_monster_names() const -{ - if( type->targeted_monster_ids.empty() ) { - return ""; - } - std::vector all_valid_monster_names; - for( const mtype_id &mon_id : type->targeted_monster_ids ) { - all_valid_monster_names.emplace_back( mon_id->nname() ); - } - //remove repeat names - all_valid_monster_names.erase( std::unique( all_valid_monster_names.begin(), - all_valid_monster_names.end() ), all_valid_monster_names.end() ); - std::string ret = enumerate_as_string( all_valid_monster_names ); - return ret; -} - -std::string spell::list_targeted_species_names() const -{ - if( type->targeted_species_ids.empty() ) { - return ""; - } - std::vector all_valid_species_names; - for( const species_id &specie_id : type->targeted_species_ids ) { - all_valid_species_names.emplace_back( specie_id.str() ); - } - //remove repeat names - all_valid_species_names.erase( std::unique( all_valid_species_names.begin(), - all_valid_species_names.end() ), all_valid_species_names.end() ); - std::string ret = enumerate_as_string( all_valid_species_names ); - return ret; -} - -std::string spell::list_ignored_species_names() const -{ - if( type->ignored_species_ids.empty() ) { - return ""; - } - std::vector all_valid_species_names; - for( const species_id &species_id : type->ignored_species_ids ) { - all_valid_species_names.emplace_back( species_id.str() ); - } - //remove repeat names - all_valid_species_names.erase( std::unique( all_valid_species_names.begin(), - all_valid_species_names.end() ), all_valid_species_names.end() ); - std::string ret = enumerate_as_string( all_valid_species_names ); - return ret; -} - -const damage_type_id &spell::dmg_type() const +damage_type spell::dmg_type() const { return type->dmg_type; } -damage_instance spell::get_damage_instance( Creature &caster ) const +damage_instance spell::get_damage_instance() const { damage_instance dmg; - dmg.add_damage( dmg_type(), damage( caster ) ); + dmg.add_damage( dmg_type(), damage() ); return dmg; } -dealt_damage_instance spell::get_dealt_damage_instance( Creature &caster ) const +dealt_damage_instance spell::get_dealt_damage_instance() const { dealt_damage_instance dmg; - dmg.set_damage( dmg_type(), damage( caster ) ); + dmg.set_damage( dmg_type(), damage() ); return dmg; } -dealt_projectile_attack spell::get_projectile_attack( const tripoint &target, - Creature &hit_critter, Creature &caster ) const -{ - projectile bolt; - bolt.speed = 10000; - bolt.impact = get_damage_instance( caster ); - bolt.proj_effects.emplace( ammo_effect_MAGIC ); - - dealt_projectile_attack atk; - atk.end_point = target; - atk.hit_critter = &hit_critter; - atk.proj = bolt; - atk.missed_by = 0.0; - - return atk; -} - std::string spell::effect_data() const { return type->effect_str; } -vproto_id spell::summon_vehicle_id() const -{ - return vproto_id( type->effect_str ); -} - -int spell::heal( const tripoint &target, Creature &caster ) const +int spell::heal( const tripoint &target ) const { - creature_tracker &creatures = get_creature_tracker(); - monster *const mon = creatures.creature_at( target ); + monster *const mon = g->critter_at( target ); if( mon ) { - return mon->heal( -damage( caster ) ); + return mon->heal( -damage() ); } - Character *const p = creatures.creature_at( target ); + player *const p = g->critter_at( target ); if( p ) { - p->healall( -damage( caster ) ); - return -damage( caster ); + p->healall( -damage() ); + return -damage(); } return -1; } void spell::cast_spell_effect( Creature &source, const tripoint &target ) const { - Character *caster = source.as_character(); - if( caster ) { - character_id c_id = caster->getID(); - // send casting to the event bus - get_event_bus().send( c_id, this->id(), this->spell_class(), - this->get_difficulty( source ), this->energy_cost( *caster ), this->casting_time( *caster ), - this->damage( source ) ); - } - type->effect( *this, source, target ); } @@ -1871,23 +1102,24 @@ void spell::cast_all_effects( Creature &source, const tripoint &target ) const { if( has_flag( spell_flag::WONDER ) ) { const auto iter = type->additional_spells.begin(); - for( int num_spells = std::abs( damage( source ) ); num_spells > 0; num_spells-- ) { + for( int num_spells = abs( damage() ); num_spells > 0; num_spells-- ) { if( type->additional_spells.empty() ) { debugmsg( "ERROR: %s has WONDER flag but no spells to choose from!", type->id.c_str() ); return; } const int rand_spell = rng( 0, type->additional_spells.size() - 1 ); - spell sp = ( iter + rand_spell )->get_spell( source, get_effective_level() ); + spell sp = ( iter + rand_spell )->get_spell( get_level() ); const bool _self = ( iter + rand_spell )->self; // This spell flag makes it so the message of the spell that's cast using this spell will be sent. // if a message is added to the casting spell, it will be sent as well. source.add_msg_if_player( sp.message() ); - if( sp.has_flag( spell_flag::RANDOM_TARGET ) ) { - if( const std::optional new_target = sp.random_valid_target( source, - _self ? source.pos() : target ) ) { - sp.cast_all_effects( source, *new_target ); + if( sp.has_flag( RANDOM_TARGET ) ) { + if( _self ) { + sp.cast_all_effects( source, sp.random_valid_target( source, source.pos() ) ); + } else { + sp.cast_all_effects( source, sp.random_valid_target( source, target ) ); } } else { if( _self ) { @@ -1898,62 +1130,48 @@ void spell::cast_all_effects( Creature &source, const tripoint &target ) const } } } else { - if( has_flag( spell_flag::EXTRA_EFFECTS_FIRST ) ) { - cast_extra_spell_effects( source, target ); - cast_spell_effect( source, target ); - } else { - cast_spell_effect( source, target ); - cast_extra_spell_effects( source, target ); - } - } -} - -void spell::cast_extra_spell_effects( Creature &source, const tripoint &target ) const -{ - for( const fake_spell &extra_spell : type->additional_spells ) { - spell sp = extra_spell.get_spell( source, get_effective_level() ); - if( sp.has_flag( spell_flag::RANDOM_TARGET ) ) { - if( const std::optional new_target = sp.random_valid_target( source, - extra_spell.self ? source.pos() : target ) ) { - sp.cast_all_effects( source, *new_target ); - } - } else { - if( extra_spell.self ) { - sp.cast_all_effects( source, source.pos() ); + // first call the effect of the main spell + cast_spell_effect( source, target ); + for( const fake_spell &extra_spell : type->additional_spells ) { + spell sp = extra_spell.get_spell( get_level() ); + if( sp.has_flag( RANDOM_TARGET ) ) { + if( extra_spell.self ) { + sp.cast_all_effects( source, sp.random_valid_target( source, source.pos() ) ); + } else { + sp.cast_all_effects( source, sp.random_valid_target( source, target ) ); + } } else { - sp.cast_all_effects( source, target ); + if( extra_spell.self ) { + sp.cast_all_effects( source, source.pos() ); + } else { + sp.cast_all_effects( source, target ); + } } } } } -std::optional spell::random_valid_target( const Creature &caster, - const tripoint &caster_pos ) const +tripoint spell::random_valid_target( const Creature &caster, const tripoint &caster_pos ) const { - const bool ignore_ground = has_flag( spell_flag::RANDOM_CRITTER ); + const std::set area = spell_effect::spell_effect_blast( *this, caster_pos, caster_pos, + range(), false ); std::set valid_area; - spell_effect::override_parameters blast_params( *this, caster ); - // we want to pick a random target within range, not aoe - blast_params.aoe_radius = range( caster ); - creature_tracker &creatures = get_creature_tracker(); - for( const tripoint &target : spell_effect::spell_effect_blast( - blast_params, caster_pos, caster_pos ) ) { - if( target != caster_pos && is_valid_target( caster, target ) && - ( !ignore_ground || creatures.creature_at( target ) ) ) { + for( const tripoint &target : area ) { + if( is_valid_target( caster, target ) ) { valid_area.emplace( target ); } } - if( valid_area.empty() ) { - return std::nullopt; - } - return random_entry( valid_area ); + size_t rand_i = rng( 0, valid_area.size() - 1 ); + auto iter = valid_area.begin(); + std::advance( iter, rand_i ); + return *iter; } // player known_magic::known_magic() { - mana_base = 3500; + mana_base = 1000; mana = mana_base; } @@ -1965,7 +1183,7 @@ void known_magic::serialize( JsonOut &json ) const json.member( "spellbook" ); json.start_array(); - for( const auto &pair : spellbook ) { + for( auto pair : spellbook ) { json.start_object(); json.member( "id", pair.second.id() ); json.member( "xp", pair.second.xp() ); @@ -1973,23 +1191,21 @@ void known_magic::serialize( JsonOut &json ) const } json.end_array(); json.member( "invlets", invlets ); - json.member( "favorites", favorites ); json.end_object(); } -void known_magic::deserialize( const JsonObject &data ) +void known_magic::deserialize( JsonIn &jsin ) { + JsonObject data = jsin.get_object(); data.read( "mana", mana ); - for( JsonObject jo : data.get_array( "spellbook" ) ) { + JsonArray parray = data.get_array( "spellbook" ); + while( parray.has_more() ) { + JsonObject jo = parray.next_object(); std::string id = jo.get_string( "id" ); spell_id sp = spell_id( id ); int xp = jo.get_int( "xp" ); - if( !sp.is_valid() ) { - DebugLog( D_WARNING, D_MAIN ) << "Tried to load bad spell: " << sp.c_str(); - continue; - } if( knows_spell( sp ) ) { spellbook[sp].set_exp( xp ); } else { @@ -1997,7 +1213,6 @@ void known_magic::deserialize( const JsonObject &data ) } } data.read( "invlets", invlets ); - data.read( "favorites", favorites ); } bool known_magic::knows_spell( const std::string &sp ) const @@ -2015,23 +1230,23 @@ bool known_magic::knows_spell() const return !spellbook.empty(); } -void known_magic::learn_spell( const std::string &sp, Character &guy, bool force ) +void known_magic::learn_spell( const std::string &sp, player &p, bool force ) { - learn_spell( spell_id( sp ), guy, force ); + learn_spell( spell_id( sp ), p, force ); } -void known_magic::learn_spell( const spell_id &sp, Character &guy, bool force ) +void known_magic::learn_spell( const spell_id &sp, player &p, bool force ) { - learn_spell( &sp.obj(), guy, force ); + learn_spell( &sp.obj(), p, force ); } -void known_magic::learn_spell( const spell_type *sp, Character &guy, bool force ) +void known_magic::learn_spell( const spell_type *sp, player &p, bool force ) { if( !sp->is_valid() ) { debugmsg( "Tried to learn invalid spell" ); return; } - if( guy.magic->knows_spell( sp->id ) ) { + if( p.magic.knows_spell( sp->id ) ) { // you already know the spell return; } @@ -2040,8 +1255,8 @@ void known_magic::learn_spell( const spell_type *sp, Character &guy, bool force debugmsg( "Tried to learn invalid spell" ); return; } - if( !force && sp->spell_class != trait_NONE ) { - if( can_learn_spell( guy, sp->id ) && !guy.has_trait( sp->spell_class ) ) { + if( !force && sp->spell_class != trait_id( "NONE" ) ) { + if( can_learn_spell( p, sp->id ) && !p.has_trait( sp->spell_class ) ) { std::string trait_cancel; for( const trait_id &cancel : sp->spell_class->cancels ) { if( cancel == sp->spell_class->cancels.back() && @@ -2059,24 +1274,22 @@ void known_magic::learn_spell( const spell_type *sp, Character &guy, bool force trait_cancel += "."; } } - if( !guy.is_avatar() || query_yn( + if( query_yn( _( "Learning this spell will make you a\n\n%s: %s\n\nand lock you out of\n\n%s\n\nContinue?" ), sp->spell_class->name(), sp->spell_class->desc(), trait_cancel ) ) { - guy.set_mutation( sp->spell_class ); - guy.on_mutation_gain( sp->spell_class ); - guy.add_msg_if_player( sp->spell_class.obj().desc() ); + p.set_mutation( sp->spell_class ); + p.on_mutation_gain( sp->spell_class ); + p.add_msg_if_player( sp->spell_class.obj().desc() ); } else { return; } } } - if( force || can_learn_spell( guy, sp->id ) ) { + if( force || can_learn_spell( p, sp->id ) ) { spellbook.emplace( sp->id, temp_spell ); - get_event_bus().send( guy.getID(), sp->id ); - guy.add_msg_player_or_npc( m_good, _( "You learned %s!" ), _( " learned %s!" ), sp->name ); + p.add_msg_if_player( m_good, _( "You learned %s!" ), sp->name ); } else { - guy.add_msg_player_or_npc( m_bad, _( "You can't learn this spell." ), - _( " can't learn this spell." ) ); + p.add_msg_if_player( m_bad, _( "You can't learn this spell." ) ); } } @@ -2092,77 +1305,22 @@ void known_magic::forget_spell( const spell_id &sp ) return; } add_msg( m_bad, _( "All knowledge of %s leaves you." ), sp->name ); - // TODO: add parameter for owner of known_magic for this function - get_event_bus().send( get_player_character().getID(), sp->id ); spellbook.erase( sp ); } -void known_magic::set_spell_level( const spell_id &sp, int new_level, const Character *guy ) -{ - spell temp_spell( sp->id ); - if( !knows_spell( sp ) ) { - if( new_level >= 0 ) { - temp_spell.set_level( *guy, new_level ); - spellbook.emplace( sp->id, spell( temp_spell ) ); - get_event_bus().send( guy->getID(), sp->id ); - } - } else { - if( new_level >= 0 ) { - spell &temp_sp = get_spell( sp ); - temp_sp.set_level( *guy, new_level ); - } else { - get_event_bus().send( guy->getID(), sp->id ); - spellbook.erase( sp ); - } - } -} - -void known_magic::set_spell_exp( const spell_id &sp, int new_exp, const Character *guy ) +bool known_magic::can_learn_spell( const player &p, const spell_id &sp ) const { - spell temp_spell( sp->id ); - if( !knows_spell( sp ) ) { - if( new_exp >= 0 ) { - temp_spell.set_exp( new_exp ); - spellbook.emplace( sp->id, spell( temp_spell ) ); - get_event_bus().send( guy->getID(), sp->id ); - } - } else { - if( new_exp >= 0 ) { - spell &temp_sp = get_spell( sp ); - int old_level = temp_sp.get_level(); - temp_sp.set_exp( new_exp ); - if( guy->is_avatar() && old_level != temp_sp.get_level() ) { - get_event_bus().send( guy->getID(), sp->id, temp_sp.get_level(), - sp->spell_class ); - } - } else { - get_event_bus().send( guy->getID(), sp->id ); - spellbook.erase( sp ); - } + const spell_type &sp_t = sp.obj(); + if( sp_t.spell_class == trait_id( "NONE" ) ) { + return true; } -} - -bool known_magic::can_learn_spell( const Character &guy, const spell_id &sp ) const -{ - return true; - - //const spell_type &sp_t = sp.obj(); - //if( sp_t.spell_class == trait_NONE ) { - // return true; - //} - //if( sp_t.spell_tags[spell_flag::MUST_HAVE_CLASS_TO_LEARN] ) { - // return guy.has_trait( sp_t.spell_class ); - //} else { - // return !guy.has_opposite_trait( sp_t.spell_class ); - //} + return !p.has_opposite_trait( sp_t.spell_class ); } spell &known_magic::get_spell( const spell_id &sp ) { if( !knows_spell( sp ) ) { debugmsg( "ERROR: Tried to get unknown spell" ); - static spell null_spell_reference( spell_id::NULL_ID() ); - return null_spell_reference; // Don't make up new spells in our spellbook } spell &temp_spell = spellbook[ sp ]; return temp_spell; @@ -2171,7 +1329,6 @@ spell &known_magic::get_spell( const spell_id &sp ) std::vector known_magic::get_spells() { std::vector spells; - spells.reserve( spellbook.size() ); for( auto &spell_pair : spellbook ) { spells.emplace_back( &spell_pair.second ); } @@ -2188,111 +1345,75 @@ void known_magic::set_mana( int new_mana ) mana = new_mana; } -void known_magic::mod_mana( const Character &guy, int add_mana ) +void known_magic::mod_mana( const player &p, int add_mana ) { - set_mana( clamp( mana + add_mana, 0, max_mana( guy ) ) ); + set_mana( clamp( mana + add_mana, 0, max_mana( p ) ) ); } -int known_magic::max_mana( const Character &guy ) const +int known_magic::max_mana( const player &p ) const { - const float int_bonus = ( ( 0.2f + guy.get_int() * 0.1f ) - 1.0f ) * mana_base; - int penalty_calc = std::round( std::max( 0, - units::to_kilojoule( guy.get_power_level() ) ) ); - - const int bionic_penalty = guy.enchantment_cache->modify_value( - enchant_vals::mod::BIONIC_MANA_PENALTY, penalty_calc ); - + const float int_bonus = ( ( 0.2f + p.get_int() * 0.1f ) - 1.0f ) * mana_base; const float unaugmented_mana = std::max( 0.0f, - ( mana_base + int_bonus ) - bionic_penalty ); - return guy.calculate_by_enchantment( unaugmented_mana, enchant_vals::mod::MAX_MANA, true ); + ( ( mana_base + int_bonus ) * p.mutation_value( "mana_multiplier" ) ) + + p.mutation_value( "mana_modifier" ) - units::to_kilojoule( p.get_power_level() ) ); + return p.calculate_by_enchantment( unaugmented_mana, enchantment::mod::MAX_MANA, true ); } -void known_magic::update_mana( const Character &guy, float turns ) +void known_magic::update_mana( const player &p, float turns ) { // mana should replenish in 8 hours. - const double full_replenish = to_turns( 8_hours ); - const double ratio = turns / full_replenish; - mod_mana( guy, std::floor( ratio * guy.calculate_by_enchantment( static_cast( max_mana( - guy ) ), enchant_vals::mod::REGEN_MANA ) ) ); + const float full_replenish = to_turns( 8_hours ); + const float ratio = turns / full_replenish; + mod_mana( p, floor( ratio * p.calculate_by_enchantment( max_mana( p ) * + p.mutation_value( "mana_regen_multiplier" ), enchantment::mod::REGEN_MANA ) ) ); } std::vector known_magic::spells() const { std::vector spell_ids; - spell_ids.reserve( spellbook.size() ); - for( const std::pair &pair : spellbook ) { + for( auto pair : spellbook ) { spell_ids.emplace_back( pair.first ); } return spell_ids; } -// does the Character have enough energy (of the type of the spell) to cast the spell? -bool known_magic::has_enough_energy( const Character &guy, const spell &sp ) const +// does the player have enough energy (of the type of the spell) to cast the spell? +bool known_magic::has_enough_energy( const player &p, spell &sp ) const { - int cost = sp.energy_cost( guy ); + int cost = sp.energy_cost( p ); switch( sp.energy_source() ) { - case magic_energy_type::mana: + case mana_energy: return available_mana() >= cost; - case magic_energy_type::bionic: - return guy.get_power_level() >= units::from_kilojoule( static_cast( cost ) ); - case magic_energy_type::stamina: - return guy.get_stamina() >= cost; - case magic_energy_type::hp: - for( const std::pair &elem : guy.get_body() ) { - if( elem.second.get_hp_cur() > cost ) { + case bionic_energy: + return p.get_power_level() >= units::from_kilojoule( cost ); + case stamina_energy: + return p.get_stamina() >= cost; + case hp_energy: + for( int i = 0; i < num_hp_parts; i++ ) { + if( p.hp_cur[i] > cost ) { return true; } } return false; - case magic_energy_type::none: + case fatigue_energy: + return p.get_fatigue() < EXHAUSTED; + case none_energy: return true; default: return false; } } -void known_magic::clear_opens_spellbook_data() -{ - caster_level_adjustment = 0; - caster_level_adjustment_by_spell.clear(); - caster_level_adjustment_by_school.clear(); - for( spell *sp : get_spells() ) { - sp->clear_temp_adjustments(); - } -} - -void known_magic::evaluate_opens_spellbook_data() -{ - for( spell *sp : get_spells() ) { - double raw_level_adjust = caster_level_adjustment; - std::map::iterator school_it = - caster_level_adjustment_by_school.find( sp->spell_class() ); - if( school_it != caster_level_adjustment_by_school.end() ) { - raw_level_adjust += school_it->second; - } - std::map::iterator spell_it = - caster_level_adjustment_by_spell.find( sp->id() ); - if( spell_it != caster_level_adjustment_by_spell.end() ) { - raw_level_adjust += spell_it->second; - } - int final_level = clamp( sp->get_level() + static_cast( raw_level_adjust ), 0, - sp->get_max_level( get_player_character() ) ); - sp->set_temp_level_adjustment( final_level - sp->get_level() ); - } -} - -int known_magic::time_to_learn_spell( const Character &guy, const std::string &str ) const +int known_magic::time_to_learn_spell( const player &p, const std::string &str ) const { - return time_to_learn_spell( guy, spell_id( str ) ); + return time_to_learn_spell( p, spell_id( str ) ); } -int known_magic::time_to_learn_spell( const Character &guy, const spell_id &sp ) const +int known_magic::time_to_learn_spell( const player &p, const spell_id &sp ) const { - dialogue d( get_talker_for( guy ), nullptr ); const int base_time = to_moves( 30_minutes ); - const double int_modifier = ( guy.get_int() - 8.0 ) / 8.0; - const double skill_modifier = guy.get_skill_level( sp->skill ) / 10.0; - return base_time * ( 1.0 + sp->difficulty.evaluate( d ) / ( 1.0 + int_modifier + skill_modifier ) ); + return base_time * ( 1.0 + sp.obj().difficulty / ( 1.0 + ( p.get_int() - 8.0 ) / 8.0 ) + + ( p.get_skill_level( skill_id( "spellcraft" ) ) / 10.0 ) ); } int known_magic::get_spellname_max_width() @@ -2304,65 +1425,31 @@ int known_magic::get_spellname_max_width() return width; } -std::vector Character::spells_known_of_class( const trait_id &spell_class ) const -{ - std::vector ret; - - if( !has_trait( spell_class ) ) { - return ret; - } - - for( const spell_id &sp : magic->spells() ) { - if( sp->spell_class == spell_class ) { - ret.push_back( magic->get_spell( sp ) ); - } - } - - return ret; -} - -static void reflesh_favorite( uilist *menu, std::vector known_spells ) -{ - for( uilist_entry &entry : menu->entries ) { - if( get_player_character().magic->is_favorite( known_spells[entry.retval]->id() ) ) { - entry.extratxt.left = 0; - entry.extratxt.txt = _( "*" ); - entry.extratxt.color = c_white; - } else { - entry.extratxt.txt = ""; - } - } -} - class spellcasting_callback : public uilist_callback { private: - int selected_sp = 0; - int scroll_pos = 0; - std::vector info_txt; std::vector known_spells; - void spell_info_text( const spell &sp, int width ); - void draw_spell_info( const uilist *menu ); + void draw_spell_info( const spell &sp, const uilist *menu ); public: // invlets reserved for special functions - const std::set reserved_invlets{ 'I', '=', '*' }; + const std::set reserved_invlets{ 'I', '=' }; bool casting_ignore; spellcasting_callback( std::vector &spells, bool casting_ignore ) : known_spells( spells ), casting_ignore( casting_ignore ) {} - bool key( const input_context &ctxt, const input_event &event, int entnum, - uilist *menu ) override { - const std::string &action = ctxt.input_to_action( event ); - if( action == "CAST_IGNORE" ) { + bool key( const input_context &, const input_event &event, int entnum, + uilist * /*menu*/ ) override { + if( event.get_first_input() == 'I' ) { casting_ignore = !casting_ignore; return true; - } else if( action == "CHOOSE_INVLET" ) { + } + if( event.get_first_input() == '=' ) { int invlet = 0; invlet = popup_getkey( _( "Choose a new hotkey for this spell." ) ); if( inv_chars.valid( invlet ) ) { - const bool invlet_set = - get_player_character().magic->set_invlet( known_spells[entnum]->id(), invlet, reserved_invlets ); + const bool invlet_set = g->u.magic.set_invlet( known_spells[entnum]->id(), invlet, + reserved_invlets ); if( !invlet_set ) { popup( _( "Hotkey already used." ) ); } else { @@ -2371,26 +1458,19 @@ class spellcasting_callback : public uilist_callback } } else { popup( _( "Hotkey removed." ) ); - get_player_character().magic->rem_invlet( known_spells[entnum]->id() ); + g->u.magic.rem_invlet( known_spells[entnum]->id() ); } return true; - } else if( action == "SCROLL_UP_SPELL_MENU" || action == "SCROLL_DOWN_SPELL_MENU" ) { - scroll_pos += action == "SCROLL_DOWN_SPELL_MENU" ? 1 : -1; - } else if( action == "SCROLL_FAVORITE" ) { - get_player_character().magic->toggle_favorite( known_spells[entnum]->id() ); - reflesh_favorite( menu, known_spells ); } return false; } - void refresh( uilist *menu ) override { - const std::string space( menu->pad_right - 2, ' ' ); + void select( int entnum, uilist *menu ) override { mvwputch( menu->window, point( menu->w_width - menu->pad_right, 0 ), c_magenta, LINE_OXXX ); mvwputch( menu->window, point( menu->w_width - menu->pad_right, menu->w_height - 1 ), c_magenta, LINE_XXOX ); for( int i = 1; i < menu->w_height - 1; i++ ) { mvwputch( menu->window, point( menu->w_width - menu->pad_right, i ), c_magenta, LINE_XOXO ); - mvwputch( menu->window, point( menu->w_width - menu->pad_right + 1, i ), menu->text_color, space ); } std::string ignore_string = casting_ignore ? _( "Ignore Distractions" ) : _( "Popup Distractions" ); @@ -2399,333 +1479,194 @@ class spellcasting_callback : public uilist_callback const std::string assign_letter = _( "Assign Hotkey [=]" ); mvwprintz( menu->window, point( menu->w_width - assign_letter.length() - 1, 0 ), c_yellow, assign_letter ); - if( menu->selected >= 0 && static_cast( menu->selected ) < known_spells.size() ) { - if( info_txt.empty() || selected_sp != menu->selected ) { - info_txt.clear(); - spell_info_text( *known_spells[menu->selected], menu->pad_right - 4 ); - selected_sp = menu->selected; - scroll_pos = 0; - } - if( scroll_pos > static_cast( info_txt.size() ) - ( menu->w_height - 2 ) ) { - scroll_pos = info_txt.size() - ( menu->w_height - 2 ); - } - if( scroll_pos < 0 ) { - scroll_pos = 0; - } - scrollbar() - .offset_x( menu->w_width - 1 ) - .offset_y( 1 ) - .content_size( info_txt.size() ) - .viewport_pos( scroll_pos ) - .viewport_size( menu->w_height - 2 ) - .apply( menu->window ); - draw_spell_info( menu ); - } - wnoutrefresh( menu->window ); + draw_spell_info( *known_spells[entnum], menu ); } }; -bool spell::casting_time_encumbered( const Character &guy ) const +static bool casting_time_encumbered( const spell &sp, const player &p ) { int encumb = 0; - if( !has_flag( spell_flag::NO_LEGS ) && temp_somatic_difficulty_multiplyer > 0 ) { + if( !sp.has_flag( spell_flag::NO_LEGS ) ) { // the first 20 points of encumbrance combined is ignored - encumb += std::max( 0, guy.avg_encumb_of_limb_type( body_part_type::type::leg ) - 10 ); + encumb += std::max( 0, p.encumb( bp_leg_l ) + p.encumb( bp_leg_r ) - 20 ); } - if( has_flag( spell_flag::SOMATIC ) && !guy.has_flag( json_flag_SUBTLE_SPELL ) && - temp_somatic_difficulty_multiplyer > 0 ) { + if( sp.has_flag( spell_flag::SOMATIC ) ) { // the first 20 points of encumbrance combined is ignored - encumb += std::max( 0, guy.avg_encumb_of_limb_type( body_part_type::type::arm ) - 10 ); + encumb += std::max( 0, p.encumb( bp_arm_l ) + p.encumb( bp_arm_r ) - 20 ); } return encumb > 0; } -bool spell::energy_cost_encumbered( const Character &guy ) const +static bool energy_cost_encumbered( const spell &sp, const player &p ) { - if( !no_hands() && !guy.has_flag( json_flag_SUBTLE_SPELL ) ) { - return std::max( 0, guy.avg_encumb_of_limb_type( body_part_type::type:: hand ) - 5 ) > - 0; + if( !sp.has_flag( spell_flag::NO_HANDS ) ) { + return std::max( 0, p.encumb( bp_hand_l ) + p.encumb( bp_hand_r ) - 10 ) > 0; } return false; } // this prints various things about the spell out in a list // including flags and things like "goes through walls" -std::string spell::enumerate_spell_data( const Character &guy ) const +static std::string enumerate_spell_data( const spell &sp ) { std::vector spell_data; - if( has_flag( spell_flag::PSIONIC ) ) { - spell_data.emplace_back( _( "is a psionic power" ) ); - } - if( has_flag( spell_flag::CONCENTRATE ) && !has_flag( spell_flag::PSIONIC ) && - temp_concentration_difficulty_multiplyer > 0 ) { + if( sp.has_flag( spell_flag::CONCENTRATE ) ) { spell_data.emplace_back( _( "requires concentration" ) ); } - if( has_flag( spell_flag::VERBAL ) && temp_sound_multiplyer > 0 ) { + if( sp.has_flag( spell_flag::VERBAL ) ) { spell_data.emplace_back( _( "verbal" ) ); } - if( has_flag( spell_flag::SOMATIC ) && temp_somatic_difficulty_multiplyer > 0 ) { + if( sp.has_flag( spell_flag::SOMATIC ) ) { spell_data.emplace_back( _( "somatic" ) ); } - if( !no_hands() ) { + if( !sp.has_flag( spell_flag::NO_HANDS ) ) { spell_data.emplace_back( _( "impeded by gloves" ) ); - } else if( no_hands() && !has_flag( spell_flag::PSIONIC ) ) { + } else { spell_data.emplace_back( _( "does not require hands" ) ); } - if( !has_flag( spell_flag::NO_LEGS ) && temp_somatic_difficulty_multiplyer > 0 ) { + if( !sp.has_flag( spell_flag::NO_LEGS ) ) { spell_data.emplace_back( _( "requires mobility" ) ); } - if( effect() == "attack" && range( guy ) > 1 && has_flag( spell_flag::NO_PROJECTILE ) && - !has_flag( spell_flag::PSIONIC ) ) { + if( sp.effect() == "target_attack" && sp.range() > 1 ) { spell_data.emplace_back( _( "can be cast through walls" ) ); } - if( effect() == "attack" && range( guy ) > 1 && has_flag( spell_flag::NO_PROJECTILE ) && - has_flag( spell_flag::PSIONIC ) ) { - spell_data.emplace_back( _( "can be channeled through walls" ) ); - } return enumerate_as_string( spell_data ); } -void spellcasting_callback::spell_info_text( const spell &sp, int width ) +void spellcasting_callback::draw_spell_info( const spell &sp, const uilist *menu ) { - Character &pc = get_player_character(); + const int h_offset = menu->w_width - menu->pad_right + 1; + // includes spaces on either side for readability + const int info_width = menu->pad_right - 4; + const int win_height = menu->w_height; + const int h_col1 = h_offset + 1; + const int h_col2 = h_offset + ( info_width / 2 ); + const catacurses::window w_menu = menu->window; + // various pieces of spell data mean different things depending on the effect of the spell + const std::string fx = sp.effect(); + int line = 1; + nc_color gray = c_light_gray; + nc_color light_green = c_light_green; + nc_color yellow = c_yellow; - info_txt.emplace_back( colorize( sp.spell_class() == trait_NONE ? _( "Classless" ) : - sp.spell_class()->name(), c_yellow ) ); - for( const std::string &line : foldstring( sp.description(), width ) ) { - info_txt.emplace_back( colorize( line, c_light_gray ) ); - } - info_txt.emplace_back( ); - for( const std::string &line : foldstring( sp.enumerate_spell_data( pc ), width ) ) { - info_txt.emplace_back( colorize( line, c_light_gray ) ); + print_colored_text( w_menu, point( h_col1, line++ ), yellow, yellow, + sp.spell_class() == trait_id( "NONE" ) ? _( "Classless" ) : sp.spell_class()->name() ); + + line += fold_and_print( w_menu, point( h_col1, line ), info_width, gray, sp.description() ); + + line++; + + line += fold_and_print( w_menu, point( h_col1, line ), info_width, gray, + enumerate_spell_data( sp ) ); + if( line <= win_height / 3 ) { + line++; } - info_txt.emplace_back( ); - auto columnize = [&width]( const std::string & col1, const std::string & col2 ) { - std::string line = col1; - int pad = clamp( width / 2 - utf8_width( line, true ), 1, width / 2 ); - line.append( pad, ' ' ); - line.append( col2 ); - return line; - }; - // Calculates temp_level_adjust from EoC, saves it to the spell for later use, and prepares to display the result - int temp_level_adjust = sp.get_temp_level_adjustment(); - std::string temp_level_adjust_string; - if( temp_level_adjust < 0 ) { - temp_level_adjust_string = " (-" + std::to_string( -temp_level_adjust ) + ")"; - } else if( temp_level_adjust > 0 ) { - temp_level_adjust_string = " (+" + std::to_string( temp_level_adjust ) + ")"; - } - const bool is_psi = sp.has_flag( spell_flag::PSIONIC ); - - if( is_psi ) { - info_txt.emplace_back( - colorize( columnize( string_format( "%s: %d%s%s", _( "Power Level" ), sp.get_effective_level(), - sp.is_max_level( pc ) ? _( " (MAX)" ) : "", temp_level_adjust_string.c_str() ), - string_format( "%s: %d", _( "Max Level" ), sp.get_max_level( pc ) ) ), c_light_gray ) ); - } else { - info_txt.emplace_back( - colorize( columnize( string_format( "%s: %d%s%s", _( "Spell Level" ), sp.get_effective_level(), - sp.is_max_level( pc ) ? _( " (MAX)" ) : "", temp_level_adjust_string.c_str() ), - string_format( "%s: %d", _( "Max Level" ), sp.get_max_level( pc ) ) ), c_light_gray ) ); - } - info_txt.emplace_back( - colorize( columnize( sp.colorized_fail_percent( pc ), - string_format( "%s: %d", _( "Difficulty" ), sp.get_difficulty( pc ) ) ), c_light_gray ) ); - info_txt.emplace_back( - colorize( columnize( string_format( "%s: %s", _( "Current Exp" ), - colorize( std::to_string( sp.xp() ), c_light_green ) ), - string_format( "%s: %s", _( "to Next Level" ), - colorize( std::to_string( sp.exp_to_next_level() ), c_light_green ) ) ), c_light_gray ) ); - - info_txt.emplace_back( ); - - const bool cost_encumb = sp.energy_cost_encumbered( pc ); - if( is_psi ) { - std::string cost_string = cost_encumb ? _( "Channeling Cost (impeded)" ) : _( "Channeling Cost" ); - std::string energy_cur = sp.energy_source() == magic_energy_type::hp ? "" : - string_format( _( " (%s current)" ), sp.energy_cur_string( pc ) ); - if( !pc.magic->has_enough_energy( pc, sp ) ) { - cost_string = colorize( _( "Not Enough Stamina" ), c_red ); - energy_cur.clear(); - } - info_txt.emplace_back( - colorize( string_format( "%s: %s %s%s", cost_string, sp.energy_cost_string( pc ), - sp.energy_string(), energy_cur ), c_light_gray ) ); - } else { - std::string cost_string = cost_encumb ? _( "Casting Cost (impeded)" ) : _( "Casting Cost" ); - std::string energy_cur = sp.energy_source() == magic_energy_type::hp ? "" : - string_format( _( " (%s current)" ), sp.energy_cur_string( pc ) ); - if( !pc.magic->has_enough_energy( pc, sp ) ) { - cost_string = colorize( _( "Not Enough Energy" ), c_red ); - energy_cur.clear(); - } - info_txt.emplace_back( - colorize( string_format( "%s: %s %s%s", cost_string, sp.energy_cost_string( pc ), - sp.energy_string(), energy_cur ), c_light_gray ) ); - }; - const bool c_t_encumb = sp.casting_time_encumbered( pc ); - if( is_psi ) { - info_txt.emplace_back( - colorize( string_format( "%s: %s", - c_t_encumb ? _( "Channeling Time (impeded)" ) : _( "Channeling Time" ), - moves_to_string( sp.casting_time( pc ) ) ), c_t_encumb ? c_red : c_light_gray ) ); - - info_txt.emplace_back( ); - } else { - info_txt.emplace_back( - colorize( string_format( "%s: %s", c_t_encumb ? _( "Casting Time (impeded)" ) : _( "Casting Time" ), - moves_to_string( sp.casting_time( pc ) ) ), c_t_encumb ? c_red : c_light_gray ) ); + print_colored_text( w_menu, point( h_col1, line ), gray, gray, + string_format( "%s: %d %s", _( "Spell Level" ), sp.get_level(), + sp.is_max_level() ? _( "(MAX)" ) : "" ) ); + print_colored_text( w_menu, point( h_col2, line++ ), gray, gray, + string_format( "%s: %d", _( "Max Level" ), sp.get_max_level() ) ); - info_txt.emplace_back( ); - }; + print_colored_text( w_menu, point( h_col1, line ), gray, gray, + sp.colorized_fail_percent( g->u ) ); + print_colored_text( w_menu, point( h_col2, line++ ), gray, gray, + string_format( "%s: %d", _( "Difficulty" ), sp.get_difficulty() ) ); + + print_colored_text( w_menu, point( h_col1, line ), gray, gray, + string_format( "%s: %s", _( "Current Exp" ), colorize( to_string( sp.xp() ), light_green ) ) ); + print_colored_text( w_menu, point( h_col2, line++ ), gray, gray, + string_format( "%s: %s", _( "to Next Level" ), colorize( to_string( sp.exp_to_next_level() ), + light_green ) ) ); + + if( line <= win_height / 2 ) { + line++; + } + + const bool cost_encumb = energy_cost_encumbered( sp, g->u ); + std::string cost_string = cost_encumb ? _( "Casting Cost (impeded)" ) : _( "Casting Cost" ); + std::string energy_cur = sp.energy_source() == hp_energy ? "" : string_format( " (%s current)", + sp.energy_cur_string( g->u ) ); + if( !sp.can_cast( g->u ) ) { + cost_string = colorize( _( "Not Enough Energy" ), c_red ); + energy_cur = ""; + } + print_colored_text( w_menu, point( h_col1, line++ ), gray, gray, + string_format( "%s: %s %s%s", cost_string, + sp.energy_cost_string( g->u ), sp.energy_string(), energy_cur ) ); + const bool c_t_encumb = casting_time_encumbered( sp, g->u ); + print_colored_text( w_menu, point( h_col1, line++ ), gray, gray, colorize( + string_format( "%s: %s", c_t_encumb ? _( "Casting Time (impeded)" ) : _( "Casting Time" ), + moves_to_string( sp.casting_time( g->u ) ) ), + c_t_encumb ? c_red : c_light_gray ) ); + + if( line <= win_height * 3 / 4 ) { + line++; + } std::string targets; - if( sp.is_valid_target( spell_target::none ) ) { - targets = _( "self" ); + if( sp.is_valid_target( target_none ) ) { + targets = "self"; } else { targets = sp.enumerate_targets(); } - info_txt.emplace_back( - colorize( string_format( "%s: %s", _( "Valid Targets" ), targets ), c_light_gray ) ); - - info_txt.emplace_back( ); + print_colored_text( w_menu, point( h_col1, line++ ), gray, gray, + string_format( "%s: %s", _( "Valid Targets" ), _( targets ) ) ); - std::string target_ids; - target_ids = sp.list_targeted_monster_names(); - if( !target_ids.empty() ) { - for( const std::string &line : - foldstring( string_format( _( "Only affects the monsters: %s" ), target_ids ), width ) ) { - info_txt.emplace_back( colorize( line, c_light_gray ) ); - } - info_txt.emplace_back( ); + if( line <= win_height * 3 / 4 ) { + line++; } - const int damage = sp.damage( pc ); + const int damage = sp.damage(); std::string damage_string; std::string aoe_string; // if it's any type of attack spell, the stats are normal. - if( sp.effect() == "attack" ) { + if( fx == "target_attack" || fx == "projectile_attack" || fx == "cone_attack" || + fx == "line_attack" ) { if( damage > 0 ) { - std::string dot_string; - if( sp.damage_dot( pc ) != 0 ) { - //~ amount of damage per second, abbreviated - dot_string = string_format( _( ", %d/sec" ), sp.damage_dot( pc ) ); - } - damage_string = string_format( "%s: %s %s%s", _( "Damage" ), sp.damage_string( pc ), - sp.damage_type_string(), dot_string ); - damage_string = colorize( damage_string, sp.damage_type_color() ); + damage_string = string_format( "%s: %s %s", _( "Damage" ), colorize( sp.damage_string(), + sp.damage_type_color() ), + colorize( sp.damage_type_string(), sp.damage_type_color() ) ); } else if( damage < 0 ) { - damage_string = string_format( "%s: %s", _( "Healing" ), colorize( sp.damage_string( pc ), - c_light_green ) ); + damage_string = string_format( "%s: %s", _( "Healing" ), colorize( sp.damage_string(), + light_green ) ); } - if( sp.aoe( pc ) > 0 ) { - std::string aoe_string_temp = _( "Spell Radius" ); + if( sp.aoe() > 0 ) { + std::string aoe_string_temp = "Spell Radius"; std::string degree_string; - if( sp.shape() == spell_shape::cone ) { - aoe_string_temp = _( "Cone Arc" ); - degree_string = _( "degrees" ); - } else if( sp.shape() == spell_shape::line ) { - aoe_string_temp = _( "Line Width" ); - } - aoe_string = string_format( "%s: %d %s", aoe_string_temp, sp.aoe( pc ), degree_string ); - } - } else if( sp.effect() == "short_range_teleport" ) { - if( sp.aoe( pc ) > 0 ) { - aoe_string = string_format( "%s: %d", _( "Variance" ), sp.aoe( pc ) ); - } - } else if( sp.effect() == "spawn_item" ) { - if( sp.has_flag( spell_flag::SPAWN_GROUP ) ) { - // todo: more user-friendly presentation - damage_string = string_format( _( "Spawn item group %1$s %2$d times" ), sp.effect_data(), - sp.damage( pc ) ); - } else { - damage_string = string_format( "%s %d %s", _( "Spawn" ), sp.damage( pc ), - item::nname( itype_id( sp.effect_data() ), sp.damage( pc ) ) ); - } - } else if( sp.effect() == "summon" ) { - std::string monster_name = "FIXME"; - if( sp.has_flag( spell_flag::SPAWN_GROUP ) ) { - // TODO: Get a more user-friendly group name - if( MonsterGroupManager::isValidMonsterGroup( mongroup_id( sp.effect_data() ) ) ) { - monster_name = "from " + sp.effect_data(); - } else { - debugmsg( "Unknown monster group: %s", sp.effect_data() ); - } - } else { - monster_name = monster( mtype_id( sp.effect_data() ) ).get_name( ); - } - damage_string = string_format( "%s %d %s", _( "Summon" ), sp.damage( pc ), monster_name ); - aoe_string = string_format( "%s: %d", _( "Spell Radius" ), sp.aoe( pc ) ); - } else if( sp.effect() == "targeted_polymorph" ) { - std::string monster_name = sp.effect_data(); - if( sp.has_flag( spell_flag::POLYMORPH_GROUP ) ) { - // TODO: Get a more user-friendly group name - if( MonsterGroupManager::isValidMonsterGroup( mongroup_id( sp.effect_data() ) ) ) { - monster_name = _( "random creature" ); - } else { - debugmsg( "Unknown monster group: %s", sp.effect_data() ); + if( fx == "cone_attack" ) { + aoe_string_temp = "Cone Arc"; + degree_string = "degrees"; + } else if( fx == "line_attack" ) { + aoe_string_temp = "Line Width"; } - } else if( monster_name.empty() ) { - monster_name = _( "random creature" ); - } else { - monster_name = mtype_id( sp.effect_data() )->nname(); + aoe_string = string_format( "%s: %d %s", _( aoe_string_temp ), sp.aoe(), degree_string ); } - damage_string = string_format( _( "Targets under: %dhp become a %s" ), sp.damage( pc ), - monster_name ); - } else if( sp.effect() == "ter_transform" ) { - aoe_string = string_format( "%s: %s", _( "Spell Radius" ), sp.aoe_string( pc ) ); - } else if( sp.effect() == "banishment" ) { - damage_string = string_format( "%s: %s %s", _( "Damage" ), sp.damage_string( pc ), - sp.damage_type_string() ); - if( sp.aoe( pc ) > 0 ) { - aoe_string = string_format( _( "Spell Radius: %d" ), sp.aoe( pc ) ); + } else if( fx == "teleport_random" ) { + if( sp.aoe() > 0 ) { + aoe_string = string_format( "%s: %d", _( "Variance" ), sp.aoe() ); } + } else if( fx == "spawn_item" ) { + damage_string = string_format( "%s %d %s", _( "Spawn" ), sp.damage(), item::nname( sp.effect_data(), + sp.damage() ) ); + } else if( fx == "summon" ) { + damage_string = string_format( "%s %d %s", _( "Summon" ), sp.damage(), + _( monster( mtype_id( sp.effect_data() ) ).get_name( ) ) ); + aoe_string = string_format( "%s: %d", _( "Spell Radius" ), sp.aoe() ); + } else if( fx == "ter_transform" ) { + aoe_string = string_format( "%s: %s", _( "Spell Radius" ), sp.aoe_string() ); } - // Range / AOE in two columns - info_txt.emplace_back( colorize( string_format( "%s: %s", _( "Range" ), - sp.range( pc ) <= 0 ? _( "self" ) : std::to_string( sp.range( pc ) ) ), c_light_gray ) ); - info_txt.emplace_back( colorize( aoe_string, c_light_gray ) ); + print_colored_text( w_menu, point( h_col1, line ), gray, gray, damage_string ); + print_colored_text( w_menu, point( h_col2, line++ ), gray, gray, aoe_string ); - // One line for damage / healing / spawn / summon effect - info_txt.emplace_back( colorize( damage_string, c_light_gray ) ); - - // todo: damage over time here, when it gets implemented - - // Show duration for spells that endure - if( sp.duration( pc ) > 0 || sp.has_flag( spell_flag::PERMANENT ) || - sp.has_flag( spell_flag::PERMANENT_ALL_LEVELS ) ) { - info_txt.emplace_back( - colorize( string_format( "%s: %s", _( "Duration" ), sp.duration_string( pc ) ), c_light_gray ) ); - } - - if( sp.has_components() ) { - if( !sp.components().get_components().empty() ) { - for( const std::string &line : sp.components().get_folded_components_list( - width - 2, c_light_gray, pc.crafting_inventory( pc.pos(), 0, false ), return_true ) ) { - info_txt.emplace_back( line ); - } - } - if( !( sp.components().get_tools().empty() && sp.components().get_qualities().empty() ) ) { - for( const std::string &line : sp.components().get_folded_tools_list( - width - 2, c_light_gray, pc.crafting_inventory( pc.pos(), 0, false ) ) ) { - info_txt.emplace_back( line ); - } - } - } -} + print_colored_text( w_menu, point( h_col1, line++ ), gray, gray, + string_format( "%s: %s", _( "Range" ), sp.range() <= 0 ? _( "self" ) : to_string( sp.range() ) ) ); -void spellcasting_callback::draw_spell_info( const uilist *menu ) -{ - const int h_offset = menu->w_width - menu->pad_right + 1; - int row = 1; - nc_color clr = c_light_gray; + // todo: damage over time here, when it gets implemeted - for( int line = scroll_pos; row < menu->w_height - 1 && - line < static_cast( info_txt.size() ); row++, line++ ) { - print_colored_text( menu->window, point( h_offset + 1, row ), clr, c_light_gray, info_txt[line] ); - } + print_colored_text( w_menu, point( h_col1, line++ ), gray, gray, sp.duration() <= 0 ? "" : + string_format( "%s: %s", _( "Duration" ), sp.duration_string() ) ); } bool known_magic::set_invlet( const spell_id &sp, int invlet, const std::set &used_invlets ) @@ -2742,27 +1683,13 @@ void known_magic::rem_invlet( const spell_id &sp ) invlets.erase( sp ); } -void known_magic::toggle_favorite( const spell_id &sp ) -{ - if( favorites.count( sp ) > 0 ) { - favorites.erase( sp ); - } else { - favorites.emplace( sp ); - } -} - -bool known_magic::is_favorite( const spell_id &sp ) -{ - return favorites.count( sp ) > 0; -} - int known_magic::get_invlet( const spell_id &sp, std::set &used_invlets ) { auto found = invlets.find( sp ); if( found != invlets.end() ) { return found->second; } - for( const std::pair &invlet_pair : invlets ) { + for( const std::pair &invlet_pair : invlets ) { used_invlets.emplace( invlet_pair.second ); } for( int i = 'a'; i <= 'z'; i++ ) { @@ -2786,93 +1713,44 @@ int known_magic::get_invlet( const spell_id &sp, std::set &used_invlets ) return 0; } -int known_magic::select_spell( Character &guy ) +int known_magic::select_spell( const player &p ) { // max width of spell names const int max_spell_name_length = get_spellname_max_width(); std::vector known_spells = get_spells(); uilist spell_menu; - spell_menu.w_height_setup = [&]() -> int { - return clamp( static_cast( known_spells.size() ), 38, TERMY * 9 / 5 ); - }; - const auto calc_width = []() -> int { - return std::max( 80, TERMX * 3 / 4 ); - }; - spell_menu.w_width_setup = calc_width; - spell_menu.pad_right_setup = [&]() -> int { - return calc_width() - max_spell_name_length - 5; - }; + spell_menu.w_height = clamp( static_cast( known_spells.size() ), 36, TERMY * 9 / 10 ); + spell_menu.w_width = std::max( 100, TERMX * 3 / 8 ); + spell_menu.w_x = ( TERMX - spell_menu.w_width ) / 2; + spell_menu.w_y = ( TERMY - spell_menu.w_height ) / 2; + spell_menu.pad_right = spell_menu.w_width - max_spell_name_length - 5; spell_menu.title = _( "Choose a Spell" ); - spell_menu.input_category = "SPELL_MENU"; - spell_menu.additional_actions.emplace_back( "CHOOSE_INVLET", translation() ); - spell_menu.additional_actions.emplace_back( "CAST_IGNORE", translation() ); - spell_menu.additional_actions.emplace_back( "SCROLL_UP_SPELL_MENU", translation() ); - spell_menu.additional_actions.emplace_back( "SCROLL_DOWN_SPELL_MENU", translation() ); - spell_menu.additional_actions.emplace_back( "SCROLL_FAVORITE", translation() ); spell_menu.hilight_disabled = true; spellcasting_callback cb( known_spells, casting_ignore ); spell_menu.callback = &cb; - spell_menu.add_category( "all", _( "All" ) ); - spell_menu.add_category( "favorites", _( "Favorites" ) ); - - std::vector> categories; - for( const spell *s : known_spells ) { - if( s->can_cast( guy ) && ( s->spell_class().is_valid() || s->spell_class() == trait_NONE ) ) { - const std::string spell_class_name = s->spell_class() == trait_NONE ? _( "Classless" ) : - s->spell_class().obj().name(); - categories.emplace_back( s->spell_class().str(), spell_class_name ); - std::sort( categories.begin(), categories.end(), []( const std::pair &a, - const std::pair &b ) { - return localized_compare( a.second, b.second ); - } ); - const auto itr = std::unique( categories.begin(), categories.end() ); - categories.erase( itr, categories.end() ); - } - } - for( std::pair &cat : categories ) { - spell_menu.add_category( cat.first, cat.second ); - } - - spell_menu.set_category_filter( [&guy, known_spells]( const uilist_entry & entry, - const std::string & key )->bool { - if( key == "all" ) - { - return true; - } else if( key == "favorites" ) - { - return guy.magic->is_favorite( known_spells[entry.retval]->id() ); - } - return ( known_spells[entry.retval]->spell_class().is_valid() || known_spells[entry.retval]->spell_class() == trait_NONE ) && known_spells[entry.retval]->spell_class().str() == key; - } ); - if( !favorites.empty() ) { - spell_menu.set_category( "favorites" ); - } else { - spell_menu.set_category( "all" ); - } std::set used_invlets{ cb.reserved_invlets }; for( size_t i = 0; i < known_spells.size(); i++ ) { - spell_menu.addentry( static_cast( i ), known_spells[i]->can_cast( guy ), + spell_menu.addentry( static_cast( i ), known_spells[i]->can_cast( p ), get_invlet( known_spells[i]->id(), used_invlets ), known_spells[i]->name() ); } - reflesh_favorite( &spell_menu, known_spells ); - spell_menu.query( true, -1, true ); + spell_menu.query(); casting_ignore = static_cast( spell_menu.callback )->casting_ignore; return spell_menu.ret; } -void known_magic::on_mutation_gain( const trait_id &mid, Character &guy ) +void known_magic::on_mutation_gain( const trait_id &mid, player &p ) { - for( const std::pair &sp : mid->spells_learned ) { - learn_spell( sp.first, guy, true ); + for( const std::pair &sp : mid->spells_learned ) { + learn_spell( sp.first, p, true ); spell &temp_sp = get_spell( sp.first ); for( int level = 0; level < sp.second; level++ ) { - temp_sp.gain_level( guy ); + temp_sp.gain_level(); } } } @@ -2898,11 +1776,11 @@ void spellbook_callback::add_spell( const spell_id &sp ) static std::string color_number( const int num ) { if( num > 0 ) { - return colorize( std::to_string( num ), c_light_green ); + return colorize( to_string( num ), c_light_green ); } else if( num < 0 ) { - return colorize( std::to_string( num ), c_light_red ); + return colorize( to_string( num ), c_light_red ); } else { - return colorize( std::to_string( num ), c_white ); + return colorize( to_string( num ), c_white ); } } @@ -2930,11 +1808,9 @@ static void draw_spellbook_info( const spell_type &sp, uilist *menu ) nc_color gray = c_light_gray; nc_color yellow = c_yellow; const spell fake_spell( sp.id ); - Character &pc = get_player_character(); - dialogue d( get_talker_for( pc ), nullptr ); const std::string spell_name = colorize( sp.name, c_light_green ); - const std::string spell_class = sp.spell_class == trait_NONE ? _( "Classless" ) : + const std::string spell_class = sp.spell_class == trait_id( "NONE" ) ? _( "Classless" ) : sp.spell_class->name(); print_colored_text( w, point( start_x, line ), gray, gray, spell_name ); print_colored_text( w, point( menu->pad_left - utf8_width( spell_class ) - 1, line++ ), yellow, @@ -2943,26 +1819,26 @@ static void draw_spellbook_info( const spell_type &sp, uilist *menu ) line += fold_and_print( w, point( start_x, line ), width, gray, "%s", sp.description ); line++; - mvwprintz( w, point( start_x, line ), c_light_gray, - string_format( "%s: %d", _( "Difficulty" ), static_cast( sp.difficulty.evaluate( d ) ) ) ); - mvwprintz( w, point( start_x + width / 2, line++ ), c_light_gray, - string_format( "%s: %d", _( "Max Level" ), static_cast( sp.max_level.evaluate( d ) ) ) ); + mvwprintz( w, point( start_x, line ), c_light_gray, string_format( "%s: %d", _( "Difficulty" ), + sp.difficulty ) ); + mvwprintz( w, point( start_x + width / 2, line++ ), c_light_gray, string_format( "%s: %d", + _( "Max Level" ), + sp.max_level ) ); const std::string fx = sp.effect_name; std::string damage_string; std::string aoe_string; bool has_damage_type = false; - if( fx == "attack" ) { + if( fx == "target_attack" || fx == "projectile_attack" || fx == "cone_attack" || + fx == "line_attack" ) { damage_string = _( "Damage" ); aoe_string = _( "AoE" ); - has_damage_type = sp.min_damage.evaluate( d ) > 0 && sp.max_damage.evaluate( d ) > 0; + has_damage_type = sp.min_damage > 0 && sp.max_damage > 0; } else if( fx == "spawn_item" || fx == "summon_monster" ) { damage_string = _( "Spawned" ); - } else if( fx == "targeted_polymorph" ) { - damage_string = _( "Threshold" ); } else if( fx == "recover_energy" ) { damage_string = _( "Recover" ); - } else if( fx == "short_range_teleport" ) { + } else if( fx == "teleport_random" ) { aoe_string = _( "Variance" ); } else if( fx == "area_pull" || fx == "area_push" || fx == "ter_transform" ) { aoe_string = _( "AoE" ); @@ -2985,74 +1861,67 @@ static void draw_spellbook_info( const spell_type &sp, uilist *menu ) left_justify( _( "per lvl" ), 7 ), //~ translation should not exceed 7 console cells left_justify( _( "max lvl" ), 7 ) ) ); + std::vector> rows; - const auto row = [&]( const std::string & label, const dbl_or_var & min_d, - const dbl_or_var & inc_d, const dbl_or_var & max_d, bool check_minmax = false ) { - const int min = static_cast( min_d.evaluate( d ) ); - const float inc = static_cast( inc_d.evaluate( d ) ); - const int max = static_cast( max_d.evaluate( d ) ); - if( check_minmax && ( min == 0 || max == 0 ) ) { - return; - } - mvwprintz( w, point( start_x, line ), c_light_gray, label ); - print_colored_text( w, point( start_x + 11, line ), gray, gray, color_number( min ) ); - print_colored_text( w, point( start_x + 19, line ), gray, gray, color_number( inc ) ); - print_colored_text( w, point( start_x + 27, line ), gray, gray, color_number( max ) ); - line++; - }; + if( sp.max_damage != 0 && sp.min_damage != 0 && !damage_string.empty() ) { + rows.emplace_back( damage_string, sp.min_damage, sp.damage_increment, sp.max_damage ); + } - if( !damage_string.empty() ) { - row( damage_string, sp.min_damage, sp.damage_increment, sp.max_damage, true ); + if( sp.max_range != 0 && sp.min_range != 0 ) { + rows.emplace_back( _( "Range" ), sp.min_range, sp.range_increment, sp.max_range ); } - row( _( "Range" ), sp.min_range, sp.range_increment, sp.max_range, true ); + if( sp.min_aoe != 0 && sp.max_aoe != 0 && !aoe_string.empty() ) { + rows.emplace_back( aoe_string, sp.min_aoe, sp.aoe_increment, sp.max_aoe ); + } - if( !aoe_string.empty() ) { - row( aoe_string, sp.min_aoe, sp.aoe_increment, sp.max_aoe, true ); + if( sp.min_duration != 0 && sp.max_duration != 0 ) { + rows.emplace_back( _( "Duration" ), sp.min_duration, sp.duration_increment, sp.max_duration ); } - row( _( "Duration" ), sp.min_duration, sp.duration_increment, sp.max_duration, true ); - row( _( "Cast Cost" ), sp.base_energy_cost, sp.energy_increment, sp.final_energy_cost, false ); - row( _( "Cast Time" ), sp.base_casting_time, sp.casting_time_increment, sp.final_casting_time, - false ); + rows.emplace_back( _( "Cast Cost" ), sp.base_energy_cost, sp.energy_increment, + sp.final_energy_cost ); + rows.emplace_back( _( "Cast Time" ), sp.base_casting_time, sp.casting_time_increment, + sp.final_casting_time ); + + for( std::tuple &row : rows ) { + mvwprintz( w, point( start_x, line ), c_light_gray, std::get<0>( row ) ); + print_colored_text( w, point( start_x + 11, line ), gray, gray, + color_number( std::get<1>( row ) ) ); + print_colored_text( w, point( start_x + 19, line ), gray, gray, + color_number( std::get<2>( row ) ) ); + print_colored_text( w, point( start_x + 27, line ), gray, gray, + color_number( std::get<3>( row ) ) ); + line++; + } } -void spellbook_callback::refresh( uilist *menu ) +void spellbook_callback::select( int entnum, uilist *menu ) { mvwputch( menu->window, point( menu->pad_left, 0 ), c_magenta, LINE_OXXX ); mvwputch( menu->window, point( menu->pad_left, menu->w_height - 1 ), c_magenta, LINE_XXOX ); for( int i = 1; i < menu->w_height - 1; i++ ) { mvwputch( menu->window, point( menu->pad_left, i ), c_magenta, LINE_XOXO ); } - if( menu->selected >= 0 && static_cast( menu->selected ) < spells.size() ) { - draw_spellbook_info( spells[menu->selected], menu ); - } - wnoutrefresh( menu->window ); -} - -bool fake_spell::is_valid() const -{ - return id.is_valid() && !id.is_empty(); + draw_spellbook_info( spells[entnum], menu ); } -void fake_spell::load( const JsonObject &jo ) +void fake_spell::load( JsonObject &jo ) { - mandatory( jo, false, "id", id ); - optional( jo, false, "hit_self", self, self_default ); - - optional( jo, false, "once_in", trigger_once_in, trigger_once_in_default ); - optional( jo, false, "message", trigger_message ); - optional( jo, false, "npc_message", npc_trigger_message ); + std::string temp_id; + mandatory( jo, false, "id", temp_id ); + id = spell_id( temp_id ); + optional( jo, false, "hit_self", self, false ); int max_level_int; optional( jo, false, "max_level", max_level_int, -1 ); if( max_level_int == -1 ) { - max_level = std::nullopt; + max_level = cata::nullopt; } else { max_level = max_level_int; } - optional( jo, false, "min_level", level, level_default ); + optional( jo, false, "min_level", level, 0 ); if( jo.has_string( "level" ) ) { - debugmsg( "level member for fake_spell was renamed to min_level. id: %s", id.c_str() ); + debugmsg( "level member for fake_spell was renamed to min_level. id: %s", temp_id ); } } @@ -3060,51 +1929,37 @@ void fake_spell::serialize( JsonOut &json ) const { json.start_object(); json.member( "id", id ); - json.member( "hit_self", self, self_default ); - json.member( "once_in", trigger_once_in, trigger_once_in_default ); - json.member( "max_level", max_level, max_level_default ); - json.member( "min_level", level, level_default ); + json.member( "hit_self", self ); + if( !max_level ) { + json.member( "max_level", -1 ); + } else { + json.member( "max_level", *max_level ); + } + json.member( "min_level", level ); json.end_object(); } -void fake_spell::deserialize( const JsonObject &data ) +void fake_spell::deserialize( JsonIn &jsin ) { + JsonObject data = jsin.get_object(); load( data ); } -spell fake_spell::get_spell( const Creature &caster, int min_level_override ) const +spell fake_spell::get_spell( int input_level ) const { spell sp( id ); - // the max level this spell will be. can be optionally limited - int spell_max_level = sp.get_max_level( caster ); - int spell_limiter = max_level ? *max_level : spell_max_level; - // level is the minimum level the fake_spell will output - min_level_override = std::max( min_level_override, level ); - if( min_level_override > spell_limiter ) { - // this override is for min level, and does not override max level - if( spell_limiter <= 0 ) { - spell_limiter = min_level_override; - } else { - min_level_override = spell_limiter; - } + int lvl = std::min( input_level, sp.get_max_level() ); + if( max_level ) { + lvl = std::min( lvl, *max_level ); } - // make sure max level is not lower then min level - spell_limiter = std::max( min_level_override, spell_limiter ); - // the "level" of the fake spell is the goal, but needs to be clamped to min and max - int level_of_spell = clamp( level, min_level_override, spell_limiter ); - if( level > spell_limiter ) { + if( level > lvl ) { debugmsg( "ERROR: fake spell %s has higher min_level than max_level", id.c_str() ); return sp; } - sp.set_exp( sp.exp_for_level( level_of_spell ) ); - - return sp; -} - -// intended for spells without casters -spell fake_spell::get_spell() const -{ - spell sp( id ); + lvl = clamp( std::max( lvl, level ), level, lvl ); + while( sp.get_level() < lvl ) { + sp.gain_level(); + } return sp; } @@ -3117,10 +1972,10 @@ void spell_events::notify( const cata::event &e ) spell_type spell_cast = spell_factory.obj( sid ); for( std::map::iterator it = spell_cast.learn_spells.begin(); it != spell_cast.learn_spells.end(); ++it ) { + std::string learn_spell_id = it->first; int learn_at_level = it->second; - const std::string learn_spell_id = it->first; - if( learn_at_level <= slvl && !get_player_character().magic->knows_spell( learn_spell_id ) ) { - get_player_character().magic->learn_spell( learn_spell_id, get_player_character() ); + if( learn_at_level == slvl ) { + g->u.magic.learn_spell( learn_spell_id, g->u ); spell_type spell_learned = spell_factory.obj( spell_id( learn_spell_id ) ); add_msg( _( "Your experience and knowledge in creating and manipulating magical energies to cast %s have opened your eyes to new possibilities, you can now cast %s." ), diff --git a/src/newcharacter.cpp b/src/newcharacter.cpp index 2ab15895c5dd5..69d5e85b9563d 100644 --- a/src/newcharacter.cpp +++ b/src/newcharacter.cpp @@ -1,109 +1,62 @@ #include "avatar.h" // IWYU pragma: associated -#include -#include #include -#include -#include -#include +#include +#include +#include #include +#include +#include +#include #include #include #include -#include #include -#include #include #include -#include -#include "achievement.h" #include "addiction.h" #include "bionics.h" -#include "calendar_ui.h" #include "cata_utility.h" #include "catacharset.h" -#include "character.h" -#include "character_martial_arts.h" -#include "city.h" -#include "color.h" -#include "cursesdef.h" -#include "enum_conversions.h" -#include "game_constants.h" -#include "input_context.h" -#include "inventory.h" -#include "item.h" +#include "game.h" +#include "ime.h" +#include "input.h" #include "json.h" -#include "localized_comparator.h" -#include "magic.h" -#include "magic_enchantment.h" -#include "make_static.h" #include "mapsharing.h" #include "martialarts.h" -#include "mission.h" -#include "mod_manager.h" #include "monster.h" #include "mutation.h" +#include "name.h" #include "options.h" #include "output.h" -#include "overmap_ui.h" #include "path_info.h" -#include "pimpl.h" -#include "player_difficulty.h" #include "profession.h" -#include "profession_group.h" -#include "proficiency.h" -#include "recipe.h" #include "recipe_dictionary.h" #include "rng.h" #include "scenario.h" #include "skill.h" -#include "skill_ui.h" #include "start_location.h" #include "string_formatter.h" #include "string_input_popup.h" -#include "text_snippets.h" #include "translations.h" -#include "type_id.h" #include "ui.h" -#include "ui_manager.h" -#include "units_utility.h" -#include "veh_type.h" #include "worldfactory.h" +#include "recipe.h" +#include "string_id.h" +#include "character.h" +#include "color.h" +#include "cursesdef.h" +#include "game_constants.h" +#include "inventory.h" +#include "optional.h" +#include "pimpl.h" +#include "type_id.h" -static const std::string flag_CHALLENGE( "CHALLENGE" ); -static const std::string flag_CITY_START( "CITY_START" ); -static const std::string flag_SECRET( "SECRET" ); - -static const std::string type_hair_style( "hair_style" ); -static const std::string type_skin_tone( "skin_tone" ); -static const std::string type_facial_hair( "facial_hair" ); -static const std::string type_eye_color( "eye_color" ); - -static const flag_id json_flag_auto_wield( "auto_wield" ); -static const flag_id json_flag_no_auto_equip( "no_auto_equip" ); - -static const json_character_flag json_flag_BIONIC_TOGGLED( "BIONIC_TOGGLED" ); - -static const matype_id style_none( "style_none" ); - -static const profession_group_id -profession_group_adult_basic_background( "adult_basic_background" ); - -static const trait_id trait_FACIAL_HAIR_NONE( "FACIAL_HAIR_NONE" ); -static const trait_id trait_SMELLY( "SMELLY" ); -static const trait_id trait_WEAKSCENT( "WEAKSCENT" ); -static const trait_id trait_XS( "XS" ); -static const trait_id trait_XXXL( "XXXL" ); - -// Wether or not to use Outfit (M) at character creation -static bool outfit = true; - -// Responsive screen behavior for small terminal sizes -static bool isWide = false; +struct points_left; // Colors used in this file: (Most else defaults to c_light_gray) -#define COL_SELECT h_white // Selected value +#define COL_STAT_ACT c_white // Selected stat #define COL_STAT_BONUS c_light_green // Bonus #define COL_STAT_NEUTRAL c_white // Neutral Property #define COL_STAT_PENALTY c_light_red // Penalty @@ -118,360 +71,220 @@ static bool isWide = false; #define COL_TR_BAD_OFF_PAS c_dark_gray // A toggled-off bad trait #define COL_TR_BAD_ON_PAS c_red // A toggled-on bad trait #define COL_TR_NEUT c_brown // Neutral trait descriptive text -#define COL_TR_NEUT_OFF_ACT c_light_gray // A toggled-off neutral trait +#define COL_TR_NEUT_OFF_ACT c_dark_gray // A toggled-off neutral trait #define COL_TR_NEUT_ON_ACT c_yellow // A toggled-on neutral trait #define COL_TR_NEUT_OFF_PAS c_dark_gray // A toggled-off neutral trait #define COL_TR_NEUT_ON_PAS c_brown // A toggled-on neutral trait #define COL_SKILL_USED c_green // A skill with at least one point #define COL_HEADER c_white // Captions, like "Profession items" +#define COL_NOTE_MAJOR c_green // Important note #define COL_NOTE_MINOR c_light_gray // Just regular note -static int skill_increment_cost( const Character &u, const skill_id &skill ); +#define HIGH_STAT 14 // The point after which stats cost double +#define MAX_STAT 30 // The point after which stats can not be increased further -class tab_manager -{ - std::vector &tab_names; - std::map> tab_map; - point window_pos; - public: - bool complete = false; - bool quit = false; - tab_list position; - - explicit tab_manager( std::vector &tab_names ) : tab_names( tab_names ), - position( tab_names ) { - } +#define NEWCHAR_TAB_MAX 6 // The ID of the rightmost tab - void draw( const catacurses::window &w ); - bool handle_input( const std::string &action, const input_context &ctxt ); - void set_up_tab_navigation( input_context &ctxt ); -}; +static int skill_increment_cost( const Character &u, const skill_id &skill ); -void tab_manager::draw( const catacurses::window &w ) -{ - std::pair, size_t> fitted_tabs = fit_tabs_to_width( getmaxx( w ), - position.cur_index(), tab_names ); - tab_map = draw_tabs( w, fitted_tabs.first, position.cur_index() - fitted_tabs.second, - fitted_tabs.second ); - window_pos = point( getbegx( w ), getbegy( w ) ); - draw_border_below_tabs( w ); +struct points_left { + int stat_points; + int trait_points; + int skill_points; - for( int i = 1; i < TERMX - 1; i++ ) { - mvwputch( w, point( i, 5 ), BORDER_COLOR, LINE_OXOX ); - } - mvwputch( w, point( 0, 5 ), BORDER_COLOR, LINE_XXXO ); // |- - mvwputch( w, point( TERMX - 1, 5 ), BORDER_COLOR, LINE_XOXX ); // -| -} + enum point_limit : int { + FREEFORM = 0, + ONE_POOL, + MULTI_POOL + } limit; -bool tab_manager::handle_input( const std::string &action, const input_context &ctxt ) -{ - if( action == "QUIT" && query_yn( _( "Return to main menu?" ) ) ) { - quit = true; - } else if( action == "PREV_TAB" ) { - position.prev(); - } else if( action == "NEXT_TAB" ) { - position.next(); - } else if( action == "SELECT" ) { - std::optional coord = ctxt.get_coordinates_text( catacurses::stdscr ); - if( coord.has_value() ) { - point local_coord = coord.value() + window_pos; - for( const auto &entry : tab_map ) { - if( entry.second.contains( local_coord ) ) { - position.set_index( entry.first ); - return true; - } - } - } - return false; - } else { - return false; + points_left() { + limit = MULTI_POOL; + init_from_options(); } - return true; -} - -void tab_manager::set_up_tab_navigation( input_context &ctxt ) -{ - ctxt.register_action( "PREV_TAB" ); - ctxt.register_action( "NEXT_TAB" ); - ctxt.register_action( "SELECT" ); - ctxt.register_action( "QUIT" ); -} -static int stat_point_pool() -{ - return 4 * 8 + get_option( "INITIAL_STAT_POINTS" ); -} -static int stat_points_used( const avatar &u ) -{ - int used = 0; - for( int stat : { - u.str_max, u.dex_max, u.int_max, u.per_max - } ) { - used += stat + std::max( 0, stat - HIGH_STAT ); + void init_from_options() { + stat_points = get_option( "INITIAL_STAT_POINTS" ); + trait_points = get_option( "INITIAL_TRAIT_POINTS" ); + skill_points = get_option( "INITIAL_SKILL_POINTS" ); } - return used; -} -static int trait_point_pool() -{ - return get_option( "INITIAL_TRAIT_POINTS" ); -} -static int trait_points_used( const avatar &u ) -{ - int used = 0; - for( trait_id cur_trait : u.get_mutations( true ) ) { - bool locked = get_scenario()->is_locked_trait( cur_trait ) - || u.prof->is_locked_trait( cur_trait ); - for( const profession *hobby : u.hobbies ) { - locked = locked || hobby->is_locked_trait( cur_trait ); + // Highest amount of points to spend on stats without points going invalid + int stat_points_left() const { + switch( limit ) { + case FREEFORM: + case ONE_POOL: + return stat_points + trait_points + skill_points; + case MULTI_POOL: + return std::min( trait_points_left(), + stat_points + std::min( 0, trait_points + skill_points ) ); } - if( locked ) { - // The starting traits granted by scenarios, professions and hobbies cost nothing - continue; - } - const mutation_branch &mdata = cur_trait.obj(); - used += mdata.points; - } - return used; -} -static int skill_point_pool() -{ - return get_option( "INITIAL_SKILL_POINTS" ); -} -static int skill_points_used( const avatar &u ) -{ - int scenario = get_scenario()->point_cost(); - int profession_points = u.prof->point_cost(); - int hobbies = 0; - for( const profession *hobby : u.hobbies ) { - hobbies += hobby->point_cost(); - } - int skills = 0; - for( const Skill &sk : Skill::skills ) { - static std::array < int, 1 + MAX_SKILL > costs = { 0, 1, 1, 2, 4, 6, 9, 12, 16, 20, 25 }; - int skill_level = u.get_skill_level( sk.ident() ); - skills += costs.at( std::min( skill_level, costs.size() - 1 ) ); + return 0; } - return scenario + profession_points + hobbies + skills; -} -static int point_pool_total() -{ - return stat_point_pool() + trait_point_pool() + skill_point_pool(); -} -static int points_used_total( const avatar &u ) -{ - return stat_points_used( u ) + trait_points_used( u ) + skill_points_used( u ); -} + int trait_points_left() const { + switch( limit ) { + case FREEFORM: + case ONE_POOL: + return stat_points + trait_points + skill_points; + case MULTI_POOL: + return stat_points + trait_points + std::min( 0, skill_points ); + } -static int has_unspent_points( const avatar &u ) -{ - return points_used_total( u ) < point_pool_total(); -} + return 0; + } -struct multi_pool { - // The amount of unspent points in the pool without counting the borrowed points - const int pure_stat_points, pure_trait_points, pure_skill_points; - // The amount of points awailable in a pool minus the points that are borrowed - // by lower pools plus the points that can be borrowed from higher pools - const int stat_points_left, trait_points_left, skill_points_left; - explicit multi_pool( const avatar &u ): - pure_stat_points( stat_point_pool() - stat_points_used( u ) ), - pure_trait_points( trait_point_pool() - trait_points_used( u ) ), - pure_skill_points( skill_point_pool() - skill_points_used( u ) ), - stat_points_left( pure_stat_points - + std::min( 0, pure_trait_points - + std::min( 0, pure_skill_points ) ) ), - trait_points_left( pure_stat_points + pure_trait_points + std::min( 0, pure_skill_points ) ), - skill_points_left( pure_stat_points + pure_trait_points + pure_skill_points ) - {} + int skill_points_left() const { + return stat_points + trait_points + skill_points; + } -}; + bool is_freeform() { + return limit == FREEFORM; + } -static int skill_points_left( const avatar &u, pool_type pool ) -{ - switch( pool ) { - case pool_type::MULTI_POOL: { - return multi_pool( u ).skill_points_left; - } - case pool_type::ONE_POOL: { - return point_pool_total() - points_used_total( u ); - } - case pool_type::TRANSFER: - case pool_type::FREEFORM: - return 0; + bool is_valid() { + return is_freeform() || + ( stat_points_left() >= 0 && trait_points_left() >= 0 && + skill_points_left() >= 0 ); } - return 0; -} -// Toggle this trait and all prereqs, removing upgrades on removal -void Character::toggle_trait_deps( const trait_id &tr, const std::string &variant ) -{ - static const int depth_max = 10; - const mutation_branch &mdata = tr.obj(); - if( mdata.category.empty() || mdata.startingtrait ) { - toggle_trait( tr, variant ); - } else if( !has_trait( tr ) ) { - int rc = 0; - while( !has_trait( tr ) && rc < depth_max ) { - const mutation_variant *chosen_var = variant.empty() ? nullptr : tr->variant( variant ); - mutate_towards( tr, chosen_var ); - rc++; - } - } else if( has_trait( tr ) ) { - for( const auto &addition : get_addition_traits( tr ) ) { - unset_mutation( addition ); - } - for( const auto &lower : get_lower_traits( tr ) ) { - unset_mutation( lower ); - } - unset_mutation( tr ); + bool has_spare() { + return !is_freeform() && is_valid() && skill_points_left() > 0; } - calc_mutation_levels(); -} -static std::string pools_to_string( const avatar &u, pool_type pool ) -{ - switch( pool ) { - case pool_type::MULTI_POOL: { - multi_pool p( u ); - bool is_valid = p.stat_points_left >= 0 && p.trait_points_left >= 0 && p.skill_points_left >= 0; + std::string to_string() { + if( limit == MULTI_POOL ) { return string_format( _( "Points left: %d%c%d%c%d=%d" ), - p.stat_points_left >= 0 ? "light_gray" : "red", p.pure_stat_points, - p.pure_trait_points >= 0 ? '+' : '-', - p.trait_points_left >= 0 ? "light_gray" : "red", std::abs( p.pure_trait_points ), - p.pure_skill_points >= 0 ? '+' : '-', - p.skill_points_left >= 0 ? "light_gray" : "red", std::abs( p.pure_skill_points ), - is_valid ? "light_gray" : "red", p.skill_points_left ); - } - case pool_type::ONE_POOL: { - return string_format( _( "Points left: %4d" ), point_pool_total() - points_used_total( u ) ); + stat_points_left() >= 0 ? "light_gray" : "red", stat_points, + trait_points >= 0 ? '+' : '-', + trait_points_left() >= 0 ? "light_gray" : "red", abs( trait_points ), + skill_points >= 0 ? '+' : '-', + skill_points_left() >= 0 ? "light_gray" : "red", abs( skill_points ), + is_valid() ? "light_gray" : "red", stat_points + trait_points + skill_points ); + } else if( limit == ONE_POOL ) { + return string_format( _( "Points left: %4d" ), skill_points_left() ); + } else { + return _( "Freeform" ); } - case pool_type::TRANSFER: - return _( "Character Transfer: No changes can be made." ); - case pool_type::FREEFORM: - return _( "Survivor" ); } - return "If you see this, this is a bug"; -} +}; -static void set_points( tab_manager &tabs, avatar &u, pool_type & ); -static void set_stats( tab_manager &tabs, avatar &u, pool_type ); -static void set_traits( tab_manager &tabs, avatar &u, pool_type ); -static void set_scenario( tab_manager &tabs, avatar &u, pool_type ); -static void set_profession( tab_manager &tabs, avatar &u, pool_type ); -static void set_hobbies( tab_manager &tabs, avatar &u, pool_type ); -static void set_skills( tab_manager &tabs, avatar &u, pool_type ); -static void set_description( tab_manager &tabs, avatar &you, bool allow_reroll, pool_type ); +enum struct tab_direction { + NONE, + FORWARD, + BACKWARD, + QUIT +}; -static std::optional query_for_template_name(); -static void reset_scenario( avatar &u, const scenario *scen ); +tab_direction set_points( const catacurses::window &w, avatar &u, points_left &points ); +tab_direction set_stats( const catacurses::window &w, avatar &u, points_left &points ); +tab_direction set_traits( const catacurses::window &w, avatar &u, points_left &points ); +tab_direction set_scenario( const catacurses::window &w, avatar &u, points_left &points, + tab_direction direction ); +tab_direction set_profession( const catacurses::window &w, avatar &u, points_left &points, + tab_direction direction ); +tab_direction set_skills( const catacurses::window &w, avatar &u, points_left &points ); +tab_direction set_description( const catacurses::window &w, avatar &you, bool allow_reroll, + points_left &points ); + +static cata::optional query_for_template_name(); +static void save_template( const avatar &u, const std::string &name, const points_left &points ); +void reset_scenario( avatar &u, const scenario *scen ); void Character::pick_name( bool bUseDefault ) { if( bUseDefault && !get_option( "DEF_CHAR_NAME" ).empty() ) { name = get_option( "DEF_CHAR_NAME" ); } else { - name = SNIPPET.expand( male ? "" : "" ); + name = Name::generate( male ); } } -static matype_id choose_ma_style( const character_type type, const std::vector &styles, - const avatar &u ) +static matype_id choose_ma_style( const character_type type, const std::vector &styles ) { - if( type == character_type::NOW || type == character_type::FULL_RANDOM ) { + if( type == PLTYPE_NOW || type == PLTYPE_FULL_RANDOM ) { return random_entry( styles ); } if( styles.size() == 1 ) { return styles.front(); } - input_context ctxt( "MELEE_STYLE_PICKER", keyboard_mode::keycode ); + input_context ctxt( "MELEE_STYLE_PICKER" ); ctxt.register_action( "SHOW_DESCRIPTION" ); uilist menu; menu.allow_cancel = false; - menu.text = string_format( _( "Select a style.\n" - "\n" - "STR: %d, DEX: %d, " - "PER: %d, INT: %d\n" - "Press [%s] for technique details and compatible weapons.\n" ), - u.get_str(), u.get_dex(), u.get_per(), u.get_int(), + menu.text = string_format( _( "Select a style. (press %s for more info)" ), ctxt.get_desc( "SHOW_DESCRIPTION" ) ); ma_style_callback callback( 0, styles ); menu.callback = &callback; menu.input_category = "MELEE_STYLE_PICKER"; - menu.additional_actions.emplace_back( "SHOW_DESCRIPTION", translation() ); + menu.additional_actions.emplace_back( "SHOW_DESCRIPTION", "" ); menu.desc_enabled = true; - for( const matype_id &s : styles ) { - const martialart &style = s.obj(); + for( auto &s : styles ) { + auto &style = s.obj(); menu.addentry_desc( style.name.translated(), style.description.translated() ); } while( true ) { menu.query( true ); - const matype_id &selected = styles[menu.ret]; - if( query_yn( string_format( _( "Use the %s style?" ), selected.obj().name ) ) ) { + auto &selected = styles[menu.ret]; + if( query_yn( _( "Use this style?" ) ) ) { return selected; } } } -void avatar::randomize( const bool random_scenario, bool play_now ) +void avatar::randomize( const bool random_scenario, points_left &points, bool play_now ) { + const int max_trait_points = get_option( "MAX_TRAIT_POINTS" ); // Reset everything to the defaults to have a clean state. *this = avatar(); - bool gender_selection = one_in( 2 ); - male = gender_selection; - outfit = gender_selection; + + male = ( rng( 1, 100 ) > 50 ); if( !MAP_SHARING::isSharing() ) { play_now ? pick_name() : pick_name( true ); } else { name = MAP_SHARING::getUsername(); } - randomize_height(); - randomize_blood(); - randomize_heartrate(); bool cities_enabled = world_generator->active_world->WORLD_OPTIONS["CITY_SIZE"].getValue() != "0"; if( random_scenario ) { std::vector scenarios; - for( const scenario &scen : scenario::get_all() ) { - if( !scen.has_flag( flag_CHALLENGE ) && !scen.scen_is_blacklisted() && - ( !scen.has_flag( flag_CITY_START ) || cities_enabled ) && scen.can_pick().success() ) { + for( const auto &scen : scenario::get_all() ) { + if( !scen.has_flag( "CHALLENGE" ) && + ( !scen.has_flag( "CITY_START" ) || cities_enabled ) ) { scenarios.emplace_back( &scen ); } } - const scenario *selected_scenario = random_entry( scenarios ); - if( selected_scenario ) { - set_scenario( selected_scenario ); - } else { - debugmsg( "Failed randomizing sceario - no entries matching requirements." ); - } + g->scen = random_entry( scenarios ); + } else if( !cities_enabled ) { + static const string_id wilderness_only_scenario( "wilderness" ); + g->scen = &wilderness_only_scenario.obj(); } - prof = get_scenario()->weighted_random_profession(); - init_age = rng( this->prof->age_lower, this->prof->age_upper ); - starting_city = std::nullopt; - world_origin = std::nullopt; - random_start_location = true; + prof = g->scen->weighted_random_profession(); + start_location = g->scen->random_start_location(); str_max = rng( 6, HIGH_STAT - 2 ); dex_max = rng( 6, HIGH_STAT - 2 ); int_max = rng( 6, HIGH_STAT - 2 ); per_max = rng( 6, HIGH_STAT - 2 ); - - set_body(); - randomize_hobbies(); + points.stat_points = points.stat_points - str_max - dex_max - int_max - per_max; + points.skill_points = points.skill_points - prof->point_cost() - g->scen->point_cost(); + // The default for each stat is 8, and that default does not cost any points. + // Values below give points back, values above require points. The line above has removed + // to many points, therefore they are added back. + points.stat_points += 8 * 4; int num_gtraits = 0; int num_btraits = 0; int tries = 0; - add_traits(); // adds mandatory profession/scenario traits. - for( const trait_id &mut : get_mutations() ) { - const mutation_branch &mut_info = mut.obj(); + add_traits( points ); // adds mandatory profession/scenario traits. + for( const auto &mut : my_mutations ) { + const mutation_branch &mut_info = mut.first.obj(); if( mut_info.profession ) { continue; } @@ -485,12 +298,10 @@ void avatar::randomize( const bool random_scenario, bool play_now ) } /* The loops variable is used to prevent the algorithm running in an infinite loop */ - for( int loops = 0; loops <= 100000; loops++ ) { - multi_pool p( *this ); - bool is_valid = p.stat_points_left >= 0 && p.trait_points_left >= 0 && p.skill_points_left >= 0; - if( is_valid && rng( -3, 20 ) <= p.skill_points_left ) { - break; - } + unsigned int loops = 0; + + while( loops <= 100000 && ( !points.is_valid() || rng( -3, 20 ) > points.skill_points_left() ) ) { + loops++; trait_id rn; if( num_btraits < max_trait_points && one_in( 3 ) ) { tries = 0; @@ -501,7 +312,8 @@ void avatar::randomize( const bool random_scenario, bool play_now ) tries < 5 ); if( tries < 5 && !has_conflicting_trait( rn ) ) { - toggle_trait_deps( rn ); + toggle_trait( rn ); + points.trait_points -= rn->points; num_btraits -= rn->points; } } else { @@ -509,33 +321,35 @@ void avatar::randomize( const bool random_scenario, bool play_now ) case 1: if( str_max > 5 ) { str_max--; + points.stat_points++; } break; case 2: if( dex_max > 5 ) { dex_max--; + points.stat_points++; } break; case 3: if( int_max > 5 ) { int_max--; + points.stat_points++; } break; case 4: if( per_max > 5 ) { per_max--; + points.stat_points++; } break; } } } - for( int loops = 0; - has_unspent_points( *this ) && loops <= 100000; - loops++ ) { - multi_pool p( *this ); - const bool allow_stats = p.stat_points_left > 0; - const bool allow_traits = p.trait_points_left > 0 && num_gtraits < max_trait_points; + loops = 0; + while( points.has_spare() && loops <= 100000 ) { + const bool allow_stats = points.stat_points_left() > 0; + const bool allow_traits = points.trait_points_left() > 0 && num_gtraits < max_trait_points; int r = rng( 1, 9 ); trait_id rn; switch( r ) { @@ -545,10 +359,11 @@ void avatar::randomize( const bool random_scenario, bool play_now ) case 4: if( allow_traits ) { rn = random_good_trait(); - const mutation_branch &mdata = rn.obj(); - if( !has_trait( rn ) && p.trait_points_left >= mdata.points && + auto &mdata = rn.obj(); + if( !has_trait( rn ) && points.trait_points_left() >= mdata.points && num_gtraits + mdata.points <= max_trait_points && !has_conflicting_trait( rn ) ) { - toggle_trait_deps( rn ); + toggle_trait( rn ); + points.trait_points -= mdata.points; num_gtraits += mdata.points; } break; @@ -558,16 +373,40 @@ void avatar::randomize( const bool random_scenario, bool play_now ) if( allow_stats ) { switch( rng( 1, 4 ) ) { case 1: - str_max++; + if( str_max < HIGH_STAT ) { + str_max++; + points.stat_points--; + } else if( points.stat_points_left() >= 2 && str_max < MAX_STAT ) { + str_max++; + points.stat_points = points.stat_points - 2; + } break; case 2: - dex_max++; + if( dex_max < HIGH_STAT ) { + dex_max++; + points.stat_points--; + } else if( points.stat_points_left() >= 2 && dex_max < MAX_STAT ) { + dex_max++; + points.stat_points = points.stat_points - 2; + } break; case 3: - int_max++; + if( int_max < HIGH_STAT ) { + int_max++; + points.stat_points--; + } else if( points.stat_points_left() >= 2 && int_max < MAX_STAT ) { + int_max++; + points.stat_points = points.stat_points - 2; + } break; case 4: - per_max++; + if( per_max < HIGH_STAT ) { + per_max++; + points.stat_points--; + } else if( points.stat_points_left() >= 2 && per_max < MAX_STAT ) { + per_max++; + points.stat_points = points.stat_points - 2; + } break; } break; @@ -580,1057 +419,644 @@ void avatar::randomize( const bool random_scenario, bool play_now ) const skill_id aSkill = Skill::random_skill(); const int level = get_skill_level( aSkill ); - if( level < p.skill_points_left && level < MAX_SKILL && loops > 10000 ) { + if( level < points.skill_points_left() && level < MAX_SKILL && ( level <= MAX_SKILL || + loops > 10000 ) ) { + points.skill_points -= skill_increment_cost( *this, aSkill ); // For balance reasons, increasing a skill from level 0 gives you 1 extra level for free set_skill_level( aSkill, ( level == 0 ? 2 : level + 1 ) ); } break; } + loops++; } - - randomize_cosmetics(); - - // Restart cardio accumulator - reset_cardio_acc(); -} - -void avatar::randomize_cosmetics() -{ - randomize_cosmetic_trait( type_hair_style ); - randomize_cosmetic_trait( type_skin_tone ); - randomize_cosmetic_trait( type_eye_color ); - //arbitrary 50% chance to add beard to male characters - if( male && one_in( 2 ) ) { - randomize_cosmetic_trait( type_facial_hair ); - } else { - set_mutation( trait_FACIAL_HAIR_NONE ); - } -} - -void avatar::add_profession_items() -{ - // Our profession should not be a hobby - if( prof->is_hobby() ) { - return; - } - - std::list prof_items = prof->items( outfit, get_mutations() ); - - for( item &it : prof_items ) { - if( it.has_flag( STATIC( flag_id( "WET" ) ) ) ) { - it.active = true; - it.item_counter = 450; // Give it some time to dry off - } - - // TODO: debugmsg if food that isn't a seed is inedible - if( it.has_flag( json_flag_no_auto_equip ) ) { - it.unset_flag( json_flag_no_auto_equip ); - inv->push_back( it ); - } else if( it.has_flag( json_flag_auto_wield ) ) { - it.unset_flag( json_flag_auto_wield ); - if( !has_wield_conflicts( it ) ) { - wield( it ); - } else { - inv->push_back( it ); - } - } else if( it.is_armor() ) { - if( can_wear( it ).success() ) { - wear_item( it, false, false ); - } else { - inv->push_back( it ); - } - } else { - inv->push_back( it ); - } - - if( it.is_book() ) { - items_identified.insert( it.typeId() ); - } - } - - recalc_sight_limits(); - calc_encumbrance(); -} - -static int calculate_cumulative_experience( int level ) -{ - int sum = 0; - - while( level > 0 ) { - sum += 10000 * level * level; - level--; - } - - return sum; } bool avatar::create( character_type type, const std::string &tempname ) { - set_wielded_item( item() ); + weapon = item( "null", 0 ); prof = profession::generic(); - set_scenario( scenario::generic() ); - - const bool interactive = type != character_type::NOW && - type != character_type::FULL_RANDOM; - - std::vector character_tabs = { - _( "POINTS" ), - _( "SCENARIO" ), - _( "PROFESSION" ), - //~ Not scenery/backdrop, but previous life up to this point - _( "BACKGROUND" ), - _( "STATS" ), - _( "TRAITS" ), - _( "SKILLS" ), - _( "DESCRIPTION" ), - }; - tab_manager tabs( character_tabs ); + g->scen = scenario::generic(); - const std::string point_pool = get_option( "CHARACTER_POINT_POOLS" ); - pool_type pool = pool_type::FREEFORM; - if( point_pool == "multi_pool" ) { - // if using legacy multipool only set it to that - pool = pool_type::MULTI_POOL; + catacurses::window w; + if( type != PLTYPE_NOW && type != PLTYPE_FULL_RANDOM ) { + w = catacurses::newwin( TERMY, TERMX, point_zero ); } + int tab = 0; + points_left points = points_left(); + switch( type ) { - case character_type::CUSTOM: - randomize_cosmetics(); + case PLTYPE_CUSTOM: break; - case character_type::RANDOM: - //random scenario, default name if exist - randomize( true ); - tabs.position.last(); + case PLTYPE_RANDOM: //random scenario, default name if exist + randomize( true, points ); + tab = NEWCHAR_TAB_MAX; break; - case character_type::NOW: - //default world, fixed scenario, random name - randomize( false, true ); + case PLTYPE_NOW: //default world, fixed scenario, random name + randomize( false, points, true ); break; - case character_type::FULL_RANDOM: - //default world, random scenario, random name - randomize( true, true ); + case PLTYPE_FULL_RANDOM: //default world, random scenario, random name + randomize( true, points, true ); break; - case character_type::TEMPLATE: - if( !load_template( tempname, /*out*/ pool ) ) { + case PLTYPE_TEMPLATE: + if( !load_template( tempname, points ) ) { return false; } - // We want to prevent recipes known by the template from being applied to the // new character. The recipe list will be rebuilt when entering the game. - // Except if it is a character transfer template - if( pool != pool_type::TRANSFER ) { - forget_all_recipes(); - } - tabs.position.last(); + learned_recipes->clear(); + tab = NEWCHAR_TAB_MAX; break; } - // Don't apply the default backgrounds on a template - if( type != character_type::TEMPLATE ) { - add_default_background(); - } - auto nameExists = [&]( const std::string & name ) { - return world_generator->active_world->save_exists( save_t::from_save_id( name ) ) && - !query_yn( _( "A save with the name '%s' already exists in this world.\n" - "Saving will overwrite the already existing character.\n\n" + return world_generator->active_world->save_exists( save_t::from_player_name( name ) ) && + !query_yn( _( "A character with the name '%s' already exists in this world.\n" + "Saving will override the already existing character.\n\n" "Continue anyways?" ), name ); }; - set_body(); - const bool allow_reroll = type == character_type::RANDOM; + + const bool allow_reroll = type == PLTYPE_RANDOM; + tab_direction result = tab_direction::QUIT; do { - if( !interactive ) { - // no window is created because "Play now" does not require any configuration + if( !w ) { + // assert( type == PLTYPE_NOW ); + // no window is created because "Play now" does not require any configuration if( nameExists( name ) ) { return false; } break; } - - if( pool == pool_type::TRANSFER ) { - tabs.position.last(); - } - - switch( tabs.position.cur_index() ) { + werase( w ); + wrefresh( w ); + switch( tab ) { case 0: - set_points( tabs, *this, /*out*/ pool ); + result = set_points( w, *this, points ); break; case 1: - set_scenario( tabs, *this, pool ); + result = set_scenario( w, *this, points, result ); break; case 2: - set_profession( tabs, *this, pool ); + result = set_profession( w, *this, points, result ); break; case 3: - set_hobbies( tabs, *this, pool ); + result = set_stats( w, *this, points ); break; case 4: - set_stats( tabs, *this, pool ); + result = set_traits( w, *this, points ); break; case 5: - set_traits( tabs, *this, pool ); + result = set_skills( w, *this, points ); break; case 6: - set_skills( tabs, *this, pool ); - break; - case 7: - set_description( tabs, *this, allow_reroll, pool ); + result = set_description( w, *this, allow_reroll, points ); break; } - if( tabs.quit ) { - return false; + switch( result ) { + case tab_direction::NONE: + break; + case tab_direction::FORWARD: + tab++; + break; + case tab_direction::BACKWARD: + tab--; + break; + case tab_direction::QUIT: + tab = -1; + break; } - } while( !tabs.complete ); - - if( pool == pool_type::TRANSFER ) { - return true; - } - - save_template( _( "Last Character" ), pool ); - - initialize( type ); - - return true; -} -void Character::set_skills_from_hobbies( bool no_override ) -{ - // 2 for an average person - float catchup_modifier = 1.0f + ( 2.0f * get_int() + get_per() ) / 24.0f; - // 1.2 for an average person, always a bit higher than base amount - float knowledge_modifier = 1.0f + get_int() / 40.0f; - // Grab skills from hobbies and train - for( const profession *profession : hobbies ) { - for( const profession::StartingSkill &e : profession->skills() ) { - if( no_override && get_skill_level( e.first ) != 0 ) { - continue; + if( !( tab >= 0 && tab <= NEWCHAR_TAB_MAX ) ) { + if( tab != -1 && nameExists( name ) ) { + tab = NEWCHAR_TAB_MAX; + } else { + break; } - // Train our skill - const int skill_xp_bonus = calculate_cumulative_experience( e.second ); - get_skill_level_object( e.first ).train( skill_xp_bonus, catchup_modifier, - knowledge_modifier, true ); } - } -} -void Character::set_proficiencies_from_hobbies() -{ - for( const profession *profession : hobbies ) { - for( const proficiency_id &proficiency : profession->proficiencies() ) { - // Do not duplicate proficiencies - if( !_proficiencies->has_learned( proficiency ) ) { - add_proficiency( proficiency ); - } - } - } -} + } while( true ); -void Character::set_bionics_from_hobbies() -{ - for( const profession *profession : hobbies ) { - for( const bionic_id &bio : profession->CBMs() ) { - if( has_bionic( bio ) && !bio->dupes_allowed ) { - return; - } else { - add_bionic( bio ); - } - } + if( tab < 0 ) { + return false; } -} -void Character::initialize( bool learn_recipes ) -{ + save_template( *this, _( "Last Character" ), points ); + recalc_hp(); + for( int i = 0; i < num_hp_parts; i++ ) { + hp_cur[i] = hp_max[i]; + } - if( has_trait( trait_SMELLY ) ) { + if( has_trait( trait_id( "SMELLY" ) ) ) { scent = 800; } - if( has_trait( trait_WEAKSCENT ) ) { + if( has_trait( trait_id( "WEAKSCENT" ) ) ) { scent = 300; } - set_wielded_item( item() ); + weapon = item( "null", 0 ); - // Grab skills from profession and increment level + // Grab the skills from the profession, if there are any // We want to do this before the recipes - for( const profession::StartingSkill &e : prof->skills() ) { + for( auto &e : prof->skills() ) { mod_skill_level( e.first, e.second ); } - set_skills_from_hobbies(); - // setup staring bank money cash = rng( -200000, 200000 ); - randomize_heartrate(); - //set stored kcal to a normal amount for your height - set_stored_kcal( get_healthy_kcal() ); - if( has_trait( trait_XS ) ) { - set_stored_kcal( std::floor( get_stored_kcal() / 5 ) ); + if( has_trait( trait_id( "XS" ) ) ) { + set_stored_kcal( 10000 ); + toggle_trait( trait_id( "XS" ) ); } - if( has_trait( trait_XXXL ) ) { - set_stored_kcal( std::floor( get_stored_kcal() * 5 ) ); + if( has_trait( trait_id( "XXXL" ) ) ) { + set_stored_kcal( 125000 ); + toggle_trait( trait_id( "XXXL" ) ); } - if( learn_recipes ) { - for( const auto &e : recipe_dict ) { - const recipe &r = e.second; - if( !r.is_practice() && !r.has_flag( flag_SECRET ) && !knows_recipe( &r ) && - has_recipe_requirements( r ) ) { - learn_recipe( &r ); - } + // Learn recipes + for( const auto &e : recipe_dict ) { + const auto &r = e.second; + if( !r.has_flag( "SECRET" ) && !knows_recipe( &r ) && has_recipe_requirements( r ) ) { + learn_recipe( &r ); } } - - std::vector prof_addictions = prof->addictions(); - for( const addiction &iter : prof_addictions ) { - addictions.push_back( iter ); + for( mtype_id elem : prof->pets() ) { + starting_pets.push_back( elem ); } + std::list prof_items = prof->items( male, get_mutations() ); - for( const profession *profession : hobbies ) { - std::vector hobby_addictions = profession->addictions(); - for( const addiction &iter : hobby_addictions ) { - addictions.push_back( iter ); + for( item &it : prof_items ) { + if( it.has_flag( "WET" ) ) { + it.active = true; + it.item_counter = 450; // Give it some time to dry off + } + // TODO: debugmsg if food that isn't a seed is inedible + if( it.has_flag( "no_auto_equip" ) ) { + it.unset_flag( "no_auto_equip" ); + inv.push_back( it ); + } else if( it.has_flag( "auto_wield" ) ) { + it.unset_flag( "auto_wield" ); + if( !is_armed() ) { + wield( it ); + } else { + inv.push_back( it ); + } + } else if( it.is_armor() ) { + // TODO: debugmsg if wearing fails + wear_item( it, false ); + } else { + inv.push_back( it ); + } + if( it.is_book() ) { + items_identified.insert( it.typeId() ); } } - for( const bionic_id &bio : prof->CBMs() ) { - add_bionic( bio ); + std::vector prof_addictions = prof->addictions(); + for( std::vector::const_iterator iter = prof_addictions.begin(); + iter != prof_addictions.end(); ++iter ) { + addictions.push_back( *iter ); } - set_bionics_from_hobbies(); + for( auto &bio : prof->CBMs() ) { + add_bionic( bio ); + } // Adjust current energy level to maximum set_power_level( get_max_power_level() ); - // Add profession proficiencies - for( const proficiency_id &pri : prof->proficiencies() ) { - add_proficiency( pri ); - } - - // Add profession recipes - for( const recipe_id &id : prof->recipes() ) { - const recipe &r = recipe_dictionary::get_craft( id->result() ); - learn_recipe( &r ); + for( const trait_id &t : get_base_traits() ) { + std::vector styles; + for( const matype_id &s : t->initial_ma_styles ) { + if( !martial_arts_data.has_martialart( s ) ) { + styles.push_back( s ); + } + } + if( !styles.empty() ) { + werase( w ); + wrefresh( w ); + const matype_id ma_type = choose_ma_style( type, styles ); + martial_arts_data.add_martialart( ma_type ); + martial_arts_data.set_style( ma_type ); + } } - // Add hobby proficiencies - set_proficiencies_from_hobbies(); - // Activate some mutations right from the start. for( const trait_id &mut : get_mutations() ) { - const mutation_branch &branch = mut.obj(); + const auto &branch = mut.obj(); if( branch.starts_active ) { my_mutations[mut].powered = true; } } + prof->learn_spells( *this ); + // Ensure that persistent morale effects (e.g. Optimist) are present at the start. apply_persistent_morale(); - // Restart cardio accumulator - reset_cardio_acc(); - - recalc_speed_bonus(); + return true; } -void avatar::initialize( character_type type ) +static void draw_character_tabs( const catacurses::window &w, const std::string &sTab ) { - this->as_character()->initialize(); - - for( const matype_id &ma : prof->ma_known() ) { - if( !martial_arts_data->has_martialart( ma ) ) { - martial_arts_data->add_martialart( ma ); - } - } - - if( !prof->ma_choices().empty() ) { - for( int i = 0; i < prof->ma_choice_amount; i++ ) { - std::vector styles; - for( const matype_id &ma : prof->ma_choices() ) { - if( !martial_arts_data->has_martialart( ma ) ) { - styles.push_back( ma ); - } - } - if( !styles.empty() ) { - const matype_id ma_type = choose_ma_style( type, styles, *this ); - martial_arts_data->add_martialart( ma_type ); - } else { - break; - } - } - } - - for( const trait_id &t : get_base_traits() ) { - std::vector styles; - for( const matype_id &s : t->initial_ma_styles ) { - if( !martial_arts_data->has_martialart( s ) ) { - styles.push_back( s ); - } - } - if( !styles.empty() ) { - const matype_id ma_type = choose_ma_style( type, styles, *this ); - martial_arts_data->add_martialart( ma_type ); - } - } - - // Select a random known style - std::vector selectable_styles = martial_arts_data->get_known_styles( false ); - if( !selectable_styles.empty() ) { - std::vector::iterator it_max_priority = std::max_element( selectable_styles.begin(), - selectable_styles.end(), []( const matype_id & a, const matype_id & b ) { - return a->priority < b->priority; - } ); - int max_priority = ( *it_max_priority )->priority; - selectable_styles.erase( std::remove_if( selectable_styles.begin(), - selectable_styles.end(), [max_priority]( const matype_id & style ) { - return style->priority != max_priority; - } ), selectable_styles.end() ); - } - martial_arts_data->set_style( random_entry( selectable_styles, style_none ) ); - - for( const mtype_id &elem : prof->pets() ) { - starting_pets.push_back( elem ); - } - - if( get_scenario()->vehicle() != vproto_id::NULL_ID() ) { - starting_vehicle = get_scenario()->vehicle(); - } else { - starting_vehicle = prof->vehicle(); - } + std::vector tab_captions = { + _( "POINTS" ), + _( "SCENARIO" ), + _( "PROFESSION" ), + _( "STATS" ), + _( "TRAITS" ), + _( "SKILLS" ), + _( "DESCRIPTION" ), + }; - prof->learn_spells( *this ); + draw_tabs( w, tab_captions, sTab ); + draw_border_below_tabs( w ); - // Also learn spells from hobbies - for( const profession *profession : hobbies ) { - profession->learn_spells( *this ); + for( int i = 1; i < TERMX - 1; i++ ) { + mvwputch( w, point( i, 4 ), BORDER_COLOR, LINE_OXOX ); } - + mvwputch( w, point( 0, 4 ), BORDER_COLOR, LINE_XXXO ); // |- + mvwputch( w, point( TERMX - 1, 4 ), BORDER_COLOR, LINE_XOXX ); // -| } - -static void draw_points( const catacurses::window &w, pool_type pool, const avatar &u, - int netPointCost = 0 ) +static void draw_points( const catacurses::window &w, points_left &points, int netPointCost = 0 ) { // Clear line (except borders) mvwprintz( w, point( 2, 3 ), c_black, std::string( getmaxx( w ) - 3, ' ' ) ); - mvwprintz( w, point( 2, 4 ), c_black, std::string( getmaxx( w ) - 3, ' ' ) ); - std::string points_msg = pools_to_string( u, pool ); + std::string points_msg = points.to_string(); int pMsg_length = utf8_width( remove_color_tags( points_msg ), true ); nc_color color = c_light_gray; print_colored_text( w, point( 2, 3 ), color, c_light_gray, points_msg ); - if( pool != pool_type::FREEFORM ) { - if( netPointCost > 0 ) { - mvwprintz( w, point( pMsg_length + 2, 3 ), c_red, " (-%d)", std::abs( netPointCost ) ); - } else if( netPointCost < 0 ) { - mvwprintz( w, point( pMsg_length + 2, 3 ), c_green, " (+%d)", std::abs( netPointCost ) ); - } + if( netPointCost > 0 ) { + mvwprintz( w, point( pMsg_length + 2, 3 ), c_red, "(-%d)", std::abs( netPointCost ) ); + } else if( netPointCost < 0 ) { + mvwprintz( w, point( pMsg_length + 2, 3 ), c_green, "(+%d)", std::abs( netPointCost ) ); } - print_colored_text( w, point( 2, 4 ), color, c_light_gray, - player_difficulty::getInstance().difficulty_to_string( u ) ); } template -static void draw_filter_and_sorting_indicators( const catacurses::window &w, - const input_context &ctxt, const std::string_view filterstring, const Compare &sorter ) -{ - const char *const sort_order = sorter.sort_by_points ? _( "default" ) : _( "name" ); - const std::string sorting_indicator = string_format( "[%1$s] %2$s: %3$s", - colorize( ctxt.get_desc( "SORT" ), c_green ), _( "sort" ), - sort_order ); - const std::string filter_indicator = - filterstring.empty() - ? string_format( _( "[%s] filter" ), colorize( ctxt.get_desc( "FILTER" ), c_green ) ) - : std::string( filterstring ); - nc_color current_color = BORDER_COLOR; - print_colored_text( w, point( 2, getmaxy( w ) - 1 ), current_color, BORDER_COLOR, - string_format( "<%1s>-<%2s>", sorting_indicator, filter_indicator ) ); -} - -static const char *g_switch_msg( const avatar &u ) -{ - return u.male ? - //~ Gender switch message. 1s - change key name, 2s - profession name. - _( "Identity: %2$s (male) (press %1$s to switch)" ) - : - //~ Gender switch message. 1s - change key name, 2s - profession name. - _( "Identity: %2$s (female) (press %1$s to switch)" ); -} - -static const char *dress_switch_msg() +void draw_sorting_indicator( const catacurses::window &w_sorting, const input_context &ctxt, + Compare sorter ) { - return outfit ? - //~ Outfit switch message. 1s - change key name. - _( "Outfit: male (press %1$s to change)" ) : - //~ Outfit switch message. 1s - change key name. - _( "Outfit: female (press %1$s to change)" ); + const auto sort_order = sorter.sort_by_points ? _( "points" ) : _( "name" ); + const auto sort_text = string_format( + _( "Sort by: %1$s (Press %2$s to change)" ), + sort_order, ctxt.get_desc( "SORT" ) ); + fold_and_print( w_sorting, point_zero, ( TERMX / 2 ), c_light_gray, sort_text ); } -void set_points( tab_manager &tabs, avatar &u, pool_type &pool ) +tab_direction set_points( const catacurses::window &w, avatar &, points_left &points ) { - const int iSecondColumn = 31; - const int iHeaderHeight = 6; - // guessing most likely, but it doesn't matter, it will be recalculated if wrong - int iHelpHeight = 3; - const bool screen_reader_mode = get_option( "SCREEN_READER_MODE" ); + tab_direction retval = tab_direction::NONE; + const int content_height = TERMY - 6; + catacurses::window w_description = catacurses::newwin( content_height, TERMX - 35, + point( 31 + getbegx( w ), 5 + getbegy( w ) ) ); - ui_adaptor ui; - catacurses::window w; - catacurses::window w_description; - const auto init_windows = [&]( ui_adaptor & ui ) { - const int freeWidth = TERMX - FULL_SCREEN_WIDTH; - isWide = freeWidth > 15; - w = catacurses::newwin( TERMY, TERMX, point_zero ); - w_description = catacurses::newwin( TERMY - iHeaderHeight - iHelpHeight - 1, - TERMX - iSecondColumn - 1, point( iSecondColumn, iHeaderHeight ) ); - ui.position_from_window( w ); - }; - init_windows( ui ); - ui.on_screen_resize( init_windows ); + draw_character_tabs( w, _( "POINTS" ) ); input_context ctxt( "NEW_CHAR_POINTS" ); - tabs.set_up_tab_navigation( ctxt ); - ctxt.register_navigate_ui_list(); + ctxt.register_cardinal(); + ctxt.register_action( "PREV_TAB" ); ctxt.register_action( "HELP_KEYBINDINGS" ); + ctxt.register_action( "NEXT_TAB" ); + ctxt.register_action( "QUIT" ); ctxt.register_action( "CONFIRM" ); const std::string point_pool = get_option( "CHARACTER_POINT_POOLS" ); - using point_limit_tuple = std::tuple; + using point_limit_tuple = std::tuple; std::vector opts; - const point_limit_tuple multi_pool = std::make_tuple( pool_type::MULTI_POOL, - _( "Legacy: Multiple pools" ), + const point_limit_tuple multi_pool = std::make_tuple( points_left::MULTI_POOL, + _( "Multiple pools" ), _( "Stats, traits and skills have separate point pools.\n" "Putting stat points into traits and skills is allowed and putting trait points into skills is allowed.\n" - "Scenarios and professions affect skill points.\n\n" - "This is a legacy mode. Point totals are no longer balanced." ) ); + "Scenarios and professions affect skill point pool" ) ); - const point_limit_tuple one_pool = std::make_tuple( pool_type::ONE_POOL, _( "Legacy: Single pool" ), - _( "Stats, traits and skills share a single point pool.\n\n" - "This is a legacy mode. Point totals are no longer balanced." ) ); + const point_limit_tuple one_pool = std::make_tuple( points_left::ONE_POOL, _( "Single pool" ), + _( "Stats, traits and skills share a single point pool." ) ); - const point_limit_tuple freeform = std::make_tuple( pool_type::FREEFORM, _( "Survivor" ), - _( "No point limits are enforced, create a character with the intention of telling a story or challenging yourself." ) ); + const point_limit_tuple freeform = std::make_tuple( points_left::FREEFORM, _( "Freeform" ), + _( "No point limits are enforced" ) ); if( point_pool == "multi_pool" ) { - opts = { { multi_pool } }; - } else if( point_pool == "story_teller" ) { - opts = { { freeform } }; + opts = {{ multi_pool }}; + } else if( point_pool == "no_freeform" ) { + opts = {{ multi_pool, one_pool }}; } else { - opts = { { freeform, multi_pool, one_pool } }; + opts = {{ multi_pool, one_pool, freeform }}; } int highlighted = 0; - ui.on_redraw( [&]( ui_adaptor & ui ) { - const std::string help_text = - ( isWide ? string_format( - _( "Press %s to view and alter keybindings.\n" - "Press %s or %s to select pool and " - "%s to confirm selection.\n" - "Press %s to go to the next tab or " - "%s to return to main menu." ), - ctxt.get_desc( "HELP_KEYBINDINGS" ), ctxt.get_desc( "UP" ), ctxt.get_desc( "DOWN" ), - ctxt.get_desc( "CONFIRM" ), ctxt.get_desc( "NEXT_TAB" ), ctxt.get_desc( "QUIT" ) ) - : string_format( - _( "Press %s to view and alter keybindings." ), - ctxt.get_desc( "HELP_KEYBINDINGS" ) ) - ); - const int new_iHelpHeight = foldstring( help_text, getmaxx( w ) - 4 ).size(); - if( new_iHelpHeight != iHelpHeight ) { - iHelpHeight = new_iHelpHeight; - init_windows( ui ); + do { + if( highlighted < 0 ) { + highlighted = opts.size() - 1; + } else if( highlighted >= static_cast( opts.size() ) ) { + highlighted = 0; } - werase( w ); - tabs.draw( w ); - std::string title = std::get<1>( opts[highlighted] ); - std::string description = std::get<2>( opts[highlighted] ); - - if( screen_reader_mode ) { - // Include option title in option description, and say whether it's active - if( std::get<0>( opts[highlighted] ) == pool ) { - title.append( _( " - active" ) ); - } - description = title + "\n" + description; - } + const auto &cur_opt = opts[highlighted]; - draw_points( w, pool, u ); + draw_points( w, points ); // Clear the bottom of the screen. werase( w_description ); - const int opts_length = static_cast( opts.size() ); - for( int i = 0; i < opts_length; i++ ) { - nc_color color; - if( pool == std::get<0>( opts[i] ) ) { - color = highlighted == i ? hilite( c_light_green ) : c_green; - } else { - color = highlighted == i ? COL_SELECT : c_light_gray; - } - const point opt_pos( 2, 6 + i ); + for( int i = 0; i < static_cast( opts.size() ); i++ ) { + nc_color color = ( points.limit == std::get<0>( opts[i] ) ? COL_SKILL_USED : c_light_gray ); if( highlighted == i ) { - ui.set_cursor( w, opt_pos ); - } - if( screen_reader_mode ) { - // The list of options only clutters up the screen in screen reader mode - } else { - mvwprintz( w, opt_pos, color, std::get<1>( opts[i] ) ); + color = hilite( color ); } + mvwprintz( w, point( 2, 5 + i ), color, std::get<1>( opts[i] ) ); } fold_and_print( w_description, point_zero, getmaxx( w_description ), - COL_SKILL_USED, description ); - - // Helptext points tab - fold_and_print( w, point( 2, TERMY - foldstring( help_text, getmaxx( w ) - 4 ).size() - 1 ), - getmaxx( w ) - 4, COL_NOTE_MINOR, help_text ); - wnoutrefresh( w ); - wnoutrefresh( w_description ); - } ); + COL_SKILL_USED, std::get<2>( cur_opt ) ); - const int opts_length = static_cast( opts.size() ); - do { - ui_manager::redraw(); + wrefresh( w ); + wrefresh( w_description ); const std::string action = ctxt.handle_input(); - if( tabs.handle_input( action, ctxt ) ) { - break; // Tab has changed or user has quit the screen - } else if( navigate_ui_list( action, highlighted, 1, opts_length, true ) ) { + if( action == "DOWN" ) { + highlighted++; + } else if( action == "UP" ) { + highlighted--; + } else if( action == "PREV_TAB" && query_yn( _( "Return to main menu?" ) ) ) { + retval = tab_direction::BACKWARD; + } else if( action == "NEXT_TAB" ) { + retval = tab_direction::FORWARD; + } else if( action == "QUIT" && query_yn( _( "Return to main menu?" ) ) ) { + retval = tab_direction::QUIT; + } else if( action == "HELP_KEYBINDINGS" ) { + // Need to redraw since the help window obscured everything. + draw_character_tabs( w, _( "POINTS" ) ); } else if( action == "CONFIRM" ) { - const auto &cur_opt = opts[highlighted]; - pool = std::get<0>( cur_opt ); - } - } while( true ); -} - -static std::string assemble_stat_details( avatar &u, const unsigned char sel ) -{ - std::string description_str; - switch( sel ) { - case 0: { - u.recalc_hp(); - u.set_stored_kcal( u.get_healthy_kcal() ); - description_str = - string_format( _( "Base HP: %d" ), u.get_part_hp_max( bodypart_id( "head" ) ) ) - + string_format( _( "\nCarry weight: %.1f %s" ), convert_weight( u.weight_capacity() ), - weight_units() ) - + string_format( _( "\nResistance to knock down effect when hit: %.1f" ), u.stability_roll() ) - + string_format( _( "\nIntimidation skill: %i" ), u.intimidation() ) - + string_format( _( "\nMaximum oxygen: %i" ), u.get_oxygen_max() ) - + string_format( _( "\nShout volume: %i" ), u.get_shout_volume() ) - + string_format( _( "\nLifting strength: %i" ), u.get_lift_str() ) - + string_format( _( "\nMove cost while swimming: %i" ), u.swim_speed() ) - + colorize( - string_format( _( "\nBash damage bonus: %.1f" ), u.bonus_damage( false ) ), - COL_STAT_BONUS ) - + _( "\n\nAffects:" ) - + colorize( - _( "\n- Throwing range, accuracy, and damage" - "\n- Reload speed for weapons using muscle power to reload" - "\n- Pull strength of some mutations" - "\n- Resistance for being pulled or grabbed by some monsters" - "\n- Speed of corpses pulping" - "\n- Speed and effectiveness of prying things open, chopping wood, and mining" - "\n- Chance of escaping grabs and traps" - "\n- Power produced by muscle-powered vehicles" - "\n- Most aspects of melee combat" - "\n- Effectiveness of smashing furniture or terrain" - "\n- Resistance to many diseases and poisons" - "\n- Ability to drag heavy objects and grants bonus to speed when dragging them" - "\n- Ability to wield heavy weapons with one hand" - "\n- Ability to manage gun recoil" - "\n- Duration of action of various drugs and alcohol" ), - c_green ); - } - break; - - case 1: { - description_str = - colorize( - string_format( _( "Melee to-hit bonus: +%.2f" ), u.get_melee_hit_base() ) - + string_format( _( "\nThrowing penalty per target's dodge: +%d" ), - u.throw_dispersion_per_dodge( false ) ), - COL_STAT_BONUS ); - if( u.ranged_dex_mod() != 0 ) { - description_str += colorize( string_format( _( "\nRanged penalty: -%d" ), - std::abs( u.ranged_dex_mod() ) ), COL_STAT_PENALTY ); - } else { - description_str += "\n"; - } - description_str += - string_format( _( "\nDodge skill: %.f" ), u.get_dodge() ) - + string_format( _( "\nMove cost while swimming: %i" ), u.swim_speed() ) - + _( "\n\nAffects:" ) - + colorize( - _( "\n- Effectiveness of lockpicking" - "\n- Resistance for being grabbed by some monsters" - "\n- Chance of escaping grabs and traps" - "\n- Effectiveness of disarming traps" - "\n- Chance of success when manipulating with gun modifications" - "\n- Effectiveness of repairing and modifying clothes and armor" - "\n- Attack speed and chance of critical hits in melee combat" - "\n- Effectiveness of stealing" - "\n- Throwing speed" - "\n- Aiming speed" - "\n- Speed and effectiveness of chopping wood with powered tools" - "\n- Chance to avoid traps" - "\n- Chance to get better results when butchering corpses or cutting items" - "\n- Chance of avoiding cuts on sharp terrain" - "\n- Chance of losing control of vehicle when driving" - "\n- Chance of damaging melee weapon on attack" - "\n- Damage from falling" ), - c_green ); - } - break; - - case 2: { - const int read_spd = u.read_speed(); - description_str = - colorize( string_format( _( "Read times: %d%%" ), read_spd ), - ( read_spd == 100 ? COL_STAT_NEUTRAL : - ( read_spd < 100 ? COL_STAT_BONUS : COL_STAT_PENALTY ) ) ) - + string_format( _( "\nPersuade/lie skill: %i" ), u.persuade_skill() ) - + colorize( string_format( _( "\nCrafting bonus: %2d%%" ), u.get_int() ), - COL_STAT_BONUS ) - + _( "\n\nAffects:" ) - + colorize( - _( "\n- Speed of 'catching up' practical experience to theoretical knowledge" - "\n- Detection and disarming traps" - "\n- Chance of success when installing bionics" - "\n- Chance of success when manipulating with gun modifications" - "\n- Chance to learn a recipe when crafting from a book" - "\n- Chance to learn martial arts techniques when using CQB bionic" - "\n- Chance of hacking computers and card readers" - "\n- Chance of successful robot reprogramming" - "\n- Chance of successful decrypting memory cards" - "\n- Chance of bypassing vehicle security system" - "\n- Chance to get better results when disassembling items" - "\n- Chance of being paralyzed by fear attack" ), - c_green ); - } - break; - - case 3: { - if( u.ranged_per_mod() > 0 ) { - description_str = - colorize( string_format( _( "Aiming penalty: -%d" ), u.ranged_per_mod() ), - COL_STAT_PENALTY ); - } - description_str += - string_format( _( "\nPersuade/lie skill: %i" ), u.persuade_skill() ) - + _( "\n\nAffects:" ) - + colorize( - _( "\n- Speed of 'catching up' practical experience to theoretical knowledge" - "\n- Time needed for safe cracking" - "\n- Sight distance on game map and overmap" - "\n- Effectiveness of stealing" - "\n- Throwing accuracy" - "\n- Chance of losing control of vehicle when driving" - "\n- Chance of spotting camouflaged creatures" - "\n- Effectiveness of lockpicking" - "\n- Effectiveness of foraging" - "\n- Precision when examining wounds and using first aid skill" - "\n- Detection and disarming traps" - "\n- Morale bonus when playing a musical instrument" - "\n- Effectiveness of repairing and modifying clothes and armor" - "\n- Chance of critical hits in melee combat" ), - c_green ); + points.limit = std::get<0>( cur_opt ); } - break; - } - return description_str; -} + } while( retval == tab_direction::NONE ); -static std::string assemble_stat_help( const input_context &ctxt ) -{ - return string_format( - _( "Press %s to view and alter keybindings.\n" - "Press %s / %s to select stat.\n" - "Press %s to increase stat or " - "%s to decrease stat.\n" - "Press %s to go to the next tab or " - "%s to return to the previous tab." ), - ctxt.get_desc( "HELP_KEYBINDINGS" ), ctxt.get_desc( "UP" ), ctxt.get_desc( "DOWN" ), - ctxt.get_desc( "RIGHT" ), ctxt.get_desc( "LEFT" ), - ctxt.get_desc( "NEXT_TAB" ), ctxt.get_desc( "PREV_TAB" ) ); + return retval; } -/** Handle the stats tab of the character generation menu */ -void set_stats( tab_manager &tabs, avatar &u, pool_type pool ) +tab_direction set_stats( const catacurses::window &w, avatar &u, points_left &points ) { - // TODO: Move this out to a common header and eliminate other separate instances of these strings. - static const std::array stat_labels = { to_translation( "Strength" ), to_translation( "Dexterity" ), to_translation( "Intelligence" ), to_translation( "Perception" ) }; - const int max_stat_points = pool == pool_type::FREEFORM ? 20 : MAX_STAT; - const int min_stat_points = 4; - - unsigned char sel = 0; - - const bool screen_reader_mode = get_option( "SCREEN_READER_MODE" ); - std::string warning_text; // Used to move warnings from the header to the details pane - std::string last_stat; // Used to ensure text is read out when increasing/decreasing stats - - int iSecondColumn; - const int iHeaderHeight = 6; - // guessing most likely, but it doesn't matter, it will be recalculated if wrong - int iHelpHeight = 4; - - ui_adaptor ui; - catacurses::window w; - catacurses::window w_details_pane; - scrolling_text_view details( w_details_pane ); - bool details_recalc = true; - const auto init_windows = [&]( ui_adaptor & ui ) { - const int thirds = std::min( ( TERMX - 4 ) / 3, 38 ); // to allign scrollbar with the traits tab - iSecondColumn = std::max( thirds, utf8_width( pools_to_string( u, pool ), true ) + 2 ); - const size_t iContentHeight = TERMY - iHeaderHeight - iHelpHeight - 1; - w = catacurses::newwin( TERMY, TERMX, point_zero ); - w_details_pane = catacurses::newwin( iContentHeight, TERMX - iSecondColumn - 1, - point( iSecondColumn, iHeaderHeight ) ); - details_recalc = true; - ui.position_from_window( w ); - }; - init_windows( ui ); - ui.on_screen_resize( init_windows ); - + unsigned char sel = 1; + const int iSecondColumn = 27; input_context ctxt( "NEW_CHAR_STATS" ); - tabs.set_up_tab_navigation( ctxt ); - details.set_up_navigation( ctxt, scrolling_key_scheme::angle_bracket_scroll ); ctxt.register_cardinal(); + ctxt.register_action( "PREV_TAB" ); ctxt.register_action( "HELP_KEYBINDINGS" ); - + ctxt.register_action( "NEXT_TAB" ); + ctxt.register_action( "QUIT" ); + int read_spd; + catacurses::window w_description = catacurses::newwin( 8, TERMX - iSecondColumn - 1, + point( iSecondColumn + getbegx( w ), 6 + getbegy( w ) ) ); + // There is no map loaded currently, so any access to the map will + // fail (player::suffer, called from player::reset_stats), might access + // the map: + // There are traits that check/change the radioactivity on the map, + // that check if in sunlight... + // Setting the position to -1 ensures that the INBOUNDS check in + // map.cpp is triggered. This check prevents access to invalid position + // on the map (like -1,0) and instead returns a dummy default value. + u.setx( -1 ); u.reset(); - std::array stats = { &u.str_max, &u.dex_max, &u.int_max, &u.per_max }; - - ui.on_redraw( [&]( ui_adaptor & ui ) { - const std::string help_text = assemble_stat_help( ctxt ); - const int new_iHelpHeight = foldstring( help_text, getmaxx( w ) - 4 ).size(); - if( new_iHelpHeight != iHelpHeight ) { - iHelpHeight = new_iHelpHeight; - init_windows( ui ); - } + + do { werase( w ); - tabs.draw( w ); - mvwputch( w, point( iSecondColumn, iHeaderHeight - 1 ), BORDER_COLOR, LINE_OXXX ); // '┬' - // Helptext stats tab - fold_and_print( w, point( 2, TERMY - iHelpHeight - 1 ), getmaxx( w ) - 4, COL_NOTE_MINOR, - help_text ); - - const point opt_pos( 2, sel + iHeaderHeight ); - if( screen_reader_mode ) { - // This list only clutters up the screen in screen reader mode - } else { - for( int i = 0; i < 4; i++ ) { - mvwprintz( w, point( 2, i + iHeaderHeight ), i == sel ? COL_SELECT : c_light_gray, "%s:", - stat_labels[i].translated() ); - mvwprintz( w, point( 16, i + iHeaderHeight ), c_light_gray, "%2d", *stats[i] ); - } - } + draw_character_tabs( w, _( "STATS" ) ); + fold_and_print( w, point( 2, 16 ), getmaxx( w ) - 4, COL_NOTE_MINOR, + _( " %s / %s to select a statistic.\n" + " %s to increase the statistic.\n" + " %s to decrease the statistic." ), + ctxt.get_desc( "UP" ), ctxt.get_desc( "DOWN" ), + ctxt.get_desc( "RIGHT" ), ctxt.get_desc( "LEFT" ) + ); + + mvwprintz( w, point( 2, TERMY - 4 ), COL_NOTE_MAJOR, + _( "%s lets you view and alter keybindings." ), ctxt.get_desc( "HELP_KEYBINDINGS" ) ); + mvwprintz( w, point( 2, TERMY - 3 ), COL_NOTE_MAJOR, _( "%s takes you to the next tab." ), + ctxt.get_desc( "NEXT_TAB" ) ); + mvwprintz( w, point( 2, TERMY - 2 ), COL_NOTE_MAJOR, _( "%s returns you to the main menu." ), + ctxt.get_desc( "PREV_TAB" ) ); + + // This is description line, meaning its length excludes first column and border + const std::string clear_line( getmaxx( w ) - iSecondColumn - 1, ' ' ); + mvwprintz( w, point( iSecondColumn, 3 ), c_black, clear_line ); + for( int i = 6; i < 13; i++ ) { + mvwprintz( w, point( iSecondColumn, i ), c_black, clear_line ); + } + + draw_points( w, points ); + + mvwprintz( w, point( 2, 6 ), c_light_gray, _( "Strength:" ) ); + mvwprintz( w, point( 16, 6 ), c_light_gray, "%2d", u.str_max ); + mvwprintz( w, point( 2, 7 ), c_light_gray, _( "Dexterity:" ) ); + mvwprintz( w, point( 16, 7 ), c_light_gray, "%2d", u.dex_max ); + mvwprintz( w, point( 2, 8 ), c_light_gray, _( "Intelligence:" ) ); + mvwprintz( w, point( 16, 8 ), c_light_gray, "%2d", u.int_max ); + mvwprintz( w, point( 2, 9 ), c_light_gray, _( "Perception:" ) ); + mvwprintz( w, point( 16, 9 ), c_light_gray, "%2d", u.per_max ); - draw_points( w, pool, u ); - const point desc_line = point( iSecondColumn, 3 ); - warning_text = ""; - if( *stats[sel] <= min_stat_points ) { - //~ %s - stat - warning_text = string_format( _( "%s cannot be further decreased" ), - stat_labels[sel].translated() ); - } else if( *stats[sel] >= max_stat_points ) { - //~ %s - stat - warning_text = string_format( _( "%s cannot be further increased" ), - stat_labels[sel].translated() ); - } else if( *stats[sel] >= HIGH_STAT && pool != pool_type::FREEFORM ) { - //~ %s - stat - warning_text = string_format( _( "Increasing %s further costs 2 points" ), - stat_labels[sel].translated() ); - } - if( !warning_text.empty() && !screen_reader_mode ) { - nc_color dummy = c_red; - print_colored_text( w, desc_line, dummy, c_red, warning_text ); - } + werase( w_description ); + switch( sel ) { + case 1: + mvwprintz( w, point( 2, 6 ), COL_STAT_ACT, _( "Strength:" ) ); + mvwprintz( w, point( 16, 6 ), COL_STAT_ACT, "%2d", u.str_max ); + if( u.str_max >= HIGH_STAT ) { + mvwprintz( w, point( iSecondColumn, 3 ), c_light_red, + _( "Increasing Str further costs 2 points." ) ); + } + u.recalc_hp(); + mvwprintz( w_description, point_zero, COL_STAT_NEUTRAL, _( "Base HP: %d" ), u.hp_max[0] ); + // NOLINTNEXTLINE(cata-use-named-point-constants) + mvwprintz( w_description, point( 0, 1 ), COL_STAT_NEUTRAL, _( "Carry weight: %.1f %s" ), + convert_weight( u.weight_capacity() ), weight_units() ); + mvwprintz( w_description, point( 0, 2 ), COL_STAT_BONUS, _( "Melee damage bonus: %.1f" ), + u.bonus_damage( false ) ); + fold_and_print( w_description, point( 0, 4 ), getmaxx( w_description ) - 1, COL_STAT_NEUTRAL, + _( "Strength also makes you more resistant to many diseases and poisons, and makes actions which require brute force more effective." ) ); + break; - u.reset_stats(); - u.set_stored_kcal( u.get_healthy_kcal() ); - u.reset_bonuses(); // Removes pollution of stats by modifications appearing inside reset_stats(). Is reset_stats() even necessary in this context? - if( details_recalc ) { - std::string stat_details; - if( screen_reader_mode ) { - stat_details = string_format( "%s: %i\n", stat_labels[sel].translated(), *stats[sel] ); - if( !last_stat.empty() && !stat_details.empty() && last_stat[0] == stat_details[0] ) { - // Shift the text to force the screen reader to read it - stat_details = " " + stat_details; - } - last_stat = stat_details; - if( !warning_text.empty() ) { - stat_details.append( warning_text + "\n" ); - } - stat_details.append( assemble_stat_details( u, sel ) ); - } else { - stat_details = assemble_stat_details( u, sel ); - } - details.set_text( stat_details ); - details_recalc = false; - } + case 2: + mvwprintz( w, point( 2, 7 ), COL_STAT_ACT, _( "Dexterity:" ) ); + mvwprintz( w, point( 16, 7 ), COL_STAT_ACT, "%2d", u.dex_max ); + if( u.dex_max >= HIGH_STAT ) { + mvwprintz( w, point( iSecondColumn, 3 ), c_light_red, + _( "Increasing Dex further costs 2 points." ) ); + } + mvwprintz( w_description, point_zero, COL_STAT_BONUS, _( "Melee to-hit bonus: +%.2f" ), + u.get_hit_base() ); + // NOLINTNEXTLINE(cata-use-named-point-constants) + mvwprintz( w_description, point( 0, 1 ), COL_STAT_BONUS, + _( "Throwing penalty per target's dodge: +%d" ), + u.throw_dispersion_per_dodge( false ) ); + if( u.ranged_dex_mod() != 0 ) { + mvwprintz( w_description, point( 0, 2 ), COL_STAT_PENALTY, _( "Ranged penalty: -%d" ), + std::abs( u.ranged_dex_mod() ) ); + } + fold_and_print( w_description, point( 0, 4 ), getmaxx( w_description ) - 1, COL_STAT_NEUTRAL, + _( "Dexterity also enhances many actions which require finesse." ) ); + break; - wnoutrefresh( w ); - ui.set_cursor( w_details_pane, point_zero ); - details.draw( COL_STAT_NEUTRAL ); - } ); + case 3: + mvwprintz( w, point( 2, 8 ), COL_STAT_ACT, _( "Intelligence:" ) ); + mvwprintz( w, point( 16, 8 ), COL_STAT_ACT, "%2d", u.int_max ); + if( u.int_max >= HIGH_STAT ) { + mvwprintz( w, point( iSecondColumn, 3 ), c_light_red, + _( "Increasing Int further costs 2 points." ) ); + } + read_spd = u.read_speed( false ); + mvwprintz( w_description, point_zero, ( read_spd == 100 ? COL_STAT_NEUTRAL : + ( read_spd < 100 ? COL_STAT_BONUS : COL_STAT_PENALTY ) ), + _( "Read times: %d%%" ), read_spd ); + // NOLINTNEXTLINE(cata-use-named-point-constants) + mvwprintz( w_description, point( 0, 1 ), COL_STAT_PENALTY, _( "Skill rust: %d%%" ), + u.rust_rate( false ) ); + mvwprintz( w_description, point( 0, 2 ), COL_STAT_BONUS, _( "Crafting bonus: %2d%%" ), + u.get_int() ); + fold_and_print( w_description, point( 0, 4 ), getmaxx( w_description ) - 1, COL_STAT_NEUTRAL, + _( "Intelligence is also used when crafting, installing bionics, and interacting with NPCs." ) ); + break; - do { - ui_manager::redraw(); - const std::string action = ctxt.handle_input(); - const unsigned char id_for_curr_description = sel; + case 4: + mvwprintz( w, point( 2, 9 ), COL_STAT_ACT, _( "Perception:" ) ); + mvwprintz( w, point( 16, 9 ), COL_STAT_ACT, "%2d", u.per_max ); + if( u.per_max >= HIGH_STAT ) { + mvwprintz( w, point( iSecondColumn, 3 ), c_light_red, + _( "Increasing Per further costs 2 points." ) ); + } + if( u.ranged_per_mod() > 0 ) { + mvwprintz( w_description, point_zero, COL_STAT_PENALTY, _( "Aiming penalty: -%d" ), + u.ranged_per_mod() ); + } + fold_and_print( w_description, point( 0, 2 ), getmaxx( w_description ) - 1, COL_STAT_NEUTRAL, + _( "Perception is also used for detecting traps and other things of interest." ) ); + break; + } - if( tabs.handle_input( action, ctxt ) ) { - break; // Tab has changed or user has quit the screen - } else if( details.handle_navigation( action, ctxt ) ) { - // NO FURTHER ACTION REQUIRED + wrefresh( w ); + wrefresh( w_description ); + const std::string action = ctxt.handle_input(); + if( action == "DOWN" ) { + if( sel < 4 ) { + sel++; + } else { + sel = 1; + } + } else if( action == "UP" ) { + if( sel > 1 ) { + sel--; + } else { + sel = 4; + } } else if( action == "LEFT" ) { - if( *stats[sel] > min_stat_points ) { - ( *stats[sel] )--; - details_recalc = true; + if( sel == 1 && u.str_max > 4 ) { + if( u.str_max > HIGH_STAT ) { + points.stat_points++; + } + u.str_max--; + points.stat_points++; + } else if( sel == 2 && u.dex_max > 4 ) { + if( u.dex_max > HIGH_STAT ) { + points.stat_points++; + } + u.dex_max--; + points.stat_points++; + } else if( sel == 3 && u.int_max > 4 ) { + if( u.int_max > HIGH_STAT ) { + points.stat_points++; + } + u.int_max--; + points.stat_points++; + } else if( sel == 4 && u.per_max > 4 ) { + if( u.per_max > HIGH_STAT ) { + points.stat_points++; + } + u.per_max--; + points.stat_points++; } } else if( action == "RIGHT" ) { - if( *stats[sel] < max_stat_points ) { - ( *stats[sel] )++; - details_recalc = true; - } - } else if( action == "DOWN" ) { - sel = ( sel + 1 ) % 4; - } else if( action == "UP" ) { - sel = ( sel + 3 ) % 4; - } - if( sel != id_for_curr_description ) { - details_recalc = true; + if( sel == 1 && u.str_max < MAX_STAT ) { + points.stat_points--; + if( u.str_max >= HIGH_STAT ) { + points.stat_points--; + } + u.str_max++; + } else if( sel == 2 && u.dex_max < MAX_STAT ) { + points.stat_points--; + if( u.dex_max >= HIGH_STAT ) { + points.stat_points--; + } + u.dex_max++; + } else if( sel == 3 && u.int_max < MAX_STAT ) { + points.stat_points--; + if( u.int_max >= HIGH_STAT ) { + points.stat_points--; + } + u.int_max++; + } else if( sel == 4 && u.per_max < MAX_STAT ) { + points.stat_points--; + if( u.per_max >= HIGH_STAT ) { + points.stat_points--; + } + u.per_max++; + } + } else if( action == "PREV_TAB" ) { + return tab_direction::BACKWARD; + } else if( action == "NEXT_TAB" ) { + return tab_direction::FORWARD; + } else if( action == "HELP_KEYBINDINGS" ) { + // Need to redraw since the help window obscured everything. + draw_character_tabs( w, _( "STATS" ) ); + } else if( action == "QUIT" && query_yn( _( "Return to main menu?" ) ) ) { + return tab_direction::QUIT; } } while( true ); } -static struct { - bool sort_by_points = false; - /** @related player */ - bool operator()( const trait_id *a, const trait_id *b ) { - return std::abs( ( *a )->points ) > std::abs( ( *b )->points ); - } -} traits_sorter; - -static void add_trait( std::vector &to, const trait_id &trait ) -{ - to.emplace_back( trait ); -} - -static const mutation_variant *variant_trait_selection_menu( const trait_id &cur_trait ) +tab_direction set_traits( const catacurses::window &w, avatar &u, points_left &points ) { - // Because the keys will change on each loop if I clear the entries, and - // if I don't clear the entries, the menu bugs out - static std::array keys = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', - 'n', 'o', 'p',/*q */'r', 's', 't',/*u */'v', 'w', 'x', 'y', 'z', - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', - 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' - }; - uilist menu; - const mutation_variant *ret = nullptr; - avatar &pc = get_avatar(); - std::vector variants; - variants.reserve( cur_trait->variants.size() ); - for( const std::pair &pr : cur_trait->variants ) { - if( pc.has_trait_variant( {cur_trait, pr.first} ) ) { - ret = &pr.second; - } - variants.emplace_back( &pr.second ); - } + const int max_trait_points = get_option( "MAX_TRAIT_POINTS" ); - menu.title = _( "Which trait?" ); - menu.desc_enabled = true; - menu.allow_cancel = false; - int idx; - do { - menu.entries.clear(); - idx = 0; - menu.addentry_desc( idx, true, 'u', ret != nullptr ? _( "Unselect" ) : - colorize( _( "Unselected" ), c_white ), - _( "Remove this trait." ) ); - ++idx; - for( const mutation_variant *var : variants ) { - std::string name = var->alt_name.translated(); - menu.addentry_desc( idx, true, keys[( idx - 1 ) % keys.size()], - ( ret && ret == var ) ? colorize( name, c_white ) : name, - var->alt_description.translated() ); - ++idx; - } - menu.addentry_desc( idx, true, 'q', _( "Done" ), _( "Exit menu." ) ); - menu.query(); - if( menu.ret == 0 ) { - ret = nullptr; - } else if( menu.ret < idx ) { - ret = variants[menu.ret - 1]; - } - } while( menu.ret != idx ); - return ret; -} + draw_character_tabs( w, _( "TRAITS" ) ); -void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) -{ - const int max_trait_points = get_option( "MAX_TRAIT_POINTS" ); - const bool screen_reader_mode = get_option( "SCREEN_READER_MODE" ); - std::string last_trait; // Used in screen_reader_mode to ensure full trait name is read + catacurses::window w_description = + catacurses::newwin( 3, TERMX - 2, point( 1 + getbegx( w ), TERMY - 4 + getbegy( w ) ) ); // Track how many good / bad POINTS we have; cap both at MAX_TRAIT_POINTS int num_good = 0; int num_bad = 0; - // 0 -> traits that take points ( positive traits ) - // 1 -> traits that give points ( negative traits ) - // 2 -> neutral traits ( facial hair, skin color, etc ) - std::array, 3> vStartingTraits; - for( const mutation_branch &traits_iter : mutation_branch::get_all() ) { + std::vector vStartingTraits[3]; + + for( auto &traits_iter : mutation_branch::get_all() ) { // Don't list blacklisted traits if( mutation_branch::trait_is_blacklisted( traits_iter.id ) ) { continue; } - const std::set scentraits = get_scenario()->get_locked_traits(); - const bool is_scentrait = scentraits.find( traits_iter.id ) != scentraits.end(); - // Always show profession locked traits, regardless of if they are forbidden - const std::vector proftraits = u.prof->get_locked_traits(); - auto pred = [&traits_iter]( const trait_and_var & query ) { - return query.trait == traits_iter.id; - }; - const bool is_proftrait = std::find_if( proftraits.begin(), proftraits.end(), - pred ) != proftraits.end(); - - bool is_hobby_locked_trait = false; - for( const profession *hobby : u.hobbies ) { - is_hobby_locked_trait = is_hobby_locked_trait || hobby->is_locked_trait( traits_iter.id ); - } - + const std::vector proftraits = u.prof->get_locked_traits(); + const bool is_proftrait = std::find( proftraits.begin(), proftraits.end(), + traits_iter.id ) != proftraits.end(); // We show all starting traits, even if we can't pick them, to keep the interface consistent. - if( traits_iter.startingtrait || get_scenario()->traitquery( traits_iter.id ) || is_proftrait || - is_hobby_locked_trait ) { + if( traits_iter.startingtrait || g->scen->traitquery( traits_iter.id ) || is_proftrait ) { if( traits_iter.points > 0 ) { - add_trait( vStartingTraits[0], traits_iter.id ); - - if( is_proftrait || is_scentrait ) { - continue; - } + vStartingTraits[0].push_back( traits_iter.id ); if( u.has_trait( traits_iter.id ) ) { num_good += traits_iter.points; } } else if( traits_iter.points < 0 ) { - add_trait( vStartingTraits[1], traits_iter.id ); - - if( is_proftrait || is_scentrait ) { - continue; - } + vStartingTraits[1].push_back( traits_iter.id ); if( u.has_trait( traits_iter.id ) ) { num_bad += traits_iter.points; } } else { - add_trait( vStartingTraits[2], traits_iter.id ); + vStartingTraits[2].push_back( traits_iter.id ); } } } @@ -1638,129 +1064,42 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) const int used_pages = vStartingTraits[2].empty() ? 2 : 3; for( auto &vStartingTrait : vStartingTraits ) { - std::sort( vStartingTrait.begin(), vStartingTrait.end(), trait_display_nocolor_sort ); + std::sort( vStartingTrait.begin(), vStartingTrait.end(), trait_display_sort ); } + nc_color col_on_act, col_off_act, col_on_pas, col_off_pas, hi_on, hi_off, col_tr; + + const size_t iContentHeight = TERMY - 9; int iCurWorkingPage = 0; - std::array iStartPos = { 0, 0, 0 }; - std::array iCurrentLine = { 0, 0, 0 }; - std::array traits_size; - bool recalc_traits = false; - // pointer for memory footprint reasons - std::array, 3> sorted_traits; - std::array trait_sbs; - std::string filterstring; + int iStartPos[3] = { 0, 0, 0 }; + int iCurrentLine[3] = { 0, 0, 0 }; + size_t traits_size[3]; for( int i = 0; i < 3; i++ ) { - const size_t size = vStartingTraits[i].size(); - traits_size[i] = size; - sorted_traits[i].reserve( size ); - for( size_t j = 0; j < size; j++ ) { - sorted_traits[i].emplace_back( &vStartingTraits[i][j] ); - } + traits_size[i] = vStartingTraits[i].size(); } - const int iHeaderHeight = 6; - const int iDetailHeight = 3; - size_t iContentHeight = 0; - size_t page_width = 0; - - const auto pos_calc = [&]() { - for( int i = 0; i < 3; i++ ) { - // Shift start position to avoid iterating beyond end - traits_size[i] = sorted_traits[i].size(); - int total = static_cast( traits_size[i] ); - int height = static_cast( iContentHeight ); - iStartPos[i] = std::min( iStartPos[i], std::max( 0, total - height ) ); - } - }; - - // this will return the next non empty page - // there will always be at least one non-empty page - // iCurWorkingPage will always be a non-empty page - const auto next_avail_page = [&traits_size, &iCurWorkingPage]( bool invert_direction ) -> int { - int prev_page = iCurWorkingPage < 1 ? 2 : iCurWorkingPage - 1; - if( !traits_size[prev_page] ) - { - prev_page = prev_page < 1 ? 2 : prev_page - 1; - if( !traits_size[prev_page] ) { - prev_page = prev_page < 1 ? 2 : prev_page - 1; - } - } - - int next_page = iCurWorkingPage > 1 ? 0 : iCurWorkingPage + 1; - if( !traits_size[next_page] ) - { - next_page = next_page > 1 ? 0 : next_page + 1; - if( !traits_size[next_page] ) { - next_page = next_page > 1 ? 0 : next_page + 1; - } - } - - return invert_direction ? prev_page : next_page; - }; - - ui_adaptor ui; - catacurses::window w; - catacurses::window w_details_pane; - scrolling_text_view details( w_details_pane ); - bool details_recalc = true; - const auto init_windows = [&]( ui_adaptor & ui ) { - w = catacurses::newwin( TERMY, TERMX, point_zero ); - w_details_pane = catacurses::newwin( iDetailHeight, TERMX - 1, - point( 0, TERMY - iDetailHeight - 1 ) ); - ui.position_from_window( w ); - page_width = std::min( ( TERMX - 4 ) / used_pages, 38 ); - iContentHeight = TERMY - iHeaderHeight - iDetailHeight - 1; - - details_recalc = true; - pos_calc(); - }; - init_windows( ui ); - ui.on_screen_resize( init_windows ); + const size_t page_width = std::min( ( TERMX - 4 ) / used_pages, 38 ); input_context ctxt( "NEW_CHAR_TRAITS" ); - tabs.set_up_tab_navigation( ctxt ); - details.set_up_navigation( ctxt, scrolling_key_scheme::angle_bracket_scroll ); - for( scrollbar &sb : trait_sbs ) { - sb.set_draggable( ctxt ); - } - ctxt.register_navigate_ui_list(); - ctxt.register_leftright(); + ctxt.register_cardinal(); ctxt.register_action( "CONFIRM" ); + ctxt.register_action( "PREV_TAB" ); + ctxt.register_action( "NEXT_TAB" ); ctxt.register_action( "HELP_KEYBINDINGS" ); - ctxt.register_action( "FILTER" ); - ctxt.register_action( "RESET_FILTER" ); - ctxt.register_action( "SORT" ); - - ui.on_redraw( [&]( ui_adaptor & ui ) { - werase( w ); + ctxt.register_action( "QUIT" ); - tabs.draw( w ); - for( int i = 1; i < 3; ++i ) { - mvwputch( w, point( i * page_width, iHeaderHeight - 1 ), BORDER_COLOR, LINE_OXXX ); // '┬' - } - draw_filter_and_sorting_indicators( w, ctxt, filterstring, traits_sorter ); - draw_points( w, pool, u ); - int full_string_length = 0; - const int remaining_points_length = utf8_width( pools_to_string( u, pool ), true ); - if( pool != pool_type::FREEFORM ) { - std::string full_string = - string_format( "%2d/%-2d %3d/-%-2d", - num_good, max_trait_points, num_bad, max_trait_points ); - fold_and_print( w, point( remaining_points_length + 3, 3 ), getmaxx( w ) - 2, c_white, - full_string ); - full_string_length = utf8_width( full_string, true ) + remaining_points_length + 3; - } else { - full_string_length = remaining_points_length + 3; + do { + draw_points( w, points ); + if( !points.is_freeform() ) { + mvwprintz( w, point( 26, 3 ), c_light_green, "%2d/%-2d", num_good, max_trait_points ); + mvwprintz( w, point( 32, 3 ), c_light_red, "%3d/-%-2d ", num_bad, max_trait_points ); } - for( int iCurrentPage = 0; iCurrentPage < 3; iCurrentPage++ ) { - nc_color col_on_act; - nc_color col_off_act; - nc_color col_on_pas; - nc_color col_off_pas; - nc_color col_tr; + // Clear the bottom of the screen. + werase( w_description ); + + for( int iCurrentPage = 0; iCurrentPage < 3; iCurrentPage++ ) { //Good/Bad switch( iCurrentPage ) { case 0: col_on_act = COL_TR_GOOD_ON_ACT; @@ -1768,6 +1107,8 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) col_on_pas = COL_TR_GOOD_ON_PAS; col_off_pas = COL_TR_GOOD_OFF_PAS; col_tr = COL_TR_GOOD; + hi_on = hilite( col_on_act ); + hi_off = hilite( col_off_act ); break; case 1: col_on_act = COL_TR_BAD_ON_ACT; @@ -1775,6 +1116,8 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) col_on_pas = COL_TR_BAD_ON_PAS; col_off_pas = COL_TR_BAD_OFF_PAS; col_tr = COL_TR_BAD; + hi_on = hilite( col_on_act ); + hi_off = hilite( col_off_act ); break; default: col_on_act = COL_TR_NEUT_ON_ACT; @@ -1782,37 +1125,44 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) col_on_pas = COL_TR_NEUT_ON_PAS; col_off_pas = COL_TR_NEUT_OFF_PAS; col_tr = COL_TR_NEUT; + hi_on = hilite( col_on_act ); + hi_off = hilite( col_off_act ); break; } - nc_color hi_on = hilite( col_on_act ); - nc_color hi_off = hilite( c_white ); - - int &start = iStartPos[iCurrentPage]; - int current = iCurrentLine[iCurrentPage]; - calcStartPos( start, current, iContentHeight, traits_size[iCurrentPage] ); - int end = start + static_cast( std::min( traits_size[iCurrentPage], iContentHeight ) ); - - for( int i = start; i < end; i++ ) { - const trait_id &cur_trait = *sorted_traits[iCurrentPage][i]; - if( current == i && iCurrentPage == iCurWorkingPage ) { - int points = cur_trait->points; + int start_y = iStartPos[iCurrentPage]; + int cur_line_y = iCurrentLine[iCurrentPage]; + calcStartPos( start_y, cur_line_y, iContentHeight, + traits_size[iCurrentPage] ); + + //Draw Traits + for( int i = start_y; i < static_cast( traits_size[iCurrentPage] ); i++ ) { + if( i < start_y || + i >= start_y + static_cast( std::min( traits_size[iCurrentPage], iContentHeight ) ) ) { + continue; + } + auto &cur_trait = vStartingTraits[iCurrentPage][i]; + auto &mdata = cur_trait.obj(); + if( cur_line_y == i && iCurrentPage == iCurWorkingPage ) { + // Clear line from 41 to end of line (minus border) + mvwprintz( w, point( 41, 3 ), c_light_gray, std::string( getmaxx( w ) - 41 - 1, ' ' ) ); + int points = mdata.points; bool negativeTrait = points < 0; if( negativeTrait ) { points *= -1; } - if( pool != pool_type::FREEFORM ) { - mvwprintz( w, point( full_string_length + 3, 3 ), col_tr, - n_gettext( "%s %s %d point", "%s %s %d points", points ), - cur_trait->name(), - negativeTrait ? _( "earns" ) : _( "costs" ), - points ); - } + mvwprintz( w, point( 41, 3 ), col_tr, ngettext( "%s %s %d point", "%s %s %d points", points ), + mdata.name(), + negativeTrait ? _( "earns" ) : _( "costs" ), + points ); + fold_and_print( w_description, point_zero, + TERMX - 2, col_tr, + mdata.desc() ); } nc_color cLine = col_off_pas; if( iCurWorkingPage == iCurrentPage ) { cLine = col_off_act; - if( current == i ) { + if( cur_line_y == i ) { cLine = hi_off; if( u.has_conflicting_trait( cur_trait ) ) { cLine = hilite( c_dark_gray ); @@ -1820,7 +1170,7 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) cLine = hi_on; } } else { - if( u.has_conflicting_trait( cur_trait ) || get_scenario()->is_forbidden_trait( cur_trait ) ) { + if( u.has_conflicting_trait( cur_trait ) || g->scen->is_forbidden_trait( cur_trait ) ) { cLine = c_dark_gray; } else if( u.has_trait( cur_trait ) ) { @@ -1830,276 +1180,112 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) } else if( u.has_trait( cur_trait ) ) { cLine = col_on_pas; - } else if( u.has_conflicting_trait( cur_trait ) || - get_scenario()->is_forbidden_trait( cur_trait ) ) { + } else if( u.has_conflicting_trait( cur_trait ) || g->scen->is_forbidden_trait( cur_trait ) ) { cLine = c_light_gray; } - const int cur_line_y = iHeaderHeight + i - start; - const int cur_line_x = 2 + iCurrentPage * page_width; - const point opt_pos( cur_line_x, cur_line_y ); - if( screen_reader_mode ) { - // This list only clutters up the screen in screen reader mode - } else { - mvwprintz( w, opt_pos, cLine, - utf8_truncate( cur_trait->name(), page_width - 2 ) ); - } + // Clear the line + int cur_line_y = 5 + i - start_y; + int cur_line_x = 2 + iCurrentPage * page_width; + mvwprintz( w, point( cur_line_x, cur_line_y ), c_light_gray, std::string( page_width, ' ' ) ); + mvwprintz( w, point( cur_line_x, cur_line_y ), cLine, utf8_truncate( mdata.name(), + page_width - 2 ) ); } - if( details_recalc ) { - std::string description; - const trait_id &cur_trait = *sorted_traits[iCurWorkingPage][iCurrentLine[iCurWorkingPage]]; - if( screen_reader_mode ) { - /* Screen readers will skip over text that has not changed. Since the lists of traits are - * alphabetical, this frequently results in letters/words being skipped. So, if the screen - * reader is likely to skip over part of a trait name, we trick it into thinking things have - * changed by shifting the text slightly. - */ - if( !last_trait.empty() && last_trait[0] == cur_trait->name()[0] ) { - description = " " + cur_trait->name(); - } else { - description = cur_trait->name(); - } - last_trait = description; - - std::string cur_trait_notes; - if( u.has_conflicting_trait( cur_trait ) ) { - cur_trait_notes = _( "a conflicting trait is active" ); - } else if( u.has_trait( cur_trait ) ) { - cur_trait_notes = _( "active" ); - } - - if( !cur_trait_notes.empty() ) { - description.append( string_format( " - %s", cur_trait_notes ) ); - } - - description.append( "\n" + cur_trait->desc() ); - } else { - description = cur_trait->desc(); - } - details.set_text( colorize( description, col_tr ) ); - details_recalc = false; + for( int i = 0; i < used_pages; i++ ) { + draw_scrollbar( w, iCurrentLine[i], iContentHeight, traits_size[i], point( page_width * i, 5 ) ); } - - trait_sbs[iCurrentPage].offset_x( page_width * iCurrentPage ) - .offset_y( iHeaderHeight ) - .content_size( traits_size[iCurrentPage] ) - .viewport_pos( start ) - .viewport_size( iContentHeight ) - .apply( w ); } - wnoutrefresh( w ); - ui.set_cursor( w_details_pane, point_zero ); - // color is never visible (COL_TR_NEUT), text is already colorized - details.draw( COL_TR_NEUT ); - } ); - - do { - if( recalc_traits ) { - for( int i = 0; i < 3; i++ ) { - const size_t size = vStartingTraits[i].size(); - sorted_traits[i].clear(); - for( size_t j = 0; j < size; j++ ) { - sorted_traits[i].emplace_back( &vStartingTraits[i][j] ); - } - } - - if( !filterstring.empty() ) { - for( std::vector &traits : sorted_traits ) { - const auto new_end_iter = std::remove_if( - traits.begin(), - traits.end(), - [&filterstring]( const trait_id * trait ) { - return !lcmatch( ( *trait )->name(), filterstring ); - } ); - - traits.erase( new_end_iter, traits.end() ); - } - } - - if( !filterstring.empty() && sorted_traits[0].empty() && sorted_traits[1].empty() && - sorted_traits[2].empty() ) { - popup( _( "Nothing found." ) ); // another case of black box in tiles - filterstring.clear(); - continue; - } - - if( traits_sorter.sort_by_points ) { - std::stable_sort( sorted_traits[0].begin(), sorted_traits[0].end(), traits_sorter ); - std::stable_sort( sorted_traits[1].begin(), sorted_traits[1].end(), traits_sorter ); + wrefresh( w ); + wrefresh( w_description ); + const std::string action = ctxt.handle_input(); + if( action == "LEFT" ) { + iCurWorkingPage--; + if( iCurWorkingPage < 0 ) { + iCurWorkingPage = used_pages - 1; } - - // Select the current page, if not empty - // There should always be at least one not empty page - iCurrentLine[0] = 0; - iCurrentLine[1] = 0; - iCurrentLine[2] = 0; - if( sorted_traits[iCurWorkingPage].empty() ) { + } else if( action == "RIGHT" ) { + iCurWorkingPage++; + if( iCurWorkingPage > used_pages - 1 ) { iCurWorkingPage = 0; - if( !sorted_traits[0].empty() ) { - iCurWorkingPage = 0; - } else if( !sorted_traits[1].empty() ) { - iCurWorkingPage = 1; - } else if( !sorted_traits[2].empty() ) { - iCurWorkingPage = 2; - } } - - pos_calc(); - recalc_traits = false; - } - - ui_manager::redraw(); - const std::string action = ctxt.handle_input(); - std::array< int, 3> cur_sb_pos = iStartPos; - bool scrollbar_handled = false; - const int iPrevWorkingPage = iCurWorkingPage; - const int iPrevLine = iCurrentLine[iCurWorkingPage]; - for( int i = 0; i < static_cast( trait_sbs.size() ); ++i ) { - if( trait_sbs[i].handle_dragging( action, ctxt.get_coordinates_text( catacurses::stdscr ), - cur_sb_pos[i] ) ) { - if( cur_sb_pos[i] != iStartPos[i] ) { - iStartPos[i] = cur_sb_pos[i]; - iCurrentLine[i] = iStartPos[i] + ( iContentHeight - 1 ) / 2; - } - scrollbar_handled = true; + } else if( action == "UP" ) { + if( iCurrentLine[iCurWorkingPage] == 0 ) { + iCurrentLine[iCurWorkingPage] = traits_size[iCurWorkingPage] - 1; + } else { + iCurrentLine[iCurWorkingPage]--; + } + } else if( action == "DOWN" ) { + iCurrentLine[iCurWorkingPage]++; + if( static_cast( iCurrentLine[iCurWorkingPage] ) >= traits_size[iCurWorkingPage] ) { + iCurrentLine[iCurWorkingPage] = 0; } - } - - if( tabs.handle_input( action, ctxt ) ) { - break; // Tab has changed or user has quit the screen - } else if( action == "LEFT" || action == "RIGHT" ) { - iCurWorkingPage = next_avail_page( action == "LEFT" ); - } else if( scrollbar_handled - || details.handle_navigation( action, ctxt ) - || navigate_ui_list( action, iCurrentLine[iCurWorkingPage], 10, - traits_size[iCurWorkingPage], true ) ) { - // No additional action required } else if( action == "CONFIRM" ) { int inc_type = 0; - const trait_id cur_trait = *sorted_traits[iCurWorkingPage][iCurrentLine[iCurWorkingPage]]; + const trait_id cur_trait = vStartingTraits[iCurWorkingPage][iCurrentLine[iCurWorkingPage]]; const mutation_branch &mdata = cur_trait.obj(); - std::string variant; + if( u.has_trait( cur_trait ) ) { - // Look through the profession bionics, and see if any of them conflict with this trait - std::vector cbms_blocking_trait = bionics_cancelling_trait( u.prof->CBMs(), cur_trait ); - const std::unordered_set conflicting_traits = u.get_conflicting_traits( cur_trait ); + inc_type = -1; - if( u.has_trait( cur_trait ) ) { - if( !cur_trait->variants.empty() ) { - const mutation_variant *rval = variant_trait_selection_menu( cur_trait ); - if( rval == nullptr ) { - inc_type = -1; - } else { - u.set_mut_variant( cur_trait, rval ); - } - } else { - inc_type = -1; - - if( get_scenario()->is_locked_trait( cur_trait ) ) { - inc_type = 0; - popup( _( "Your scenario of %s prevents you from removing this trait." ), - get_scenario()->gender_appropriate_name( u.male ) ); - } else if( u.prof->is_locked_trait( cur_trait ) ) { - inc_type = 0; - popup( _( "Your profession of %s prevents you from removing this trait." ), - u.prof->gender_appropriate_name( u.male ) ); - } - for( const profession *hobbies : u.hobbies ) { - if( hobbies->is_locked_trait( cur_trait ) ) { - inc_type = 0; - popup( _( "Your background of %s prevents you from removing this trait." ), - hobbies->gender_appropriate_name( u.male ) ); - } - } + if( g->scen->is_locked_trait( cur_trait ) ) { + inc_type = 0; + popup( _( "Your scenario of %s prevents you from removing this trait." ), + g->scen->gender_appropriate_name( u.male ) ); + } else if( u.prof->is_locked_trait( cur_trait ) ) { + inc_type = 0; + popup( _( "Your profession of %s prevents you from removing this trait." ), + u.prof->gender_appropriate_name( u.male ) ); } - } else if( !conflicting_traits.empty() ) { - std::vector conflict_names; - conflict_names.reserve( conflicting_traits.size() ); - for( const trait_id &trait : conflicting_traits ) { - conflict_names.emplace_back( u.mutation_name( trait ) ); - } - popup( _( "You already picked some conflicting traits: %s." ), - enumerate_as_string( conflict_names ) ); - } else if( get_scenario()->is_forbidden_trait( cur_trait ) ) { + } else if( u.has_conflicting_trait( cur_trait ) ) { + popup( _( "You already picked a conflicting trait!" ) ); + } else if( g->scen->is_forbidden_trait( cur_trait ) ) { popup( _( "The scenario you picked prevents you from taking this trait!" ) ); - } else if( u.prof->is_forbidden_trait( cur_trait ) ) { - popup( _( "Your profession of %s prevents you from taking this trait." ), - u.prof->gender_appropriate_name( u.male ) ); - } else if( !cbms_blocking_trait.empty() ) { - // Grab a list of the names of the bionics that block this trait - // So that the player know what is preventing them from taking it - std::vector conflict_names; - conflict_names.reserve( cbms_blocking_trait.size() ); - for( const bionic_id &conflict : cbms_blocking_trait ) { - conflict_names.emplace_back( conflict->name.translated() ); - } - popup( _( "The following bionics prevent you from taking this trait: %s." ), - enumerate_as_string( conflict_names ) ); } else if( iCurWorkingPage == 0 && num_good + mdata.points > - max_trait_points && pool != pool_type::FREEFORM ) { - popup( n_gettext( "Sorry, but you can only take %d point of advantages.", - "Sorry, but you can only take %d points of advantages.", max_trait_points ), + max_trait_points && !points.is_freeform() ) { + popup( ngettext( "Sorry, but you can only take %d point of advantages.", + "Sorry, but you can only take %d points of advantages.", max_trait_points ), max_trait_points ); } else if( iCurWorkingPage != 0 && num_bad + mdata.points < - -max_trait_points && pool != pool_type::FREEFORM ) { - popup( n_gettext( "Sorry, but you can only take %d point of disadvantages.", - "Sorry, but you can only take %d points of disadvantages.", max_trait_points ), + -max_trait_points && !points.is_freeform() ) { + popup( ngettext( "Sorry, but you can only take %d point of disadvantages.", + "Sorry, but you can only take %d points of disadvantages.", max_trait_points ), max_trait_points ); } else { - if( !cur_trait->variants.empty() ) { - const mutation_variant *rval = variant_trait_selection_menu( cur_trait ); - if( rval != nullptr ) { - inc_type = 1; - variant = rval->id; - } else { - inc_type = 0; - } - } else { - inc_type = 1; - } + inc_type = 1; } //inc_type is either -1 or 1, so we can just multiply by it to invert if( inc_type != 0 ) { - details_recalc = true; - u.toggle_trait_deps( cur_trait, variant ); + u.toggle_trait( cur_trait ); + points.trait_points -= mdata.points * inc_type; if( iCurWorkingPage == 0 ) { num_good += mdata.points * inc_type; } else { num_bad += mdata.points * inc_type; } } - } else if( action == "SORT" ) { - traits_sorter.sort_by_points = !traits_sorter.sort_by_points; - recalc_traits = true; - } else if( action == "FILTER" ) { - string_input_popup() - .title( _( "Search:" ) ) - .width( 10 ) - .description( _( "Search by trait name." ) ) - .edit( filterstring ); - recalc_traits = true; - } else if( action == "RESET_FILTER" ) { - if( !filterstring.empty() ) { - filterstring.clear(); - recalc_traits = true; - } - } - if( iCurWorkingPage != iPrevWorkingPage || iCurrentLine[iCurWorkingPage] != iPrevLine ) { - details_recalc = true; + } else if( action == "PREV_TAB" ) { + return tab_direction::BACKWARD; + } else if( action == "NEXT_TAB" ) { + return tab_direction::FORWARD; + } else if( action == "HELP_KEYBINDINGS" ) { + // Need to redraw since the help window obscured everything. + draw_character_tabs( w, _( "TRAITS" ) ); + } else if( action == "QUIT" && query_yn( _( "Return to main menu?" ) ) ) { + return tab_direction::QUIT; } } while( true ); } -static struct { +struct { bool sort_by_points = true; - bool male = false; + bool male; /** @related player */ - bool operator()( const string_id &a, const string_id &b ) const { + bool operator()( const string_id &a, const string_id &b ) { // The generic ("Unemployed") profession should be listed first. const profession *gen = profession::generic(); if( &b.obj() == gen ) { @@ -2108,1207 +1294,557 @@ static struct { return true; } - if( !a->can_pick().success() && b->can_pick().success() ) { - return false; - } - - if( a->can_pick().success() && !b->can_pick().success() ) { - return true; - } - if( sort_by_points ) { return a->point_cost() < b->point_cost(); } else { - return localized_compare( a->gender_appropriate_name( male ), - b->gender_appropriate_name( male ) ); + return a->gender_appropriate_name( male ) < + b->gender_appropriate_name( male ); } } } profession_sorter; -static std::string assemble_profession_details( const avatar &u, const input_context &ctxt, - const std::vector> &sorted_profs, const int cur_id, const std::string ¬es ) -{ - std::string assembled; - - // Display Origin - const std::string mod_src = enumerate_as_string( sorted_profs[cur_id]->src, []( - const std::pair &source ) { - return string_format( "'%s'", source.second->name() ); - }, enumeration_conjunction::arrow ); - assembled += string_format( _( "Origin: %s" ), mod_src ) + "\n"; - - std::string profession_name = sorted_profs[cur_id]->gender_appropriate_name( u.male ); - if( get_option( "SCREEN_READER_MODE" ) && !notes.empty() ) { - profession_name = profession_name.append( string_format( " - %s", notes ) ); - } - assembled += string_format( g_switch_msg( u ), ctxt.get_desc( "CHANGE_GENDER" ), - profession_name ) + "\n"; - assembled += string_format( dress_switch_msg(), ctxt.get_desc( "CHANGE_OUTFIT" ) ) + "\n"; - - if( sorted_profs[cur_id]->get_requirement().has_value() ) { - assembled += "\n" + colorize( _( "Profession requirements:" ), COL_HEADER ) + "\n"; - assembled += string_format( _( "Complete \"%s\"\n" ), - sorted_profs[cur_id]->get_requirement().value()->name() ); - } - //Profession story - assembled += "\n" + colorize( _( "Profession story:" ), COL_HEADER ) + "\n"; - if( !sorted_profs[cur_id]->can_pick().success() ) { - assembled += colorize( sorted_profs[cur_id]->can_pick().str(), c_red ) + "\n"; - } - assembled += colorize( sorted_profs[cur_id]->description( u.male ), c_green ) + "\n"; - - // Profession addictions - const auto prof_addictions = sorted_profs[cur_id]->addictions(); - if( !prof_addictions.empty() ) { - assembled += "\n" + colorize( _( "Profession addictions:" ), COL_HEADER ) + "\n"; - for( const addiction &a : prof_addictions ) { - const char *format = pgettext( "set_profession_addictions", "%1$s (%2$d)" ); - assembled += string_format( format, a.type->get_name().translated(), a.intensity ) + "\n"; - } - } - - // Profession traits - const auto prof_traits = sorted_profs[cur_id]->get_locked_traits(); - assembled += "\n" + colorize( _( "Profession traits:" ), COL_HEADER ) + "\n"; - if( prof_traits.empty() ) { - assembled += pgettext( "set_profession_trait", "None" ) + std::string( "\n" ); - } else { - for( const trait_and_var &t : prof_traits ) { - assembled += t.name() + "\n"; - } - } - - // Profession martial art styles - const auto prof_ma_known = sorted_profs[cur_id]->ma_known(); - const auto prof_ma_choices = sorted_profs[cur_id]->ma_choices(); - int ma_amount = sorted_profs[cur_id]->ma_choice_amount; - assembled += "\n" + colorize( _( "Profession martial arts:" ), COL_HEADER ) + "\n"; - if( prof_ma_known.empty() && prof_ma_choices.empty() ) { - assembled += pgettext( "set_profession_ma", "None" ) + std::string( "\n" ); - } else { - if( !prof_ma_known.empty() ) { - assembled += colorize( _( "Known:" ), c_cyan ) + "\n"; - for( const matype_id &ma : prof_ma_known ) { - const martialart &style = ma.obj(); - assembled += style.name.translated() + "\n"; - } - } - if( !prof_ma_known.empty() && !prof_ma_choices.empty() ) { - assembled += "\n"; - } - if( !prof_ma_choices.empty() ) { - assembled += colorize( string_format( _( "Choose %d:" ), ma_amount ), c_cyan ) + "\n"; - for( const matype_id &ma : prof_ma_choices ) { - const martialart &style = ma.obj(); - assembled += style.name.translated() + "\n"; - } - } - } - - // Profession skills - const profession::StartingSkillList prof_skills = sorted_profs[cur_id]->skills(); - assembled += "\n" + colorize( _( "Profession skills:" ), COL_HEADER ) + "\n"; - if( prof_skills.empty() ) { - assembled += pgettext( "set_profession_skill", "None" ) + std::string( "\n" ); - } else { - for( const auto &sl : prof_skills ) { - const char *format = pgettext( "set_profession_skill", "%1$s (%2$d)" ); - assembled += string_format( format, sl.first.obj().name(), sl.second ) + "\n"; - } - } - - // Profession items - const auto prof_items = sorted_profs[cur_id]->items( outfit, u.get_mutations() ); - assembled += "\n" + colorize( _( "Profession items:" ), COL_HEADER ) + "\n"; - if( prof_items.empty() ) { - assembled += pgettext( "set_profession_item", "None" ) + std::string( "\n" ); - } else { - // TODO: If the item group is randomized *at all*, these will be different each time - // and it won't match what you actually start with - // TODO: Put like items together like the inventory does, so we don't have to scroll - // through a list of a dozen forks. - std::string assembled_wielded; - std::string assembled_worn; - std::string assembled_inventory; - for( const item &it : prof_items ) { - if( it.has_flag( json_flag_no_auto_equip ) ) { // NOLINT(bugprone-branch-clone) - assembled_inventory += it.display_name() + "\n"; - } else if( it.has_flag( json_flag_auto_wield ) ) { - assembled_wielded += it.display_name() + "\n"; - } else if( it.is_armor() ) { - assembled_worn += it.display_name() + "\n"; - } else { - assembled_inventory += it.display_name() + "\n"; - } - } - assembled += colorize( _( "Wielded:" ), c_cyan ) + "\n"; - assembled += !assembled_wielded.empty() ? assembled_wielded : - pgettext( "set_profession_item_wielded", - "None\n" ); - assembled += colorize( _( "Worn:" ), c_cyan ) + "\n"; - assembled += !assembled_worn.empty() ? assembled_worn : pgettext( "set_profession_item_worn", - "None\n" ); - assembled += colorize( _( "Inventory:" ), c_cyan ) + "\n"; - assembled += !assembled_inventory.empty() ? assembled_inventory : - pgettext( "set_profession_item_inventory", - "None\n" ); - } - - // Profession bionics, active bionics shown first - auto prof_CBMs = sorted_profs[cur_id]->CBMs(); - - if( !prof_CBMs.empty() ) { - assembled += "\n" + colorize( _( "Profession bionics:" ), COL_HEADER ) + "\n"; - std::sort( std::begin( prof_CBMs ), std::end( prof_CBMs ), []( const bionic_id & a, - const bionic_id & b ) { - return a->activated && !b->activated; - } ); - for( const auto &b : prof_CBMs ) { - const bionic_data &cbm = b.obj(); - if( cbm.activated && cbm.has_flag( json_flag_BIONIC_TOGGLED ) ) { - assembled += string_format( _( "%s (toggled)" ), cbm.name ) + "\n"; - } else if( cbm.activated ) { - assembled += string_format( _( "%s (activated)" ), cbm.name ) + "\n"; - } else { - assembled += cbm.name + "\n"; - } - } - } - // Proficiencies - std::vector prof_proficiencies = sorted_profs[cur_id]->proficiencies(); - if( !prof_proficiencies.empty() ) { - assembled += "\n" + colorize( _( "Profession proficiencies:" ), COL_HEADER ) + "\n"; - for( const proficiency_id &prof : prof_proficiencies ) { - assembled += prof->name() + "\n"; - } - } - // Recipes - std::vector prof_recipe = sorted_profs[cur_id]->recipes(); - if( !prof_recipe.empty() ) { - assembled += "\n" + colorize( _( "Profession recipes:" ), COL_HEADER ) + "\n"; - for( const recipe_id &prof : prof_recipe ) { - assembled += prof->result_name() + "\n"; - } - } - // Profession pet - if( !sorted_profs[cur_id]->pets().empty() ) { - assembled += "\n" + colorize( _( "Profession pets:" ), COL_HEADER ) + "\n"; - for( const auto &elem : sorted_profs[cur_id]->pets() ) { - monster mon( elem ); - assembled += mon.get_name() + "\n"; - } - } - // Profession vehicle - if( sorted_profs[cur_id]->vehicle() ) { - assembled += "\n" + colorize( _( "Profession vehicle:" ), COL_HEADER ) + "\n"; - vproto_id veh_id = sorted_profs[cur_id]->vehicle(); - assembled += veh_id->name.translated() + "\n"; - } - // Profession spells - if( !sorted_profs[cur_id]->spells().empty() ) { - assembled += "\n" + colorize( _( "Profession spells:" ), COL_HEADER ) + "\n"; - for( const std::pair spell_pair : sorted_profs[cur_id]->spells() ) { - assembled += string_format( _( "%s level %d" ), spell_pair.first->name, spell_pair.second ) + "\n"; - } - } - // Profession missions - if( !sorted_profs[cur_id]->missions().empty() ) { - assembled += "\n" + colorize( _( "Profession missions:" ), COL_HEADER ) + "\n"; - for( mission_type_id mission_id : sorted_profs[cur_id]->missions() ) { - assembled += mission_type::get( mission_id )->tname() + "\n"; - } - } - return assembled; -} - -/** Helper to filter and move the cursor in the hobby/profession lists */ -template -size_t filter_entries( avatar &u, int &cur_id, std::vector &old_entries, - std::vector &new_entries, T chosen_entry, - std::string filterstring, S sorter ) -{ - T previously_highlighted = old_entries.empty() ? T() : old_entries[cur_id]; - - old_entries = new_entries; - - // Filter the list of entries - const auto new_end = std::remove_if( old_entries.begin(), - old_entries.end(), [&]( const T & arg ) { - return !lcmatch( arg->gender_appropriate_name( u.male ), filterstring ); - } ); - old_entries.erase( new_end, old_entries.end() ); - - if( old_entries.empty() ) { - popup( _( "Nothing found." ) ); // another case of black box in tiles - return 0; // tell caller to try again without a filterstring - } - - int entries_length = old_entries.size(); - - std::stable_sort( old_entries.begin(), old_entries.end(), sorter ); - - bool match = false; - - // Put the cursor on the previously highlighted entry, if possible. - for( int i = 0; i < entries_length; ++i ) { - if( old_entries[i] == previously_highlighted ) { - cur_id = i; - match = true; - break; - } - } - - if( !match ) { - // Pur the cursor on the currently chosen entry, if possible. - for( int i = 0; i < entries_length; ++i ) { - if( old_entries[i] == chosen_entry ) { - cur_id = i; - match = true; - break; - } - } - } - - if( !match ) { - cur_id = 0; - } - - return old_entries.size(); -} - /** Handle the profession tab of the character generation menu */ -void set_profession( tab_manager &tabs, avatar &u, pool_type pool ) +tab_direction set_profession( const catacurses::window &w, avatar &u, points_left &points, + const tab_direction direction ) { + draw_character_tabs( w, _( "PROFESSION" ) ); int cur_id = 0; - size_t iContentHeight = 0; + tab_direction retval = tab_direction::NONE; + int desc_offset = 0; + const int iContentHeight = TERMY - 10; int iStartPos = 0; - const bool screen_reader_mode = get_option( "SCREEN_READER_MODE" ); + catacurses::window w_description = + catacurses::newwin( 4, TERMX - 2, point( 1 + getbegx( w ), TERMY - 5 + getbegy( w ) ) ); - ui_adaptor ui; - catacurses::window w; - catacurses::window w_details_pane; - scrolling_text_view details( w_details_pane ); - bool details_recalc = true; - const int iHeaderHeight = 6; - scrollbar list_sb; - const auto init_windows = [&]( ui_adaptor & ui ) { - iContentHeight = TERMY - iHeaderHeight - 1; - w = catacurses::newwin( TERMY, TERMX, point_zero ); - w_details_pane = catacurses::newwin( iContentHeight, TERMX / 2 - 1, point( TERMX / 2, - iHeaderHeight ) ); - details_recalc = true; - ui.position_from_window( w ); - }; - init_windows( ui ); - ui.on_screen_resize( init_windows ); + catacurses::window w_sorting = + catacurses::newwin( 1, 55, point( ( TERMX / 2 ) + getbegx( w ), 5 + getbegy( w ) ) ); + catacurses::window w_genderswap = + catacurses::newwin( 1, 55, point( ( TERMX / 2 ) + getbegx( w ), 6 + getbegy( w ) ) ); + catacurses::window w_items = + catacurses::newwin( iContentHeight - 2, 55, + point( ( TERMX / 2 ) + getbegx( w ), 7 + getbegy( w ) ) ); input_context ctxt( "NEW_CHAR_PROFESSIONS" ); - tabs.set_up_tab_navigation( ctxt ); - details.set_up_navigation( ctxt, scrolling_key_scheme::angle_bracket_scroll ); - list_sb.set_draggable( ctxt ); - ctxt.register_navigate_ui_list(); + ctxt.register_cardinal(); ctxt.register_action( "CONFIRM" ); ctxt.register_action( "CHANGE_GENDER" ); - ctxt.register_action( "CHANGE_OUTFIT" ); + ctxt.register_action( "PREV_TAB" ); + ctxt.register_action( "NEXT_TAB" ); ctxt.register_action( "SORT" ); ctxt.register_action( "HELP_KEYBINDINGS" ); ctxt.register_action( "FILTER" ); - ctxt.register_action( "RESET_FILTER" ); - ctxt.register_action( "RANDOMIZE" ); + ctxt.register_action( "QUIT" ); bool recalc_profs = true; - size_t profs_length = 0; + int profs_length = 0; std::string filterstring; std::vector> sorted_profs; - ui.on_redraw( [&]( ui_adaptor & ui ) { - werase( w ); - tabs.draw( w ); - mvwputch( w, point( TERMX / 2, iHeaderHeight - 1 ), BORDER_COLOR, LINE_OXXX ); // '┬' - mvwputch( w, point( TERMX / 2, TERMY - 1 ), BORDER_COLOR, LINE_XXOX ); // 'â”´' - draw_filter_and_sorting_indicators( w, ctxt, filterstring, profession_sorter ); - - const bool cur_id_is_valid = cur_id >= 0 && static_cast( cur_id ) < sorted_profs.size(); - if( cur_id_is_valid ) { - int netPointCost = sorted_profs[cur_id]->point_cost() - u.prof->point_cost(); - ret_val can_afford = sorted_profs[cur_id]->can_afford( u, skill_points_left( u, pool ) ); - ret_val can_pick = sorted_profs[cur_id]->can_pick(); - int pointsForProf = sorted_profs[cur_id]->point_cost(); - bool negativeProf = pointsForProf < 0; - if( negativeProf ) { - pointsForProf *= -1; - } - - // Draw header. - draw_points( w, pool, u, netPointCost ); - if( pool != pool_type::FREEFORM ) { - const char *prof_msg_temp; - if( negativeProf ) { - //~ 1s - profession name, 2d - current character points. - prof_msg_temp = n_gettext( "Profession %1$s earns %2$d point", - "Profession %1$s earns %2$d points", - pointsForProf ); - } else { - //~ 1s - profession name, 2d - current character points. - prof_msg_temp = n_gettext( "Profession %1$s costs %2$d point", - "Profession %1$s costs %2$d points", - pointsForProf ); - } - - int pMsg_length = utf8_width( remove_color_tags( pools_to_string( u, pool ) ) ); - mvwprintz( w, point( pMsg_length + 9, 3 ), can_afford.success() ? c_green : c_light_red, - prof_msg_temp, sorted_profs[cur_id]->gender_appropriate_name( u.male ), pointsForProf ); - } - } - - //Draw options - calcStartPos( iStartPos, cur_id, iContentHeight, profs_length ); - const int end_pos = iStartPos + std::min( iContentHeight, profs_length ); - std::string cur_prof_notes; - for( int i = iStartPos; i < end_pos; i++ ) { - nc_color col; - if( u.prof != &sorted_profs[i].obj() ) { - - if( cur_id_is_valid && sorted_profs[i] == sorted_profs[cur_id] && - !sorted_profs[i]->can_pick().success() ) { - col = h_dark_gray; - if( i == cur_id ) { - cur_prof_notes = _( "unavailable" ); - } - } else if( cur_id_is_valid && sorted_profs[i] != sorted_profs[cur_id] && - !sorted_profs[i]->can_pick().success() ) { - col = c_dark_gray; - if( i == cur_id ) { - cur_prof_notes = _( "unavailable" ); - } - } else { - col = ( cur_id_is_valid && sorted_profs[i] == sorted_profs[cur_id] ? COL_SELECT : c_light_gray ); - } - } else { - col = ( cur_id_is_valid && - sorted_profs[i] == sorted_profs[cur_id] ? hilite( c_light_green ) : COL_SKILL_USED ); - if( i == cur_id ) { - cur_prof_notes = _( "active" ); - } - } - if( screen_reader_mode ) { - // This list only clutters up the screen in screen reader mode - } else { - const point opt_pos( 2, iHeaderHeight + i - iStartPos ); - mvwprintz( w, opt_pos, col, - sorted_profs[i]->gender_appropriate_name( u.male ) ); - } - } - - if( details_recalc && cur_id_is_valid ) { - details.set_text( assemble_profession_details( u, ctxt, sorted_profs, cur_id, cur_prof_notes ) ); - details_recalc = false; - } - - list_sb.offset_x( 0 ) - .offset_y( iHeaderHeight ) - .content_size( profs_length ) - .viewport_pos( iStartPos ) - .viewport_size( iContentHeight ) - .apply( w ); - - wnoutrefresh( w ); - ui.set_cursor( w_details_pane, point_zero ); - details.draw( c_light_gray ); - } ); + if( direction == tab_direction::FORWARD ) { + points.skill_points -= u.prof->point_cost(); + } do { if( recalc_profs ) { - std::vector new_profs = get_scenario()->permitted_professions(); - profession_sorter.male = u.male; - if( ( profs_length = filter_entries( u, cur_id, sorted_profs, new_profs, u.prof->ident(), - filterstring, - profession_sorter ) ) == 0 ) { + sorted_profs = g->scen->permitted_professions(); + const auto new_end = std::remove_if( sorted_profs.begin(), + sorted_profs.end(), [&]( const string_id &arg ) { + return !lcmatch( arg->gender_appropriate_name( u.male ), filterstring ); + } ); + sorted_profs.erase( new_end, sorted_profs.end() ); + profs_length = sorted_profs.size(); + if( profs_length == 0 ) { + popup( _( "Nothing found." ) ); // another case of black box in tiles filterstring.clear(); continue; } - recalc_profs = false; - } - - ui_manager::redraw(); - const std::string action = ctxt.handle_input(); - const int recmax = profs_length; - const int scroll_rate = recmax > 20 ? 10 : 2; - const int id_for_curr_description = cur_id; - int scrollbar_pos = iStartPos; - - if( tabs.handle_input( action, ctxt ) ) { - break; // Tab has changed or user has quit the screen - } else if( details.handle_navigation( action, ctxt ) - || navigate_ui_list( action, cur_id, scroll_rate, recmax, true ) ) { - // NO FURTHER ACTION REQUIRED - } else if( list_sb.handle_dragging( action, ctxt.get_coordinates_text( catacurses::stdscr ), - scrollbar_pos ) ) { - if( scrollbar_pos != iStartPos ) { - iStartPos = scrollbar_pos; - cur_id = iStartPos + ( iContentHeight - 1 ) / 2; - } - } else if( action == "CONFIRM" ) { - ret_val can_pick = sorted_profs[cur_id]->can_pick(); - - if( !can_pick.success() ) { - popup( can_pick.str() ); - continue; - } - - // Selecting a profession will, under certain circumstances, change the detail text - details_recalc = true; - - // Remove traits from the previous profession - for( const trait_and_var &old : u.prof->get_locked_traits() ) { - u.toggle_trait_deps( old.trait ); - } - u.prof = &sorted_profs[cur_id].obj(); + // Sort professions by points. + // profession_display_sort() keeps "unemployed" at the top. + profession_sorter.male = u.male; + std::stable_sort( sorted_profs.begin(), sorted_profs.end(), profession_sorter ); - // Remove pre-selected traits that conflict - // with the new profession's traits - for( const trait_and_var &new_trait : u.prof->get_locked_traits() ) { - if( u.has_conflicting_trait( new_trait.trait ) ) { - for( const trait_id &suspect_trait : u.get_mutations() ) { - if( are_conflicting_traits( new_trait.trait, suspect_trait ) ) { - popup( _( "Your trait %1$s has been removed since it conflicts with the %2$s's %3$s trait." ), - u.mutation_name( suspect_trait ), u.prof->gender_appropriate_name( u.male ), new_trait.name() ); - u.toggle_trait_deps( suspect_trait ); - } - } + // Select the current profession, if possible. + for( int i = 0; i < profs_length; ++i ) { + if( sorted_profs[i] == u.prof->ident() ) { + cur_id = i; + break; } } - // Add traits for the new profession (and perhaps scenario, if, for example, - // both the scenario and old profession require the same trait) - u.add_traits(); - } else if( action == "CHANGE_OUTFIT" ) { - outfit = !outfit; - recalc_profs = true; - } else if( action == "CHANGE_GENDER" ) { - u.male = !u.male; - profession_sorter.male = u.male; - if( !profession_sorter.sort_by_points ) { - std::sort( sorted_profs.begin(), sorted_profs.end(), profession_sorter ); - } - recalc_profs = true; - } else if( action == "SORT" ) { - profession_sorter.sort_by_points = !profession_sorter.sort_by_points; - recalc_profs = true; - } else if( action == "FILTER" ) { - string_input_popup() - .title( _( "Search:" ) ) - .width( 60 ) - .description( _( "Search by profession name." ) ) - .edit( filterstring ); - recalc_profs = true; - } else if( action == "RESET_FILTER" ) { - if( !filterstring.empty() ) { - filterstring.clear(); - recalc_profs = true; + if( cur_id > profs_length - 1 ) { + cur_id = 0; } - } else if( action == "RANDOMIZE" ) { - cur_id = rng( 0, profs_length - 1 ); - } - if( cur_id != id_for_curr_description || recalc_profs ) { - details_recalc = true; - } - - } while( true ); -} -static std::string assemble_hobby_details( const avatar &u, const input_context &ctxt, - const std::vector> &sorted_hobbies, const int cur_id, - const std::string ¬es ) -{ - std::string assembled; - - std::string hobby_name = sorted_hobbies[cur_id]->gender_appropriate_name( u.male ); - if( get_option( "SCREEN_READER_MODE" ) && !notes.empty() ) { - hobby_name = hobby_name.append( string_format( " - %s", notes ) ); - } + // Draw filter indicator + for( int i = 1; i < TERMX - 1; i++ ) { + mvwputch( w, point( i, TERMY - 1 ), BORDER_COLOR, LINE_OXOX ); + } + const auto filter_indicator = filterstring.empty() ? _( "no filter" ) + : filterstring; + mvwprintz( w, point( 2, getmaxy( w ) - 1 ), c_light_gray, "<%s>", filter_indicator ); - assembled += string_format( g_switch_msg( u ), ctxt.get_desc( "CHANGE_GENDER" ), - hobby_name ) + "\n"; - assembled += string_format( dress_switch_msg(), ctxt.get_desc( "CHANGE_OUTFIT" ) ) + "\n"; + recalc_profs = false; + } - assembled += "\n" + colorize( _( "Background story:" ), COL_HEADER ) + "\n"; - assembled += colorize( sorted_hobbies[cur_id]->description( u.male ), c_green ) + "\n"; + int netPointCost = sorted_profs[cur_id]->point_cost() - u.prof->point_cost(); + bool can_pick = sorted_profs[cur_id]->can_pick( u, points.skill_points_left() ); + const std::string clear_line( getmaxx( w ) - 2, ' ' ); - // Background addictions - const auto prof_addictions = sorted_hobbies[cur_id]->addictions(); - if( !prof_addictions.empty() ) { - assembled += "\n" + colorize( _( "Background addictions:" ), COL_HEADER ) + "\n"; - for( const addiction &a : prof_addictions ) { - const char *format = pgettext( "set_profession_addictions", "%1$s (%2$d)" ); - assembled += string_format( format, a.type->get_name().translated(), a.intensity ) + "\n"; + // Clear the bottom of the screen and header. + werase( w_description ); + mvwprintz( w, point( 1, 3 ), c_light_gray, clear_line ); + + int pointsForProf = sorted_profs[cur_id]->point_cost(); + bool negativeProf = pointsForProf < 0; + if( negativeProf ) { + pointsForProf *= -1; + } + // Draw header. + draw_points( w, points, netPointCost ); + std::string prof_msg_temp; + if( negativeProf ) { + //~ 1s - profession name, 2d - current character points. + prof_msg_temp = ngettext( "Profession %1$s earns %2$d point", + "Profession %1$s earns %2$d points", + pointsForProf ); + } else { + //~ 1s - profession name, 2d - current character points. + prof_msg_temp = ngettext( "Profession %1$s costs %2$d point", + "Profession %1$s costs %2$d points", + pointsForProf ); } - } - // Background traits - const auto prof_traits = sorted_hobbies[cur_id]->get_locked_traits(); - assembled += "\n" + colorize( _( "Background traits:" ), COL_HEADER ) + "\n"; - if( prof_traits.empty() ) { - assembled += pgettext( "set_profession_trait", "None" ) + std::string( "\n" ); - } else { - for( const trait_and_var &t : prof_traits ) { - assembled += t.name() + "\n"; - } - } + int pMsg_length = utf8_width( remove_color_tags( points.to_string() ) ); + mvwprintz( w, point( pMsg_length + 9, 3 ), can_pick ? c_green : c_light_red, prof_msg_temp.c_str(), + sorted_profs[cur_id]->gender_appropriate_name( u.male ), + pointsForProf ); - // Background skills - const profession::StartingSkillList prof_skills = sorted_hobbies[cur_id]->skills(); - assembled += "\n" + colorize( _( "Background skill experience:" ), COL_HEADER ) + "\n"; - if( prof_skills.empty() ) { - assembled += pgettext( "set_profession_skill", "None" ) + std::string( "\n" ); - } else { - for( const auto &sl : prof_skills ) { - const Skill &skill = sl.first.obj(); - const int level = sl.second; - if( level < 1 ) { - debugmsg( "Unexpected skill level for %s: %d", skill.ident().str(), level ); - continue; - } - std::string skill_degree; - if( level == 1 ) { - skill_degree = pgettext( "set_profession_skill", "beginner" ); - } else if( level == 2 ) { - skill_degree = pgettext( "set_profession_skill", "intermediate" ); - } else if( level == 3 ) { - skill_degree = pgettext( "set_profession_skill", "competent" ); + fold_and_print( w_description, point_zero, TERMX - 2, c_green, + sorted_profs[cur_id]->description( u.male ) ); + + //Draw options + calcStartPos( iStartPos, cur_id, iContentHeight, profs_length ); + const int end_pos = iStartPos + ( ( iContentHeight > profs_length ) ? + profs_length : iContentHeight ); + int i; + for( i = iStartPos; i < end_pos; i++ ) { + mvwprintz( w, point( 2, 5 + i - iStartPos ), c_light_gray, + " " ); // Clear the line + nc_color col; + if( u.prof != &sorted_profs[i].obj() ) { + col = ( sorted_profs[i] == sorted_profs[cur_id] ? h_light_gray : c_light_gray ); } else { - skill_degree = pgettext( "set_profession_skill", "advanced" ); + col = ( sorted_profs[i] == sorted_profs[cur_id] ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ); } - assembled += string_format( "%s (%s)", skill.name(), skill_degree ) + "\n"; + mvwprintz( w, point( 2, 5 + i - iStartPos ), col, + sorted_profs[i]->gender_appropriate_name( u.male ) ); } - } - - // Background Proficiencies - std::vector prof_proficiencies = sorted_hobbies[cur_id]->proficiencies(); - if( !prof_proficiencies.empty() ) { - assembled += "\n" + colorize( _( "Background proficiencies:" ), COL_HEADER ) + "\n"; - for( const proficiency_id &prof : prof_proficiencies ) { - assembled += prof->name() + ": " + colorize( prof->description(), COL_HEADER ) + "\n"; + //Clear rest of space in case stuff got filtered out + for( ; i < iStartPos + iContentHeight; ++i ) { + mvwprintz( w, point( 2, 5 + i - iStartPos ), c_light_gray, + " " ); // Clear the line } - } - auto prof_CBMs = sorted_hobbies[cur_id]->CBMs(); - if( !prof_CBMs.empty() ) { - assembled += "\n" + colorize( _( "Background bionics:" ), COL_HEADER ) + "\n"; - std::sort( std::begin( prof_CBMs ), std::end( prof_CBMs ), []( const bionic_id & a, - const bionic_id & b ) { - return a->activated && !b->activated; - } ); - for( const auto &b : prof_CBMs ) { - const bionic_data &cbm = b.obj(); - if( cbm.activated && cbm.has_flag( json_flag_BIONIC_TOGGLED ) ) { - assembled += string_format( _( "%s (toggled)" ), cbm.name ) + "\n"; - } else if( cbm.activated ) { - assembled += string_format( _( "%s (activated)" ), cbm.name ) + "\n"; - } else { - assembled += cbm.name + "\n"; + std::ostringstream buffer; + // Profession addictions + const auto prof_addictions = sorted_profs[cur_id]->addictions(); + if( !prof_addictions.empty() ) { + buffer << colorize( _( "Addictions:" ), c_light_blue ) << "\n"; + for( const auto &a : prof_addictions ) { + const auto format = pgettext( "set_profession_addictions", "%1$s (%2$d)" ); + buffer << string_format( format, addiction_name( a ), a.intensity ) << "\n"; } } - } - // Background spells - if( !sorted_hobbies[cur_id]->spells().empty() ) { - assembled += "\n" + colorize( _( "Background spells:" ), COL_HEADER ) + "\n"; - for( const std::pair spell_pair : sorted_hobbies[cur_id]->spells() ) { - assembled += string_format( _( "%s level %d" ), spell_pair.first->name, spell_pair.second ) + "\n"; - } - } - - // Background missions - if( !sorted_hobbies[cur_id]->missions().empty() ) { - assembled += "\n" + colorize( _( "Background missions:" ), COL_HEADER ) + "\n"; - for( mission_type_id mission_id : sorted_hobbies[cur_id]->missions() ) { - assembled += mission_type::get( mission_id )->tname() + "\n"; + // Profession traits + const auto prof_traits = sorted_profs[cur_id]->get_locked_traits(); + buffer << colorize( _( "Profession traits:" ), c_light_blue ) << "\n"; + if( prof_traits.empty() ) { + buffer << pgettext( "set_profession_trait", "None" ) << "\n"; + } else { + for( const auto &t : prof_traits ) { + buffer << mutation_branch::get_name( t ) << "\n"; + } } - } - - return assembled; -} - -/** Handle the hobbies tab of the character generation menu */ -void set_hobbies( tab_manager &tabs, avatar &u, pool_type pool ) -{ - int cur_id = 0; - size_t iContentHeight = 0; - int iStartPos = 0; - - const bool screen_reader_mode = get_option( "SCREEN_READER_MODE" ); - - ui_adaptor ui; - catacurses::window w; - catacurses::window w_details_pane; - scrolling_text_view details( w_details_pane ); - bool details_recalc = true; - const int iHeaderHeight = 6; - scrollbar list_sb; - - const auto init_windows = [&]( ui_adaptor & ui ) { - iContentHeight = TERMY - iHeaderHeight - 1; - w = catacurses::newwin( TERMY, TERMX, point_zero ); - w_details_pane = catacurses::newwin( iContentHeight, TERMX / 2 - 1, point( TERMX / 2, - iHeaderHeight ) ); - details_recalc = true; - ui.position_from_window( w ); - }; - init_windows( ui ); - ui.on_screen_resize( init_windows ); - - input_context ctxt( "NEW_CHAR_PROFESSIONS" ); - tabs.set_up_tab_navigation( ctxt ); - details.set_up_navigation( ctxt, scrolling_key_scheme::angle_bracket_scroll ); - list_sb.set_draggable( ctxt ); - ctxt.register_navigate_ui_list(); - ctxt.register_action( "CONFIRM" ); - ctxt.register_action( "CHANGE_GENDER" ); - ctxt.register_action( "CHANGE_OUTFIT" ); - ctxt.register_action( "SORT" ); - ctxt.register_action( "HELP_KEYBINDINGS" ); - ctxt.register_action( "FILTER" ); - ctxt.register_action( "RESET_FILTER" ); - ctxt.register_action( "RANDOMIZE" ); - bool recalc_hobbies = true; - size_t hobbies_length = 0; - std::string filterstring; - std::vector> sorted_hobbies; - - ui.on_redraw( [&]( ui_adaptor & ui ) { - werase( w ); - tabs.draw( w ); - mvwputch( w, point( TERMX / 2, iHeaderHeight - 1 ), BORDER_COLOR, LINE_OXXX ); // '┬' - mvwputch( w, point( TERMX / 2, TERMY - 1 ), BORDER_COLOR, LINE_XXOX ); // 'â”´' - draw_filter_and_sorting_indicators( w, ctxt, filterstring, profession_sorter ); - - const bool cur_id_is_valid = cur_id >= 0 && static_cast( cur_id ) < sorted_hobbies.size(); - if( cur_id_is_valid ) { - int netPointCost = sorted_hobbies[cur_id]->point_cost() - u.prof->point_cost(); - ret_val can_pick = sorted_hobbies[cur_id]->can_afford( u, skill_points_left( u, pool ) ); - int pointsForProf = sorted_hobbies[cur_id]->point_cost(); - bool negativeProf = pointsForProf < 0; - if( negativeProf ) { - pointsForProf *= -1; + // Profession skills + const auto prof_skills = sorted_profs[cur_id]->skills(); + buffer << colorize( _( "Profession skills:" ), c_light_blue ) << "\n"; + if( prof_skills.empty() ) { + buffer << pgettext( "set_profession_skill", "None" ) << "\n"; + } else { + for( const auto &sl : prof_skills ) { + const auto format = pgettext( "set_profession_skill", "%1$s (%2$d)" ); + buffer << string_format( format, sl.first.obj().name(), sl.second ) << "\n"; } + } - // Draw header. - draw_points( w, pool, u, netPointCost ); - if( pool != pool_type::FREEFORM ) { - const char *prof_msg_temp; - if( negativeProf ) { - //~ 1s - profession name, 2d - current character points. - prof_msg_temp = n_gettext( "Background %1$s earns %2$d point", - "Background %1$s earns %2$d points", - pointsForProf ); + // Profession items + const auto prof_items = sorted_profs[cur_id]->items( u.male, u.get_mutations() ); + buffer << colorize( _( "Profession items:" ), c_light_blue ) << "\n"; + if( prof_items.empty() ) { + buffer << pgettext( "set_profession_item", "None" ) << "\n"; + } else { + // TODO: If the item group is randomized *at all*, these will be different each time + // and it won't match what you actually start with + // TODO: Put like items together like the inventory does, so we don't have to scroll + // through a list of a dozen forks. + std::ostringstream buffer_wielded; + std::ostringstream buffer_worn; + std::ostringstream buffer_inventory; + for( const auto &it : prof_items ) { + if( it.has_flag( "no_auto_equip" ) ) { + buffer_inventory << it.display_name() << "\n"; + } else if( it.has_flag( "auto_wield" ) ) { + buffer_wielded << it.display_name() << "\n"; + } else if( it.is_armor() ) { + buffer_worn << it.display_name() << "\n"; } else { - //~ 1s - profession name, 2d - current character points. - prof_msg_temp = n_gettext( "Background %1$s costs %2$d point", - "Background %1$s costs %2$d points", - pointsForProf ); + buffer_inventory << it.display_name() << "\n"; } - - int pMsg_length = utf8_width( remove_color_tags( pools_to_string( u, pool ) ) ); - mvwprintz( w, point( pMsg_length + 9, 3 ), can_pick.success() ? c_green : c_light_red, - prof_msg_temp, sorted_hobbies[cur_id]->gender_appropriate_name( u.male ), pointsForProf ); } + buffer << colorize( _( "Wielded:" ), c_cyan ) << "\n"; + buffer << ( !buffer_wielded.str().empty() ? buffer_wielded.str() : + pgettext( "set_profession_item_wielded", "None\n" ) ); + buffer << colorize( _( "Worn:" ), c_cyan ) << "\n"; + buffer << ( !buffer_worn.str().empty() ? buffer_worn.str() : + pgettext( "set_profession_item_worn", "None\n" ) ); + buffer << colorize( _( "Inventory:" ), c_cyan ) << "\n"; + buffer << ( !buffer_inventory.str().empty() ? buffer_inventory.str() : + pgettext( "set_profession_item_inventory", "None\n" ) ); } - //Draw options - calcStartPos( iStartPos, cur_id, iContentHeight, hobbies_length ); - const int end_pos = iStartPos + std::min( iContentHeight, hobbies_length ); - std::string cur_hob_notes; - for( int i = iStartPos; i < end_pos; i++ ) { - nc_color col; - if( u.hobbies.count( &sorted_hobbies[i].obj() ) != 0 ) { - col = ( cur_id_is_valid && - sorted_hobbies[i] == sorted_hobbies[cur_id] ? hilite( c_light_green ) : COL_SKILL_USED ); - if( i == cur_id ) { - cur_hob_notes = _( "active" ); + // Profession bionics, active bionics shown first + auto prof_CBMs = sorted_profs[cur_id]->CBMs(); + std::sort( begin( prof_CBMs ), end( prof_CBMs ), []( const bionic_id & a, const bionic_id & b ) { + return a->activated && !b->activated; + } ); + buffer << colorize( _( "Profession bionics:" ), c_light_blue ) << "\n"; + if( prof_CBMs.empty() ) { + buffer << pgettext( "set_profession_bionic", "None" ) << "\n"; + } else { + for( const auto &b : prof_CBMs ) { + const auto &cbm = b.obj(); + + if( cbm.activated && cbm.toggled ) { + buffer << cbm.name << " (" << _( "toggled" ) << ")\n"; + } else if( cbm.activated ) { + buffer << cbm.name << " (" << _( "activated" ) << ")\n"; + } else { + buffer << cbm.name << "\n"; } - } else { - col = ( cur_id_is_valid && - sorted_hobbies[i] == sorted_hobbies[cur_id] ? COL_SELECT : c_light_gray ); } - - const point opt_pos( 2, iHeaderHeight + i - iStartPos ); - if( screen_reader_mode ) { - // This list only clutters up the screen in screen reader mode - } else { - mvwprintz( w, opt_pos, col, - sorted_hobbies[i]->gender_appropriate_name( u.male ) ); + } + // Profession pet + cata::optional montype; + if( !sorted_profs[cur_id]->pets().empty() ) { + buffer << colorize( _( "Pets:" ), c_light_blue ) << "\n"; + for( auto elem : sorted_profs[cur_id]->pets() ) { + monster mon( elem ); + buffer << mon.get_name() << "\n"; } } - - if( details_recalc && cur_id_is_valid ) { - details.set_text( assemble_hobby_details( u, ctxt, sorted_hobbies, cur_id, cur_hob_notes ) ); - details_recalc = false; + // Profession spells + if( !sorted_profs[cur_id]->spells().empty() ) { + buffer << colorize( _( "Spells:" ), c_light_blue ) << "\n"; + for( const std::pair spell_pair : sorted_profs[cur_id]->spells() ) { + buffer << spell_pair.first->name << _( " level " ) << spell_pair.second << "\n"; + } } + werase( w_items ); + const auto scroll_msg = string_format( + _( "Press %1$s or %2$s to scroll." ), + ctxt.get_desc( "LEFT" ), + ctxt.get_desc( "RIGHT" ) ); + const int iheight = print_scrollable( w_items, desc_offset, buffer.str(), c_light_gray, + scroll_msg ); + werase( w_sorting ); + draw_sorting_indicator( w_sorting, ctxt, profession_sorter ); - list_sb.offset_x( 0 ) - .offset_y( 6 ) - .content_size( hobbies_length ) - .viewport_pos( iStartPos ) - .viewport_size( iContentHeight ) - .apply( w ); + werase( w_genderswap ); + //~ Gender switch message. 1s - change key name, 2s - profession name. + std::string g_switch_msg = u.male ? _( "Press %1$s to switch to %2$s( female )." ) : + _( "Press %1$s to switch to %2$s(male)." ); + mvwprintz( w_genderswap, point_zero, c_magenta, g_switch_msg.c_str(), + ctxt.get_desc( "CHANGE_GENDER" ), + sorted_profs[cur_id]->gender_appropriate_name( !u.male ) ); - wnoutrefresh( w ); - ui.set_cursor( w_details_pane, point_zero ); - details.draw( c_light_gray ); - } ); + draw_scrollbar( w, cur_id, iContentHeight, profs_length, point( 0, 5 ) ); - do { - if( recalc_hobbies ) { - std::vector new_hobbies = get_scenario()->permitted_hobbies(); - new_hobbies.erase( std::remove_if( new_hobbies.begin(), new_hobbies.end(), - [&u]( const string_id &hobby ) { - return !u.prof->allows_hobby( hobby ); - } ), new_hobbies.end() ); - if( new_hobbies.empty() ) { - debugmsg( "Why would you blacklist all hobbies?" ); - new_hobbies = profession::get_all_hobbies(); - } - profession_sorter.male = u.male; - if( ( hobbies_length = filter_entries( u, cur_id, sorted_hobbies, new_hobbies, - u.hobbies.empty() ? string_id() : ( *u.hobbies.begin() )->ident(), filterstring, - profession_sorter ) ) == 0 ) { - filterstring.clear(); - continue; - } - recalc_hobbies = false; - } + wrefresh( w ); + wrefresh( w_description ); + wrefresh( w_items ); + wrefresh( w_genderswap ); + wrefresh( w_sorting ); - ui_manager::redraw(); - const int id_for_curr_description = cur_id; const std::string action = ctxt.handle_input(); - const int recmax = hobbies_length; - const int scroll_rate = recmax > 20 ? 10 : 2; - int scrollbar_pos = iStartPos; - - if( tabs.handle_input( action, ctxt ) ) { - break; // Tab has changed or user has quit the screen - } else if( details.handle_navigation( action, ctxt ) - || navigate_ui_list( action, cur_id, scroll_rate, recmax, true ) ) { - // NO FURTHER ACTION REQUIRED - } else if( list_sb.handle_dragging( action, ctxt.get_coordinates_text( catacurses::stdscr ), - scrollbar_pos ) ) { - if( scrollbar_pos != iStartPos ) { - iStartPos = scrollbar_pos; - cur_id = iStartPos + ( iContentHeight - 1 ) / 2; + if( action == "DOWN" ) { + cur_id++; + if( cur_id > static_cast( profs_length ) - 1 ) { + cur_id = 0; } - } else if( action == "CONFIRM" ) { - // Do not allow selection of hobby if there's a trait conflict - const profession *hobb = &sorted_hobbies[cur_id].obj(); - bool conflict_found = false; - for( const trait_and_var &new_trait : hobb->get_locked_traits() ) { - if( u.has_conflicting_trait( new_trait.trait ) ) { - for( const profession *hobby : u.hobbies ) { - for( const trait_and_var &suspect : hobby->get_locked_traits() ) { - if( are_conflicting_traits( new_trait.trait, suspect.trait ) ) { - conflict_found = true; - popup( _( "The trait [%1$s] conflicts with background [%2$s]'s trait [%3$s]." ), new_trait.name(), - hobby->gender_appropriate_name( u.male ), suspect.name() ); - } - } - } - } + desc_offset = 0; + } else if( action == "UP" ) { + cur_id--; + if( cur_id < 0 ) { + cur_id = profs_length - 1; } - if( conflict_found ) { - continue; + desc_offset = 0; + } else if( action == "LEFT" ) { + if( desc_offset > 0 ) { + desc_offset--; } - - // Toggle hobby - bool enabling = false; - if( u.hobbies.count( hobb ) == 0 ) { - // Add hobby, and decrement point cost - u.hobbies.insert( hobb ); - enabling = true; - } else { - // Remove hobby and refund point cost - u.hobbies.erase( hobb ); + } else if( action == "RIGHT" ) { + if( desc_offset < iheight ) { + desc_offset++; } - - // Selecting a hobby will, under certain circumstances, change the detail text - details_recalc = true; - - // Add or remove traits from hobby - for( const trait_and_var &cur : hobb->get_locked_traits() ) { - const trait_id &trait = cur.trait; - if( enabling ) { - if( !u.has_trait( trait ) ) { - u.toggle_trait_deps( trait ); - } - continue; - } - int from_other_hobbies = u.prof->is_locked_trait( trait ) ? 1 : 0; - for( const profession *hby : u.hobbies ) { - if( hby->ident() != hobb->ident() && hby->is_locked_trait( trait ) ) { - from_other_hobbies++; - } - } - if( from_other_hobbies > 0 ) { - continue; - } - u.toggle_trait_deps( trait ); + } else if( action == "CONFIRM" ) { + // Remove traits from the previous profession + for( const trait_id &old_trait : u.prof->get_locked_traits() ) { + u.toggle_trait( old_trait ); } - - } else if( action == "CHANGE_OUTFIT" ) { - outfit = !outfit; - recalc_hobbies = true; + u.prof = &sorted_profs[cur_id].obj(); + // Add traits for the new profession (and perhaps scenario, if, for example, + // both the scenario and old profession require the same trait) + u.add_traits( points ); + points.skill_points -= netPointCost; } else if( action == "CHANGE_GENDER" ) { u.male = !u.male; profession_sorter.male = u.male; if( !profession_sorter.sort_by_points ) { - std::sort( sorted_hobbies.begin(), sorted_hobbies.end(), profession_sorter ); + std::sort( sorted_profs.begin(), sorted_profs.end(), profession_sorter ); } - recalc_hobbies = true; + } else if( action == "PREV_TAB" ) { + retval = tab_direction::BACKWARD; + } else if( action == "NEXT_TAB" ) { + retval = tab_direction::FORWARD; } else if( action == "SORT" ) { profession_sorter.sort_by_points = !profession_sorter.sort_by_points; - recalc_hobbies = true; + recalc_profs = true; } else if( action == "FILTER" ) { string_input_popup() - .title( _( "Search:" ) ) - .width( 60 ) - .description( _( "Search by background name." ) ) - .edit( filterstring ); - recalc_hobbies = true; - } else if( action == "RESET_FILTER" ) { - if( !filterstring.empty() ) { - filterstring.clear(); - recalc_hobbies = true; - } - } else if( action == "RANDOMIZE" ) { - cur_id = rng( 0, hobbies_length - 1 ); - } - - if( cur_id != id_for_curr_description || recalc_hobbies ) { - details_recalc = true; - } - } while( true ); -} - -/** - * @return The skill points to consume when a skill is increased (by one level) from the - * current level. - * - * @note: There is one exception: if the current level is 0, it can be boosted by 2 levels for 1 point. - */ -static int skill_increment_cost( const Character &u, const skill_id &skill ) -{ - return std::max( 1, ( static_cast( u.get_skill_level( skill ) ) + 1 ) / 2 ); -} - -static std::string assemble_skill_details( const avatar &u, - const std::map &prof_skills, const Skill *currentSkill ) -{ - std::string assembled; - // We want recipes from profession skills displayed, but - // without boosting the skills. Copy the skills, and boost the copy - SkillLevelMap with_prof_skills = u.get_all_skills(); - for( const auto &sk : prof_skills ) { - with_prof_skills.mod_skill_level( sk.first, sk.second ); - } - - std::map > > recipes; - for( const auto &e : recipe_dict ) { - const recipe &r = e.second; - if( r.is_practice() || r.has_flag( "SECRET" ) ) { - continue; - } - //Find out if the current skill and its level is in the requirement list - auto req_skill = r.required_skills.find( currentSkill->ident() ); - int skill = req_skill != r.required_skills.end() ? req_skill->second : 0; - bool would_autolearn_recipe = - recipe_dict.all_autolearn().count( &r ) && - with_prof_skills.meets_skill_requirements( r.autolearn_requirements ); - - if( !would_autolearn_recipe && !r.never_learn && - ( r.skill_used == currentSkill->ident() || skill > 0 ) && - with_prof_skills.has_recipe_requirements( r ) ) { - - recipes[r.skill_used->name()].emplace_back( r.result_name( /*decorated=*/true ), - ( skill > 0 ) ? skill : r.difficulty ); - } - // TODO: Find out why kevlar gambeson hood disppears when going from tailoring 7->8 - } - - for( auto &elem : recipes ) { - std::sort( elem.second.begin(), elem.second.end(), - []( const std::pair &lhs, - const std::pair &rhs ) { - return localized_compare( std::make_pair( lhs.second, lhs.first ), - std::make_pair( rhs.second, rhs.first ) ); - } ); - - const std::string rec_temp = enumerate_as_string( elem.second.begin(), elem.second.end(), - []( const std::pair &rec ) { - return string_format( "%s (%d)", rec.first, rec.second ); - } ); - - if( elem.first == currentSkill->name() ) { - assembled = "\n\n" + colorize( rec_temp, c_brown ) + std::move( assembled ); - } else { - assembled += "\n\n" + colorize( "[" + elem.first + "]\n" + rec_temp, c_light_gray ); + .title( _( "Search:" ) ) + .width( 60 ) + .description( _( "Search by profession name." ) ) + .edit( filterstring ); + recalc_profs = true; + } else if( action == "HELP_KEYBINDINGS" ) { + // Need to redraw since the help window obscured everything. + draw_character_tabs( w, _( "PROFESSION" ) ); + } else if( action == "QUIT" && query_yn( _( "Return to main menu?" ) ) ) { + retval = tab_direction::QUIT; } - } - assembled = currentSkill->description() + assembled; - return assembled; + } while( retval == tab_direction::NONE ); + + return retval; } -static std::string assemble_skill_help( const input_context &ctxt ) +/** + * @return The skill points to consume when a skill is increased (by one level) from the + * current level. + * + * @note: There is one exception: if the current level is 0, it can be boosted by 2 levels for 1 point. + */ +static int skill_increment_cost( const Character &u, const skill_id &skill ) { - return string_format( - _( "Press %s to view and alter keybindings.\n" - "Press %s / %s to select skill.\n" - "Press %s to increase skill or " - "%s to decrease skill.\n" - "Press %s to go to the next tab or " - "%s to return to the previous tab." ), - ctxt.get_desc( "HELP_KEYBINDINGS" ), ctxt.get_desc( "UP" ), ctxt.get_desc( "DOWN" ), - ctxt.get_desc( "RIGHT" ), ctxt.get_desc( "LEFT" ), - ctxt.get_desc( "NEXT_TAB" ), ctxt.get_desc( "PREV_TAB" ) ); + return std::max( 1, ( u.get_skill_level( skill ) + 1 ) / 2 ); } -void set_skills( tab_manager &tabs, avatar &u, pool_type pool ) +tab_direction set_skills( const catacurses::window &w, avatar &u, points_left &points ) { - const bool screen_reader_mode = get_option( "SCREEN_READER_MODE" ); - ui_adaptor ui; - catacurses::window w; - catacurses::window w_list; - catacurses::window w_details_pane; - scrolling_text_view details( w_details_pane ); - bool details_recalc = true; - const int iSecondColumn = 31; - int iContentHeight = 0; - const int iHeaderHeight = 6; - // guessing most likely, but it doesn't matter, it will be recalculated if wrong - int iHelpHeight = 4; - scrollbar list_sb; + draw_character_tabs( w, _( "SKILLS" ) ); + const int iContentHeight = TERMY - 6; + catacurses::window w_description = catacurses::newwin( iContentHeight, TERMX - 35, + point( 31 + getbegx( w ), 5 + getbegy( w ) ) ); + + auto sorted_skills = Skill::get_skills_sorted_by( []( const Skill & a, const Skill & b ) { + return a.name() < b.name(); + } ); + + const int num_skills = sorted_skills.size(); + int cur_offset = 0; + int cur_pos = 0; + const Skill *currentSkill = sorted_skills[cur_pos]; + int selected = 0; + input_context ctxt( "NEW_CHAR_SKILLS" ); - details.set_up_navigation( ctxt, scrolling_key_scheme::angle_bracket_scroll ); - list_sb.set_draggable( ctxt ); ctxt.register_cardinal(); - ctxt.register_action( "PAGE_UP", to_translation( "Fast scroll up" ) ); - ctxt.register_action( "PAGE_DOWN", to_translation( "Fast scroll down" ) ); - ctxt.register_action( "HOME" ); - ctxt.register_action( "END" ); + ctxt.register_action( "SCROLL_DOWN" ); + ctxt.register_action( "SCROLL_UP" ); ctxt.register_action( "PREV_TAB" ); ctxt.register_action( "NEXT_TAB" ); ctxt.register_action( "HELP_KEYBINDINGS" ); ctxt.register_action( "QUIT" ); - const auto init_windows = [&]( ui_adaptor & ui ) { - iContentHeight = TERMY - iHelpHeight - iHeaderHeight - 1; - w = catacurses::newwin( TERMY, TERMX, point_zero ); - w_list = catacurses::newwin( iContentHeight, iSecondColumn - 1, point( 1, iHeaderHeight ) ); - w_details_pane = catacurses::newwin( iContentHeight, TERMX - iSecondColumn - 1, - point( iSecondColumn, iHeaderHeight ) ); - details_recalc = true; - ui.position_from_window( w ); - }; - init_windows( ui ); - ui.on_screen_resize( init_windows ); + std::map prof_skills; + const auto &pskills = u.prof->skills(); - std::vector sorted_skills = Skill::get_skills_sorted_by( - []( const Skill & a, const Skill & b ) { - return localized_compare( a.name(), b.name() ); - } ); + std::copy( pskills.begin(), pskills.end(), + std::inserter( prof_skills, prof_skills.begin() ) ); - std::stable_sort( sorted_skills.begin(), sorted_skills.end(), - []( const Skill * a, const Skill * b ) { - return a->display_category() < b->display_category(); - } ); + do { + draw_points( w, points ); + // Clear the bottom of the screen. + werase( w_description ); + mvwprintz( w, point( 31, 3 ), c_light_gray, std::string( getmaxx( w ) - 32, ' ' ) ); + const int cost = skill_increment_cost( u, currentSkill->ident() ); + mvwprintz( w, point( 31, 3 ), points.skill_points_left() >= cost ? COL_SKILL_USED : c_light_red, + ngettext( "Upgrading %s costs %d point", "Upgrading %s costs %d points", cost ), + currentSkill->name(), cost ); - std::vector skill_list = get_HeaderSkills( sorted_skills ); + // We want recipes from profession skills displayed, but without boosting the skills + // Copy the skills, and boost the copy + SkillLevelMap with_prof_skills = u.get_all_skills(); + for( const auto &sk : prof_skills ) { + with_prof_skills.mod_skill_level( sk.first, sk.second ); + } - const int num_skills = skill_list.size(); - int cur_offset = 1; - int cur_pos = 0; - const Skill *currentSkill = nullptr; + std::map > > recipes; + for( const auto &e : recipe_dict ) { + const auto &r = e.second; + //Find out if the current skill and its level is in the requirement list + auto req_skill = r.required_skills.find( currentSkill->ident() ); + int skill = req_skill != r.required_skills.end() ? req_skill->second : 0; + bool would_autolearn_recipe = + recipe_dict.all_autolearn().count( &r ) && + with_prof_skills.meets_skill_requirements( r.autolearn_requirements ); - const int scroll_rate = num_skills > 20 ? 5 : 2; - auto get_next = [&]( bool go_up ) { - skip_skill_headers( skill_list, cur_pos, !go_up ); - currentSkill = skill_list[cur_pos].skill; - }; + if( !would_autolearn_recipe && !r.never_learn && + ( r.skill_used == currentSkill->ident() || skill > 0 ) && + with_prof_skills.has_recipe_requirements( r ) ) { - get_next( false ); + recipes[r.skill_used->name()].emplace_back( + r.result_name(), + ( skill > 0 ) ? skill : r.difficulty + ); + } + } - std::map prof_skills; - const profession::StartingSkillList &pskills = u.prof->skills(); + std::string rec_disp; - std::copy( pskills.begin(), pskills.end(), - std::inserter( prof_skills, prof_skills.begin() ) ); + for( auto &elem : recipes ) { + std::sort( elem.second.begin(), elem.second.end(), + []( const std::pair &lhs, + const std::pair &rhs ) { + return lhs.second < rhs.second || + ( lhs.second == rhs.second && lhs.first < rhs.first ); + } ); - const int remaining_points_length = utf8_width( pools_to_string( u, pool ), true ); + const std::string rec_temp = enumerate_as_string( elem.second.begin(), elem.second.end(), + []( const std::pair &rec ) { + return string_format( "%s (%d)", rec.first, rec.second ); + } ); - ui.on_redraw( [&]( ui_adaptor & ui ) { - std::string cur_skill_text; - const std::string help_text = assemble_skill_help( ctxt ); - const int new_iHelpHeight = foldstring( help_text, getmaxx( w ) - 4 ).size(); - if( new_iHelpHeight != iHelpHeight ) { - iHelpHeight = new_iHelpHeight; - init_windows( ui ); + if( elem.first == currentSkill->name() ) { + rec_disp = "\n\n" + colorize( rec_temp, c_brown ) + rec_disp; + } else { + rec_disp += "\n\n" + colorize( "[" + elem.first + "]\n" + rec_temp, c_light_gray ); + } } - werase( w ); - werase( w_list ); - tabs.draw( w ); - mvwputch( w, point( iSecondColumn, iHeaderHeight - 1 ), BORDER_COLOR, LINE_OXXX ); // '┬' - draw_points( w, pool, u ); - // Helptext skill tab - fold_and_print( w, point( 2, TERMY - iHelpHeight - 1 ), getmaxx( w ) - 4, COL_NOTE_MINOR, - help_text ); - - // Write the hint as to upgrade costs - const int cost = skill_increment_cost( u, currentSkill->ident() ); - const int level = u.get_skill_level( currentSkill->ident() ); - if( pool != pool_type::FREEFORM ) { - // in pool the first level of a skill gives 2 - const int upgrade_levels = level == 0 ? 2 : 1; - // We have two different strings to pluralize, so we have to use two translation calls. - const std::string upgrade_levels_s = string_format( - //~ levels here are skill levels at character creation time - n_gettext( "%d level", "%d levels", upgrade_levels ), upgrade_levels ); - const nc_color color = skill_points_left( u, pool ) >= cost ? COL_SKILL_USED : c_light_red; - mvwprintz( w, point( remaining_points_length + 9, 3 ), color, - //~ Second string is e.g. "1 level" or "2 levels" - n_gettext( "Upgrading %s by %s costs %d point", - "Upgrading %s by %s costs %d points", cost ), - currentSkill->name(), upgrade_levels_s, cost ); + + rec_disp = currentSkill->description() + rec_disp; + + const auto vFolded = foldstring( rec_disp, getmaxx( w_description ) ); + int iLines = vFolded.size(); + + if( selected < 0 ) { + selected = 0; + } else if( iLines < iContentHeight ) { + selected = 0; + } else if( selected >= iLines - iContentHeight ) { + selected = iLines - iContentHeight; } + fold_and_print_from( w_description, point_zero, getmaxx( w_description ), + selected, COL_SKILL_USED, rec_disp ); + + draw_scrollbar( w, selected, iContentHeight, iLines, + point( getmaxx( w ) - 1, 5 ), BORDER_COLOR, true ); + calcStartPos( cur_offset, cur_pos, iContentHeight, num_skills ); for( int i = cur_offset; i < num_skills && i - cur_offset < iContentHeight; ++i ) { - const int y = i - cur_offset; - const Skill *thisSkill = skill_list[i].skill; - int prof_skill_level = 0; - if( !skill_list[i].is_header ) { - for( auto &prof_skill : u.prof->skills() ) { - if( prof_skill.first == thisSkill->ident() ) { - prof_skill_level += prof_skill.second; - break; - } - } - } - const point opt_pos( 1, y ); - std::string skill_text; - if( skill_list[i].is_header ) { - skill_text = colorize( thisSkill->display_category()->display_string(), c_yellow ); - } else if( static_cast( u.get_skill_level( thisSkill->ident() ) ) + prof_skill_level == 0 ) { - skill_text = colorize( thisSkill->name(), ( i == cur_pos ? COL_SELECT : c_light_gray ) ); + const int y = 5 + i - cur_offset; + const Skill *thisSkill = sorted_skills[i]; + // Clear the line + mvwprintz( w, point( 2, y ), c_light_gray, std::string( getmaxx( w ) - 3, ' ' ) ); + if( u.get_skill_level( thisSkill->ident() ) == 0 ) { + mvwprintz( w, point( 2, y ), + ( i == cur_pos ? h_light_gray : c_light_gray ), thisSkill->name() ); } else { - skill_text = colorize( thisSkill->name(), - ( i == cur_pos ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ) ); - if( prof_skill_level > 0 ) { - skill_text.append( colorize( string_format( " ( %d + %d )", prof_skill_level, - static_cast( u.get_skill_level( thisSkill->ident() ) ) ), - ( i == cur_pos ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ) ) ); - } else { - skill_text.append( colorize( string_format( " ( %d )", - static_cast( u.get_skill_level( thisSkill->ident() ) ) ), - ( i == cur_pos ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ) ) ); + mvwprintz( w, point( 2, y ), + ( i == cur_pos ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ), + thisSkill->name() ); + wprintz( w, ( i == cur_pos ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ), + " ( %d )", u.get_skill_level( thisSkill->ident() ) ); + } + for( auto &prof_skill : u.prof->skills() ) { + if( prof_skill.first == thisSkill->ident() ) { + wprintz( w, ( i == cur_pos ? h_white : c_white ), " (+%d)", + static_cast( prof_skill.second ) ); + break; } } - if( i == cur_pos ) { - cur_skill_text = skill_text; - } - if( screen_reader_mode ) { - // This list only clutters up the screen in screen reader mode - } else { - nc_color dummy = c_light_gray; - print_colored_text( w_list, opt_pos, dummy, c_light_gray, skill_text ); - } - } - - if( details_recalc ) { - std::string description; - if( screen_reader_mode ) { - description = currentSkill->display_category()->display_string() + " - "; - description.append( cur_skill_text + "\n" ); - description.append( assemble_skill_details( u, prof_skills, currentSkill ) ); - } else { - description = assemble_skill_details( u, prof_skills, currentSkill ); - } - details.set_text( description ); - details_recalc = false; } - list_sb.offset_x( 0 ) - .offset_y( iHeaderHeight ) - .content_size( num_skills ) - .viewport_pos( cur_offset ) - .viewport_size( iContentHeight ) - .apply( w ); - - wnoutrefresh( w ); - wnoutrefresh( w_list ); - ui.set_cursor( w_details_pane, point_zero ); - details.draw( c_light_gray ); - } ); + draw_scrollbar( w, cur_pos, iContentHeight, num_skills, point( 0, 5 ) ); - do { - ui_manager::redraw(); + wrefresh( w ); + wrefresh( w_description ); const std::string action = ctxt.handle_input(); - const int pos_for_curr_description = cur_pos; - int scrollbar_pos = cur_offset; - - if( tabs.handle_input( action, ctxt ) ) { - break; // Tab has changed or user has quit the screen - } else if( details.handle_navigation( action, ctxt ) ) { - // NO FURTHER ACTION REQUIRED - } else if( list_sb.handle_dragging( action, ctxt.get_coordinates_text( catacurses::stdscr ), - scrollbar_pos ) ) { - if( scrollbar_pos != cur_offset ) { - cur_offset = scrollbar_pos; - cur_pos = cur_offset + ( iContentHeight - 1 ) / 2; // Get approximate location - get_next( false ); // Then make sure it's a skill rather than a heading - if( cur_pos < num_skills / 2 ) { - get_next( true ); // Go back to where we were to ensure we can drag to the top - } + if( action == "DOWN" ) { + cur_pos++; + if( cur_pos >= num_skills ) { + cur_pos = 0; + } + currentSkill = sorted_skills[cur_pos]; + } else if( action == "UP" ) { + cur_pos--; + if( cur_pos < 0 ) { + cur_pos = num_skills - 1; } - } else if( navigate_ui_list( action, cur_pos, scroll_rate, skill_list.size(), true ) ) { - // omitting action == "PAGE_UP", because that shouldn't wrap - const bool go_up = action == "UP" || action == "SCROLL_UP"; - get_next( go_up ); + currentSkill = sorted_skills[cur_pos]; } else if( action == "LEFT" ) { - const skill_id &skill_id = currentSkill->ident(); - const int level = u.get_skill_level( skill_id ); + const int level = u.get_skill_level( currentSkill->ident() ); if( level > 0 ) { // For balance reasons, increasing a skill from level 0 gives 1 extra level for free, but // decreasing it from level 2 forfeits the free extra level (thus changes it to 0) - // this only matters in legacy character creation modes - u.mod_skill_level( skill_id, level == 2 && pool != pool_type::FREEFORM ? -2 : -1 ); - u.set_knowledge_level( skill_id, static_cast( u.get_skill_level( skill_id ) ) ); + u.mod_skill_level( currentSkill->ident(), level == 2 ? -2 : -1 ); + // Done *after* the decrementing to get the original cost for incrementing back. + points.skill_points += skill_increment_cost( u, currentSkill->ident() ); } - details_recalc = true; } else if( action == "RIGHT" ) { - const skill_id &skill_id = currentSkill->ident(); - const int level = u.get_skill_level( skill_id ); + const int level = u.get_skill_level( currentSkill->ident() ); if( level < MAX_SKILL ) { + points.skill_points -= skill_increment_cost( u, currentSkill->ident() ); // For balance reasons, increasing a skill from level 0 gives 1 extra level for free - // this only matters in legacy character creation modes - u.mod_skill_level( skill_id, level == 0 && pool != pool_type::FREEFORM ? +2 : +1 ); - u.set_knowledge_level( skill_id, static_cast( u.get_skill_level( skill_id ) ) ); - } - details_recalc = true; - } - if( cur_pos != pos_for_curr_description ) { - details_recalc = true; + u.mod_skill_level( currentSkill->ident(), level == 0 ? +2 : +1 ); + } + } else if( action == "SCROLL_DOWN" ) { + selected++; + } else if( action == "SCROLL_UP" ) { + selected--; + } else if( action == "PREV_TAB" ) { + return tab_direction::BACKWARD; + } else if( action == "NEXT_TAB" ) { + return tab_direction::FORWARD; + } else if( action == "HELP_KEYBINDINGS" ) { + // Need to redraw since the help window obscured everything. + draw_character_tabs( w, _( "SKILLS" ) ); + } else if( action == "QUIT" && query_yn( _( "Return to main menu?" ) ) ) { + return tab_direction::QUIT; } } while( true ); } -static struct { +struct { bool sort_by_points = true; - bool male = false; - bool cities_enabled = false; + bool male; + bool cities_enabled; /** @related player */ - bool operator()( const scenario *a, const scenario *b ) const { + bool operator()( const scenario *a, const scenario *b ) { if( cities_enabled ) { // The generic ("Unemployed") profession should be listed first. const scenario *gen = scenario::generic(); @@ -3319,734 +1855,379 @@ static struct { } } - if( !a->can_pick().success() && b->can_pick().success() ) { - return false; - } - - if( a->can_pick().success() && !b->can_pick().success() ) { - return true; - } - if( !cities_enabled && a->has_flag( "CITY_START" ) != b->has_flag( "CITY_START" ) ) { return a->has_flag( "CITY_START" ) < b->has_flag( "CITY_START" ); } else if( sort_by_points ) { return a->point_cost() < b->point_cost(); } else { - return localized_compare( a->gender_appropriate_name( male ), - b->gender_appropriate_name( male ) ); + return a->gender_appropriate_name( male ) < + b->gender_appropriate_name( male ); } } } scenario_sorter; -static std::string assemble_scenario_details( const avatar &u, const input_context &ctxt, - const scenario *current_scenario, const std::string ¬es ) +tab_direction set_scenario( const catacurses::window &w, avatar &u, points_left &points, + const tab_direction direction ) { - std::string assembled; - // Display Origin - const std::string mod_src = enumerate_as_string( current_scenario->src, - []( const std::pair, mod_id> &source ) { - return string_format( "'%s'", source.second->name() ); - }, enumeration_conjunction::arrow ); - assembled += string_format( _( "Origin: %s" ), mod_src ) + "\n"; - - std::string scenario_name = current_scenario->gender_appropriate_name( !u.male ); - if( get_option( "SCREEN_READER_MODE" ) && !notes.empty() ) { - scenario_name = scenario_name.append( string_format( " - %s", notes ) ); - } - assembled += string_format( g_switch_msg( u ), ctxt.get_desc( "CHANGE_GENDER" ), - scenario_name ) + "\n"; - assembled += string_format( dress_switch_msg(), ctxt.get_desc( "CHANGE_OUTFIT" ) ) + "\n"; - - assembled += string_format( - _( "Press %1$s to change cataclysm start date, %2$s to change game start date, %3$s to reset calendar." ), - ctxt.get_desc( "CHANGE_START_OF_CATACLYSM" ), ctxt.get_desc( "CHANGE_START_OF_GAME" ), - ctxt.get_desc( "RESET_CALENDAR" ) ) + "\n"; - assembled += "\n" + colorize( _( "Scenario Story:" ), COL_HEADER ) + "\n"; - assembled += colorize( current_scenario->description( u.male ), c_green ) + "\n"; - const std::optional scenRequirement = current_scenario->get_requirement(); - - if( scenRequirement.has_value() || - ( current_scenario->has_flag( "CITY_START" ) && !scenario_sorter.cities_enabled ) ) { - assembled += "\n" + colorize( _( "Scenario Requirements:" ), COL_HEADER ) + "\n"; - if( current_scenario->has_flag( "CITY_START" ) && !scenario_sorter.cities_enabled ) { - const std::string scenUnavailable = - _( "This scenario is not available in this world due to city size settings." ); - assembled += colorize( scenUnavailable, c_red ) + "\n"; - } - if( scenRequirement.has_value() ) { - nc_color requirement_color = c_red; - if( current_scenario->can_pick().success() ) { - requirement_color = c_green; - } - assembled += colorize( string_format( _( "Complete \"%s\"" ), scenRequirement.value()->name() ), - requirement_color ) + "\n"; - } - } - - assembled += "\n" + colorize( _( "Scenario Professions:" ), COL_HEADER ); - assembled += string_format( _( "\n%s" ), current_scenario->prof_count_str() ); - assembled += _( ", default:\n" ); - - auto psorter = profession_sorter; - psorter.sort_by_points = true; - const auto permitted = current_scenario->permitted_professions(); - const auto default_prof = *std::min_element( permitted.begin(), permitted.end(), psorter ); - const int prof_points = default_prof->point_cost(); - - assembled += default_prof->gender_appropriate_name( u.male ); - if( prof_points > 0 ) { - assembled += colorize( string_format( " (-%d)", prof_points ), c_red ); - } else if( prof_points < 0 ) { - assembled += colorize( string_format( " (+%d)", -prof_points ), c_green ); - } - assembled += "\n"; - - assembled += "\n" + colorize( _( "Scenario Location:" ), COL_HEADER ) + "\n"; - assembled += string_format( _( "%s (%d locations, %d variants)" ), - current_scenario->start_name(), - current_scenario->start_location_count(), - current_scenario->start_location_targets_count() ) + "\n"; - - if( current_scenario->vehicle() ) { - assembled += "\n" + colorize( _( "Scenario Vehicle:" ), COL_HEADER ) + "\n"; - assembled += current_scenario->vehicle()->name + "\n"; - } - - assembled += "\n" + colorize( _( "Start of cataclysm:" ), COL_HEADER ) + "\n"; - assembled += to_string( current_scenario->start_of_cataclysm() ) + "\n"; - - assembled += "\n" + colorize( _( "Start of game:" ), COL_HEADER ) + "\n"; - assembled += to_string( current_scenario->start_of_game() ) + "\n"; + draw_character_tabs( w, _( "SCENARIO" ) ); - if( !current_scenario->missions().empty() ) { - assembled += "\n" + colorize( _( "Scenario missions:" ), COL_HEADER ) + "\n"; - for( mission_type_id mission_id : current_scenario->missions() ) { - assembled += mission_type::get( mission_id )->tname() + "\n"; - } - } - - //TODO: Move this to JSON? - const std::vector> flag_descriptions = { - { "FIRE_START", translate_marker( "Fire nearby" ) }, - { "SUR_START", translate_marker( "Zombies nearby" ) }, - { "HELI_CRASH", translate_marker( "Various limb wounds" ) }, - { "LONE_START", translate_marker( "No starting NPC" ) }, - { "BORDERED", translate_marker( "Starting location is bordered by an immense wall" ) }, - }; - - bool flag_header_added = false; - for( const std::pair &flag_pair : flag_descriptions ) { - if( current_scenario->has_flag( std::get<0>( flag_pair ) ) ) { - if( !flag_header_added ) { - assembled += "\n" + colorize( _( "Scenario Flags:" ), COL_HEADER ) + "\n"; - flag_header_added = true; - } - assembled += _( std::get<1>( flag_pair ) ) + "\n"; - } - } - - return assembled; -} - -void set_scenario( tab_manager &tabs, avatar &u, pool_type pool ) -{ int cur_id = 0; - size_t iContentHeight = 0; + tab_direction retval = tab_direction::NONE; + const int iContentHeight = TERMY - 10; int iStartPos = 0; - ui_adaptor ui; - catacurses::window w; - catacurses::window w_details_pane; - scrolling_text_view details( w_details_pane ); - bool details_recalc = true; - const int iHeaderHeight = 6; - scrollbar list_sb; - - const bool screen_reader_mode = get_option( "SCREEN_READER_MODE" ); - - const auto init_windows = [&]( ui_adaptor & ui ) { - iContentHeight = TERMY - iHeaderHeight - 1; - w = catacurses::newwin( TERMY, TERMX, point_zero ); - const int second_column_w = TERMX / 2 - 1; - point origin = point( second_column_w + 1, iHeaderHeight ); - w_details_pane = catacurses::newwin( iContentHeight, second_column_w, origin ); - details_recalc = true; - ui.position_from_window( w ); - }; - init_windows( ui ); - ui.on_screen_resize( init_windows ); + catacurses::window w_description = + catacurses::newwin( 4, TERMX - 2, point( 1 + getbegx( w ), TERMY - 5 + getbegy( w ) ) ); + catacurses::window w_sorting = + catacurses::newwin( 2, ( TERMX / 2 ) - 1, + point( ( TERMX / 2 ) + getbegx( w ), 5 + getbegy( w ) ) ); + catacurses::window w_profession = + catacurses::newwin( 4, ( TERMX / 2 ) - 1, + point( ( TERMX / 2 ) + getbegx( w ), 7 + getbegy( w ) ) ); + catacurses::window w_location = + catacurses::newwin( 3, ( TERMX / 2 ) - 1, + point( ( TERMX / 2 ) + getbegx( w ), 11 + getbegy( w ) ) ); + + // 9 = 2 + 4 + 3, so we use rest of space for flags + catacurses::window w_flags = + catacurses::newwin( iContentHeight - 9, ( TERMX / 2 ) - 1, + point( ( TERMX / 2 ) + getbegx( w ), 14 + getbegy( w ) ) ); input_context ctxt( "NEW_CHAR_SCENARIOS" ); - tabs.set_up_tab_navigation( ctxt ); - details.set_up_navigation( ctxt, scrolling_key_scheme::angle_bracket_scroll ); - list_sb.set_draggable( ctxt ); - ctxt.register_navigate_ui_list(); + ctxt.register_cardinal(); ctxt.register_action( "CONFIRM" ); + ctxt.register_action( "PREV_TAB" ); + ctxt.register_action( "NEXT_TAB" ); ctxt.register_action( "SORT" ); ctxt.register_action( "HELP_KEYBINDINGS" ); - ctxt.register_action( "CHANGE_GENDER" ); - ctxt.register_action( "CHANGE_OUTFIT" ); ctxt.register_action( "FILTER" ); - ctxt.register_action( "RESET_FILTER" ); - ctxt.register_action( "CHANGE_START_OF_CATACLYSM" ); - ctxt.register_action( "CHANGE_START_OF_GAME" ); - ctxt.register_action( "RESET_CALENDAR" ); + ctxt.register_action( "QUIT" ); bool recalc_scens = true; - size_t scens_length = 0; + int scens_length = 0; std::string filterstring; std::vector sorted_scens; - ui.on_redraw( [&]( ui_adaptor & ui ) { - werase( w ); - tabs.draw( w ); - mvwputch( w, point( TERMX / 2, iHeaderHeight - 1 ), BORDER_COLOR, LINE_OXXX ); // '┬' - mvwputch( w, point( TERMX / 2, TERMY - 1 ), BORDER_COLOR, LINE_XXOX ); // 'â”´' - draw_filter_and_sorting_indicators( w, ctxt, filterstring, scenario_sorter ); - - const bool cur_id_is_valid = cur_id >= 0 && static_cast( cur_id ) < sorted_scens.size(); - if( cur_id_is_valid ) { - int netPointCost = sorted_scens[cur_id]->point_cost() - get_scenario()->point_cost(); - ret_val can_afford = sorted_scens[cur_id]->can_afford( - *get_scenario(), - skill_points_left( u, pool ) ); - ret_val can_pick = sorted_scens[cur_id]->can_pick(); - - int pointsForScen = sorted_scens[cur_id]->point_cost(); - bool negativeScen = pointsForScen < 0; - if( negativeScen ) { - pointsForScen *= -1; - } - // Draw header. - draw_points( w, pool, u, netPointCost ); - if( pool != pool_type::FREEFORM ) { - - const char *scen_msg_temp; - - if( negativeScen ) { - scen_msg_temp = n_gettext( "Scenario earns %2$d point", - "Scenario earns %2$d points", pointsForScen ); - } else { - scen_msg_temp = n_gettext( "Scenario costs %2$d point", - "Scenario costs %2$d points", pointsForScen ); - } - - int pMsg_length = utf8_width( remove_color_tags( pools_to_string( u, pool ) ) ); - mvwprintz( w, point( pMsg_length + 9, 3 ), can_afford.success() ? c_green : c_light_red, - scen_msg_temp, sorted_scens[cur_id]->gender_appropriate_name( u.male ), pointsForScen ); - } - } - - //Draw options - calcStartPos( iStartPos, cur_id, iContentHeight, scens_length ); - const int end_pos = iStartPos + std::min( iContentHeight, scens_length ); - std::string current_scenario_notes; - for( int i = iStartPos; i < end_pos; i++ ) { - nc_color col; - if( get_scenario() != sorted_scens[i] ) { - if( cur_id_is_valid && sorted_scens[i] == sorted_scens[cur_id] && - ( ( sorted_scens[i]->has_flag( "CITY_START" ) && !scenario_sorter.cities_enabled ) || - !sorted_scens[i]->can_pick().success() ) ) { - col = h_dark_gray; - if( i == cur_id ) { - current_scenario_notes = _( "unavailable" ); - } - } else if( cur_id_is_valid && sorted_scens[i] != sorted_scens[cur_id] && - ( ( sorted_scens[i]->has_flag( "CITY_START" ) && !scenario_sorter.cities_enabled ) || - !sorted_scens[i]->can_pick().success() ) ) { - col = c_dark_gray; - if( i == cur_id ) { - current_scenario_notes = _( "unavailable" ); - } - } else { - col = ( cur_id_is_valid && sorted_scens[i] == sorted_scens[cur_id] ? COL_SELECT : c_light_gray ); - } - } else { - col = ( cur_id_is_valid && - sorted_scens[i] == sorted_scens[cur_id] ? hilite( c_light_green ) : COL_SKILL_USED ); - if( i == cur_id ) { - current_scenario_notes = _( "active" ); - } - } - const point opt_pos( 2, iHeaderHeight + i - iStartPos ); - if( screen_reader_mode ) { - // The list of options only clutters up the screen in screen reader mode - } else { - mvwprintz( w, opt_pos, col, - sorted_scens[i]->gender_appropriate_name( u.male ) ); - } - } - - if( details_recalc && cur_id_is_valid ) { - details.set_text( assemble_scenario_details( u, ctxt, sorted_scens[cur_id], - current_scenario_notes ) ); - details_recalc = false; - } - - list_sb.offset_x( 0 ) - .offset_y( 6 ) - .content_size( scens_length ) - .viewport_pos( iStartPos ) - .viewport_size( iContentHeight ) - .apply( w ); - - wnoutrefresh( w ); - ui.set_cursor( w_details_pane, point_zero ); - details.draw( c_light_gray ); - } ); + if( direction == tab_direction::BACKWARD ) { + points.skill_points += u.prof->point_cost(); + } do { if( recalc_scens ) { - options_manager::options_container &wopts = world_generator->active_world->WORLD_OPTIONS; - std::vector new_scens; - for( const scenario &scen : scenario::get_all() ) { - if( scen.scen_is_blacklisted() ) { + sorted_scens.clear(); + auto &wopts = world_generator->active_world->WORLD_OPTIONS; + for( const auto &scen : scenario::get_all() ) { + if( !lcmatch( scen.gender_appropriate_name( u.male ), filterstring ) ) { continue; } - new_scens.push_back( &scen ); + sorted_scens.push_back( &scen ); } - scenario_sorter.male = u.male; - scenario_sorter.cities_enabled = wopts["CITY_SIZE"].getValue() != "0"; - if( ( scens_length = filter_entries( u, cur_id, sorted_scens, new_scens, get_scenario(), - filterstring, - scenario_sorter ) ) == 0 ) { + scens_length = sorted_scens.size(); + if( scens_length == 0 ) { + popup( _( "Nothing found." ) ); // another case of black box in tiles filterstring.clear(); continue; } - recalc_scens = false; - } - ui_manager::redraw(); - const std::string action = ctxt.handle_input(); - const int scroll_rate = scens_length > 20 ? 5 : 2; - const int id_for_curr_description = cur_id; - int scrollbar_pos = iStartPos; - - if( tabs.handle_input( action, ctxt ) ) { - break; // Tab has changed or user has quit the screen - } else if( details.handle_navigation( action, ctxt ) - || navigate_ui_list( action, cur_id, scroll_rate, scens_length, true ) ) { - // NO FURTHER ACTION REQUIRED - } else if( list_sb.handle_dragging( action, ctxt.get_coordinates_text( catacurses::stdscr ), - scrollbar_pos ) ) { - if( scrollbar_pos != iStartPos ) { - iStartPos = scrollbar_pos; - cur_id = iStartPos + ( iContentHeight - 1 ) / 2; - } - } else if( action == "CONFIRM" ) { - // set arbitrarily high points and check if we have the achievment - ret_val can_pick = sorted_scens[cur_id]->can_pick(); + // Sort scenarios by points. + // scenario_display_sort() keeps "Evacuee" at the top. + scenario_sorter.male = u.male; + scenario_sorter.cities_enabled = wopts["CITY_SIZE"].getValue() != "0"; + std::stable_sort( sorted_scens.begin(), sorted_scens.end(), scenario_sorter ); - if( !can_pick.success() ) { - popup( can_pick.str() ); - continue; + // If city size is 0 but the current scenario requires cities reset the scenario + if( !scenario_sorter.cities_enabled && g->scen->has_flag( "CITY_START" ) ) { + reset_scenario( u, sorted_scens[0] ); + points.init_from_options(); + points.skill_points -= sorted_scens[cur_id]->point_cost(); } - if( sorted_scens[cur_id]->has_flag( "CITY_START" ) && !scenario_sorter.cities_enabled ) { - continue; - } - reset_scenario( u, sorted_scens[cur_id] ); - details_recalc = true; - } else if( action == "CHANGE_OUTFIT" ) { - outfit = !outfit; - recalc_scens = true; - } else if( action == "CHANGE_GENDER" ) { - u.male = !u.male; - recalc_scens = true; - } else if( action == "SORT" ) { - scenario_sorter.sort_by_points = !scenario_sorter.sort_by_points; - recalc_scens = true; - } else if( action == "FILTER" ) { - string_input_popup() - .title( _( "Search:" ) ) - .width( 60 ) - .description( _( "Search by scenario name." ) ) - .edit( filterstring ); - recalc_scens = true; - } else if( action == "RESET_FILTER" ) { - if( !filterstring.empty() ) { - filterstring.clear(); - recalc_scens = true; - } - } else if( action == "RANDOMIZE" ) { - cur_id = rng( 0, scens_length - 1 ); - } else if( action == "CHANGE_START_OF_CATACLYSM" ) { - const scenario *scen = sorted_scens[cur_id]; - if( cur_id != id_for_curr_description ) { - scen = get_scenario(); + // Select the current scenario, if possible. + for( int i = 0; i < scens_length; ++i ) { + if( sorted_scens[i]->ident() == g->scen->ident() ) { + cur_id = i; + break; + } } - scen->change_start_of_cataclysm( calendar_ui::select_time_point( scen->start_of_cataclysm(), - _( "Select cataclysm start date" ), calendar_ui::granularity::hour ) ); - details_recalc = true; - } else if( action == "CHANGE_START_OF_GAME" ) { - const scenario *scen = sorted_scens[cur_id]; - if( cur_id != id_for_curr_description ) { - scen = get_scenario(); + if( cur_id > scens_length - 1 ) { + cur_id = 0; } - scen->change_start_of_game( calendar_ui::select_time_point( scen->start_of_game(), - _( "Select game start date" ), calendar_ui::granularity::hour ) ); - details_recalc = true; - } else if( action == "RESET_CALENDAR" ) { - const scenario *scen = sorted_scens[cur_id]; - if( cur_id != id_for_curr_description ) { - get_scenario()->reset_calendar(); + + // Draw filter indicator + for( int i = 1; i < TERMX - 1; i++ ) { + mvwputch( w, point( i, TERMY - 1 ), BORDER_COLOR, LINE_OXOX ); } - scen->reset_calendar(); - details_recalc = true; - } + const auto filter_indicator = filterstring.empty() ? _( "no filter" ) + : filterstring; + mvwprintz( w, point( 2, getmaxy( w ) - 1 ), c_light_gray, "<%s>", filter_indicator ); - if( cur_id != id_for_curr_description || recalc_scens ) { - details_recalc = true; + recalc_scens = false; } - } while( true ); -} - -namespace char_creation -{ -enum description_selector { - NAME, - GENDER, - OUTFIT, - HEIGHT, - AGE, - BLOOD, - LOCATION -}; - -static void draw_name( ui_adaptor &ui, const catacurses::window &w_name, - const avatar &you, const bool highlight, - const bool no_name_entered ) -{ - werase( w_name ); - mvwprintz( w_name, point_zero, - highlight ? COL_SELECT : c_light_gray, _( "Name:" ) ); - const point opt_pos( 1 + utf8_width( _( "Name:" ) ), 0 ); - if( highlight ) { - ui.set_cursor( w_name, opt_pos ); - } - if( no_name_entered ) { - mvwprintz( w_name, opt_pos, COL_SELECT, _( "--- NO NAME ENTERED ---" ) ); - } else if( you.name.empty() ) { - mvwprintz( w_name, opt_pos, c_light_gray, _( "--- RANDOM NAME ---" ) ); - } else { - mvwprintz( w_name, opt_pos, c_white, you.name ); - } - - wnoutrefresh( w_name ); -} - -static void draw_gender( ui_adaptor &ui, const catacurses::window &w_gender, - const avatar &you, const bool highlight ) -{ - const point male_pos( 1 + utf8_width( _( "Gender:" ) ), 0 ); - const point female_pos = male_pos + point( 2 + utf8_width( _( "Male" ) ), 0 ); - - werase( w_gender ); - mvwprintz( w_gender, point_zero, highlight ? COL_SELECT : c_light_gray, _( "Gender:" ) ); - if( highlight && you.male ) { - ui.set_cursor( w_gender, male_pos ); - } - mvwprintz( w_gender, male_pos, ( you.male ? c_light_cyan : c_light_gray ), - _( "Male" ) ); - if( highlight && !you.male ) { - ui.set_cursor( w_gender, female_pos ); - } - mvwprintz( w_gender, female_pos, ( you.male ? c_light_gray : c_pink ), - _( "Female" ) ); - wnoutrefresh( w_gender ); -} - -static void draw_outfit( ui_adaptor &ui, const catacurses::window &w_outfit, const bool highlight ) -{ - const point male_pos( 1 + utf8_width( _( "Outfit:" ) ), 0 ); - const point female_pos = male_pos + point( 2 + utf8_width( _( "Male" ) ), 0 ); - werase( w_outfit ); - mvwprintz( w_outfit, point_zero, highlight ? COL_SELECT : c_light_gray, _( "Outfit:" ) ); - if( highlight && outfit ) { - ui.set_cursor( w_outfit, male_pos ); - } - mvwprintz( w_outfit, male_pos, ( outfit ? c_light_cyan : c_light_gray ), - _( "Male" ) ); - if( highlight && !outfit ) { - ui.set_cursor( w_outfit, female_pos ); - } - mvwprintz( w_outfit, female_pos, ( outfit ? c_light_gray : c_pink ), - _( "Female" ) ); - wnoutrefresh( w_outfit ); -} + int netPointCost = sorted_scens[cur_id]->point_cost() - g->scen->point_cost(); + bool can_pick = sorted_scens[cur_id]->can_pick( *g->scen, points.skill_points_left() ); + const std::string clear_line( getmaxx( w_description ), ' ' ); -static void draw_height( ui_adaptor &ui, const catacurses::window &w_height, - const avatar &you, const bool highlight ) -{ - werase( w_height ); - mvwprintz( w_height, point_zero, highlight ? COL_SELECT : c_light_gray, _( "Height:" ) ); - const point opt_pos( 1 + utf8_width( _( "Height:" ) ), 0 ); - if( highlight ) { - ui.set_cursor( w_height, opt_pos ); - } - mvwprintz( w_height, opt_pos, c_white, you.height_string() ); - wnoutrefresh( w_height ); -} + // Clear the bottom of the screen and header. + werase( w_description ); + mvwprintz( w, point( 1, 3 ), c_light_gray, clear_line ); -static void draw_age( ui_adaptor &ui, const catacurses::window &w_age, - const avatar &you, const bool highlight ) -{ - werase( w_age ); - mvwprintz( w_age, point_zero, highlight ? COL_SELECT : c_light_gray, _( "Age:" ) ); - const point opt_pos( 1 + utf8_width( _( "Age:" ) ), 0 ); - if( highlight ) { - ui.set_cursor( w_age, opt_pos ); - } - mvwprintz( w_age, opt_pos, c_white, you.age_string( get_scenario()->start_of_game() ) ); - wnoutrefresh( w_age ); -} + int pointsForScen = sorted_scens[cur_id]->point_cost(); + bool negativeScen = pointsForScen < 0; + if( negativeScen ) { + pointsForScen *= -1; + } -static void draw_blood( ui_adaptor &ui, const catacurses::window &w_blood, - const avatar &you, const bool highlight ) -{ - werase( w_blood ); - mvwprintz( w_blood, point_zero, highlight ? COL_SELECT : c_light_gray, _( "Blood type:" ) ); - const point opt_pos( 1 + utf8_width( _( "Blood type:" ) ), 0 ); - if( highlight ) { - ui.set_cursor( w_blood, opt_pos ); - } - mvwprintz( w_blood, opt_pos, c_white, - io::enum_to_string( you.my_blood_type ) + ( you.blood_rh_factor ? "+" : "-" ) ); - wnoutrefresh( w_blood ); -} + // Draw header. + draw_points( w, points, netPointCost ); -static void draw_location( ui_adaptor &ui, const catacurses::window &w_location, - const avatar &you, const bool highlight ) -{ - std::string random_start_location_text = string_format( n_gettext( - "* Random location * (%d variant)", - "* Random location * (%d variants)", - get_scenario()->start_location_targets_count() ), get_scenario()->start_location_targets_count() ); + std::string scen_msg_temp; + if( negativeScen ) { + //~ 1s - scenario name, 2d - current character points. + scen_msg_temp = ngettext( "Scenario %1$s earns %2$d point", + "Scenario %1$s earns %2$d points", + pointsForScen ); + } else { + //~ 1s - scenario name, 2d - current character points. + scen_msg_temp = ngettext( "Scenario %1$s costs %2$d point", + "Scenario %1$s cost %2$d points", + pointsForScen ); + } - if( get_scenario()->start_location_targets_count() == 1 ) { - random_start_location_text = get_scenario()->start_location().obj().name(); - } + int pMsg_length = utf8_width( remove_color_tags( points.to_string() ) ); + mvwprintz( w, point( pMsg_length + 9, 3 ), can_pick ? c_green : c_light_red, scen_msg_temp.c_str(), + sorted_scens[cur_id]->gender_appropriate_name( u.male ), + pointsForScen ); - werase( w_location ); - mvwprintz( w_location, point_zero, highlight ? COL_SELECT : c_light_gray, - _( "Starting location:" ) ); - const point opt_pos( utf8_width( _( "Starting location:" ) ) + 1, 0 ); - if( highlight ) { - ui.set_cursor( w_location, opt_pos ); - } - // ::find will return empty location if id was not found. Debug msg will be printed too. - mvwprintz( w_location, opt_pos, - you.random_start_location ? c_red : c_white, - you.random_start_location ? remove_color_tags( random_start_location_text ) : - string_format( n_gettext( "%s (%d variant)", "%s (%d variants)", - you.start_location.obj().targets_count() ), - you.start_location.obj().name(), you.start_location.obj().targets_count() ) ); - wnoutrefresh( w_location ); -} + const std::string scenDesc = sorted_scens[cur_id]->description( u.male ); -} // namespace char_creation + if( sorted_scens[cur_id]->has_flag( "CITY_START" ) && !scenario_sorter.cities_enabled ) { + const std::string scenUnavailable = + _( "This scenario is not available in this world due to city size settings." ); + fold_and_print( w_description, point_zero, TERMX - 2, c_red, scenUnavailable ); + // NOLINTNEXTLINE(cata-use-named-point-constants) + fold_and_print( w_description, point( 0, 1 ), TERMX - 2, c_green, scenDesc ); + } else { + fold_and_print( w_description, point_zero, TERMX - 2, c_green, scenDesc ); + } -static std::string assemble_description_help( const input_context &ctxt, const bool allow_reroll ) -{ - if( !isWide ) { - return string_format( _( "Press %s to view and alter keybindings." ), - ctxt.get_desc( "HELP_KEYBINDINGS" ) ); - } - std::string help_text = string_format( - _( "Press %s to view and alter keybindings." ), - ctxt.get_desc( "HELP_KEYBINDINGS" ) ) - + string_format( _( "\nPress %s to save character template." ), - ctxt.get_desc( "SAVE_TEMPLATE" ) ); - if( !MAP_SHARING::isSharing() && allow_reroll ) { // no random names when sharing maps - help_text += string_format( - _( "\nPress %s to pick a random name, " - "%s to randomize all description values, " - "%s to randomize all but scenario, or " - "%s to randomize everything." ), - ctxt.get_desc( "RANDOMIZE_CHAR_NAME" ), ctxt.get_desc( "RANDOMIZE_CHAR_DESCRIPTION" ), - ctxt.get_desc( "REROLL_CHARACTER" ), ctxt.get_desc( "REROLL_CHARACTER_WITH_SCENARIO" ) ); - } else { - help_text += string_format( - _( "\nPress %s to pick a random name, " - "%s to randomize all description values." ), - ctxt.get_desc( "RANDOMIZE_CHAR_NAME" ), ctxt.get_desc( "RANDOMIZE_CHAR_DESCRIPTION" ) ); - } - help_text += string_format( - _( "\nPress %1$s to change cataclysm start date, " - "%2$s to change game start date, " - "%3$s to reset calendar." ), - ctxt.get_desc( "CHANGE_START_OF_CATACLYSM" ), ctxt.get_desc( "CHANGE_START_OF_GAME" ), - ctxt.get_desc( "RESET_CALENDAR" ) ); - if( !get_option( "SELECT_STARTING_CITY" ) ) { - help_text += string_format( - _( "\nPress %s to select a specific starting location." ), - ctxt.get_desc( "CHOOSE_LOCATION" ) ); - } else { - help_text += string_format( - _( "\nPress %s to select a specific starting city and " - "%s to select a specific starting location." ), - ctxt.get_desc( "CHOOSE_CITY" ), ctxt.get_desc( "CHOOSE_LOCATION" ) ); - } - help_text += string_format( - _( "\nPress %s or %s " - "to cycle through editable values." ), - ctxt.get_desc( "UP" ), ctxt.get_desc( "DOWN" ) ) - + string_format( _( "\nPress %s and " - "%s to change gender, height, age, and blood type." ), - ctxt.get_desc( "LEFT" ), ctxt.get_desc( "RIGHT" ) ) - + string_format( _( "\nPress %s to edit value via popup input." ), - ctxt.get_desc( "CONFIRM" ) ) - + string_format( _( "\nPress %s to finish character creation " - "or %s to return to the previous TAB." ), - ctxt.get_desc( "NEXT_TAB" ), ctxt.get_desc( "PREV_TAB" ) ); - return help_text; -} + //Draw options + calcStartPos( iStartPos, cur_id, iContentHeight, scens_length ); + const int end_pos = iStartPos + ( ( iContentHeight > scens_length ) ? + scens_length : iContentHeight ); + int i; + for( i = iStartPos; i < end_pos; i++ ) { + mvwprintz( w, point( 2, 5 + i - iStartPos ), c_light_gray, + " " ); + nc_color col; + if( g->scen != sorted_scens[i] ) { + if( sorted_scens[i] == sorted_scens[cur_id] && ( sorted_scens[i]->has_flag( "CITY_START" ) && + !scenario_sorter.cities_enabled ) ) { + col = h_dark_gray; + } else if( sorted_scens[i] != sorted_scens[cur_id] && ( sorted_scens[i]->has_flag( "CITY_START" ) && + !scenario_sorter.cities_enabled ) ) { + col = c_dark_gray; + } else { + col = ( sorted_scens[i] == sorted_scens[cur_id] ? h_light_gray : c_light_gray ); + } + } else { + col = ( sorted_scens[i] == sorted_scens[cur_id] ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ); + } + mvwprintz( w, point( 2, 5 + i - iStartPos ), col, + sorted_scens[i]->gender_appropriate_name( u.male ) ); -// NOLINTNEXTLINE(readability-function-size) -void set_description( tab_manager &tabs, avatar &you, const bool allow_reroll, - pool_type pool ) -{ - static constexpr int RANDOM_START_LOC_ENTRY = INT_MIN; + } + //Clear rest of space in case stuff got filtered out + for( ; i < iStartPos + iContentHeight; ++i ) { + mvwprintz( w, point( 2, 5 + i - iStartPos ), c_light_gray, + " " ); // Clear the line + } - // guessing most likely, but it doesn't matter, it will be recalculated if wrong - int iHelpHeight = 10; + werase( w_sorting ); + werase( w_profession ); + werase( w_location ); + werase( w_flags ); + + draw_sorting_indicator( w_sorting, ctxt, scenario_sorter ); + + mvwprintz( w_profession, point_zero, COL_HEADER, _( "Professions:" ) ); + wprintz( w_profession, c_light_gray, + string_format( _( "\n%s" ), sorted_scens[cur_id]->prof_count_str() ) ); + wprintz( w_profession, c_light_gray, _( ", default:\n" ) ); + + auto psorter = profession_sorter; + psorter.sort_by_points = true; + const auto permitted = sorted_scens[cur_id]->permitted_professions(); + const auto default_prof = *std::min_element( permitted.begin(), permitted.end(), psorter ); + const int prof_points = default_prof->point_cost(); + wprintz( w_profession, c_light_gray, + default_prof->gender_appropriate_name( u.male ) ); + if( prof_points > 0 ) { + wprintz( w_profession, c_red, " (-%d)", prof_points ); + } else if( prof_points < 0 ) { + wprintz( w_profession, c_green, " (+%d)", -prof_points ); + } + + mvwprintz( w_location, point_zero, COL_HEADER, _( "Scenario Location:" ) ); + wprintz( w_location, c_light_gray, ( "\n" ) ); + wprintz( w_location, c_light_gray, sorted_scens[cur_id]->start_name() ); + + mvwprintz( w_flags, point_zero, COL_HEADER, _( "Scenario Flags:" ) ); + wprintz( w_flags, c_light_gray, ( "\n" ) ); + + if( sorted_scens[cur_id]->has_flag( "SPR_START" ) ) { + wprintz( w_flags, c_light_gray, _( "Spring start" ) ); + wprintz( w_flags, c_light_gray, ( "\n" ) ); + } else if( sorted_scens[cur_id]->has_flag( "SUM_START" ) ) { + wprintz( w_flags, c_light_gray, _( "Summer start" ) ); + wprintz( w_flags, c_light_gray, ( "\n" ) ); + } else if( sorted_scens[cur_id]->has_flag( "AUT_START" ) ) { + wprintz( w_flags, c_light_gray, _( "Autumn start" ) ); + wprintz( w_flags, c_light_gray, ( "\n" ) ); + } else if( sorted_scens[cur_id]->has_flag( "WIN_START" ) ) { + wprintz( w_flags, c_light_gray, _( "Winter start" ) ); + wprintz( w_flags, c_light_gray, ( "\n" ) ); + } else if( sorted_scens[cur_id]->has_flag( "SUM_ADV_START" ) ) { + wprintz( w_flags, c_light_gray, _( "Next summer start" ) ); + wprintz( w_flags, c_light_gray, ( "\n" ) ); + } + + if( sorted_scens[cur_id]->has_flag( "INFECTED" ) ) { + wprintz( w_flags, c_light_gray, _( "Infected player" ) ); + wprintz( w_flags, c_light_gray, ( "\n" ) ); + } + if( sorted_scens[cur_id]->has_flag( "BAD_DAY" ) ) { + wprintz( w_flags, c_light_gray, _( "Drunk and sick player" ) ); + wprintz( w_flags, c_light_gray, ( "\n" ) ); + } + if( sorted_scens[cur_id]->has_flag( "FIRE_START" ) ) { + wprintz( w_flags, c_light_gray, _( "Fire nearby" ) ); + wprintz( w_flags, c_light_gray, ( "\n" ) ); + } + if( sorted_scens[cur_id]->has_flag( "SUR_START" ) ) { + wprintz( w_flags, c_light_gray, _( "Zombies nearby" ) ); + wprintz( w_flags, c_light_gray, ( "\n" ) ); + } + if( sorted_scens[cur_id]->has_flag( "HELI_CRASH" ) ) { + wprintz( w_flags, c_light_gray, _( "Various limb wounds" ) ); + wprintz( w_flags, c_light_gray, ( "\n" ) ); + } + if( get_option( "STARTING_NPC" ) == "scenario" && + sorted_scens[cur_id]->has_flag( "LONE_START" ) ) { + wprintz( w_flags, c_light_gray, _( "No starting NPC" ) ); + wprintz( w_flags, c_light_gray, ( "\n" ) ); + } + + draw_scrollbar( w, cur_id, iContentHeight, scens_length, point( 0, 5 ) ); + wrefresh( w ); + wrefresh( w_description ); + wrefresh( w_sorting ); + wrefresh( w_profession ); + wrefresh( w_location ); + wrefresh( w_flags ); - ui_adaptor ui; - catacurses::window w; - catacurses::window w_name; - catacurses::window w_gender; - catacurses::window w_outfit; - catacurses::window w_location; - catacurses::window w_vehicle; - catacurses::window w_stats; - catacurses::window w_traits; - catacurses::window w_bionics; - catacurses::window w_proficiencies; - catacurses::window w_addictions; - catacurses::window w_scenario; - catacurses::window w_profession; - catacurses::window w_hobbies; - catacurses::window w_skills; - catacurses::window w_guide; - catacurses::window w_height; - catacurses::window w_age; - catacurses::window w_blood; - catacurses::window w_calendar; - const auto init_windows = [&]( ui_adaptor & ui ) { - const int freeWidth = TERMX - FULL_SCREEN_WIDTH; - isWide = freeWidth > 15; - const int beginx2 = 46; - const int ncol2 = 40; - const int beginx3 = TERMX <= 88 ? TERMX - TERMX / 4 : 86; - const int ncol3 = TERMX - beginx3 - 2; - const int beginx4 = TERMX <= 130 ? TERMX - TERMX / 5 : 128; - const int ncol4 = TERMX - beginx4 - 2; - const int ncol_small = ( TERMX / 2 ) - 2; - const int begin_sncol = TERMX / 2; - if( isWide ) { - w = catacurses::newwin( TERMY, TERMX, point_zero ); - w_name = catacurses::newwin( 2, ncol2 + 2, point( 2, 6 ) ); - w_gender = catacurses::newwin( 1, ncol2 + 2, point( 2, 7 ) ); - w_outfit = catacurses::newwin( 1, ncol2 + 1, point( 2, 8 ) ); - w_location = catacurses::newwin( 1, ncol3, point( beginx3, 6 ) ); - w_vehicle = catacurses::newwin( 1, ncol3, point( beginx3, 7 ) ); - w_addictions = catacurses::newwin( 1, ncol3, point( beginx3, 8 ) ); - w_stats = catacurses::newwin( 6, 20, point( 2, 10 ) ); - w_traits = catacurses::newwin( TERMY - 11, ncol2, point( beginx2, 10 ) ); - w_calendar = catacurses::newwin( 4, ncol3, point( beginx3, 10 ) ); - w_bionics = catacurses::newwin( TERMY - 16, ncol3, point( beginx3, 15 ) ); - w_proficiencies = catacurses::newwin( TERMY - 21, 19, point( 2, 16 ) ); - // Extra - 11 to avoid overlap with long text in w_guide. - w_hobbies = catacurses::newwin( TERMY - 11 - 11, ncol4, point( beginx4, 10 ) ); - w_scenario = catacurses::newwin( 1, ncol2, point( beginx2, 3 ) ); - w_profession = catacurses::newwin( 1, ncol3, point( beginx3, 3 ) ); - w_skills = catacurses::newwin( TERMY - 11, 23, point( 22, 10 ) ); - w_height = catacurses::newwin( 1, ncol2, point( beginx2, 6 ) ); - w_age = catacurses::newwin( 1, ncol2, point( beginx2, 7 ) ); - w_blood = catacurses::newwin( 1, ncol2, point( beginx2, 8 ) ); - ui.position_from_window( w ); - } else { - w = catacurses::newwin( TERMY, TERMX, point_zero ); - w_name = catacurses::newwin( 1, ncol_small, point( 2, 5 ) ); - w_gender = catacurses::newwin( 1, ncol_small, point( 2, 6 ) ); - w_outfit = catacurses::newwin( 1, ncol_small, point( 2, 7 ) ); - w_height = catacurses::newwin( 1, ncol_small, point( 2, 8 ) ); - w_age = catacurses::newwin( 1, ncol_small, point( begin_sncol, 6 ) ); - w_blood = catacurses::newwin( 1, ncol_small, point( begin_sncol, 7 ) ); - w_location = catacurses::newwin( 1, ncol_small, point( begin_sncol, 8 ) ); - w_stats = catacurses::newwin( 6, ncol_small, point( 2, 10 ) ); - w_scenario = catacurses::newwin( 1, ncol_small, point( begin_sncol, 10 ) ); - w_profession = catacurses::newwin( 1, ncol_small, point( begin_sncol, 11 ) ); - w_calendar = catacurses::newwin( 4, ncol_small, point( begin_sncol, 13 ) ); - w_vehicle = catacurses::newwin( 2, ncol_small, point( begin_sncol, 18 ) ); - w_addictions = catacurses::newwin( 2, ncol_small, point( begin_sncol, 20 ) ); - w_traits = catacurses::window(); - w_bionics = catacurses::window(); - w_proficiencies = catacurses::window(); - w_hobbies = catacurses::window(); - w_skills = catacurses::window(); - ui.position_from_window( w ); - } - w_guide = catacurses::newwin( iHelpHeight, TERMX - 4, point( 2, TERMY - iHelpHeight - 1 ) ); - }; - init_windows( ui ); - ui.on_screen_resize( init_windows ); + const std::string action = ctxt.handle_input(); + if( action == "DOWN" ) { + cur_id++; + if( cur_id > scens_length - 1 ) { + cur_id = 0; + } + } else if( action == "UP" ) { + cur_id--; + if( cur_id < 0 ) { + cur_id = scens_length - 1; + } + } else if( action == "CONFIRM" ) { + if( sorted_scens[cur_id]->has_flag( "CITY_START" ) && !scenario_sorter.cities_enabled ) { + continue; + } + reset_scenario( u, sorted_scens[cur_id] ); + points.init_from_options(); + points.skill_points -= sorted_scens[cur_id]->point_cost(); + } else if( action == "PREV_TAB" ) { + retval = tab_direction::BACKWARD; + } else if( action == "NEXT_TAB" ) { + retval = tab_direction::FORWARD; + } else if( action == "SORT" ) { + scenario_sorter.sort_by_points = !scenario_sorter.sort_by_points; + recalc_scens = true; + } else if( action == "FILTER" ) { + string_input_popup() + .title( _( "Search:" ) ) + .width( 60 ) + .description( _( "Search by scenario name." ) ) + .edit( filterstring ); + recalc_scens = true; + } else if( action == "HELP_KEYBINDINGS" ) { + // Need to redraw since the help window obscured everything. + draw_character_tabs( w, _( "SCENARIO" ) ); + } else if( action == "QUIT" && query_yn( _( "Return to main menu?" ) ) ) { + retval = tab_direction::QUIT; + } + } while( retval == tab_direction::NONE ); + + return retval; +} + +tab_direction set_description( const catacurses::window &w, avatar &you, const bool allow_reroll, + points_left &points ) +{ + draw_character_tabs( w, _( "DESCRIPTION" ) ); + + catacurses::window w_name = + catacurses::newwin( 2, 42, point( getbegx( w ) + 2, getbegy( w ) + 5 ) ); + catacurses::window w_gender = + catacurses::newwin( 2, 33, point( getbegx( w ) + 46, getbegy( w ) + 5 ) ); + catacurses::window w_location = + catacurses::newwin( 1, TERMX - 3, point( getbegx( w ) + 2, getbegy( w ) + 7 ) ); + catacurses::window w_stats = + catacurses::newwin( 6, 20, point( getbegx( w ) + 2, getbegy( w ) + 9 ) ); + catacurses::window w_traits = + catacurses::newwin( 30, 24, point( getbegx( w ) + 22, getbegy( w ) + 9 ) ); + catacurses::window w_scenario = + catacurses::newwin( 1, TERMX - 47, point( getbegx( w ) + 46, getbegy( w ) + 9 ) ); + catacurses::window w_profession = + catacurses::newwin( 1, TERMX - 47, point( getbegx( w ) + 46, getbegy( w ) + 10 ) ); + catacurses::window w_skills = + catacurses::newwin( 30, 33, point( getbegx( w ) + 46, getbegy( w ) + 11 ) ); + catacurses::window w_guide = + catacurses::newwin( 4, TERMX - 3, point( getbegx( w ) + 2, TERMY - 5 ) ); + + draw_points( w, points ); + + const unsigned namebar_pos = 1 + utf8_width( _( "Name:" ) ); + unsigned male_pos = 1 + utf8_width( _( "Gender:" ) ); + unsigned female_pos = 2 + male_pos + utf8_width( _( "Male" ) ); + bool redraw = true; input_context ctxt( "NEW_CHAR_DESCRIPTION" ); - tabs.set_up_tab_navigation( ctxt ); - ctxt.register_cardinal(); ctxt.register_action( "SAVE_TEMPLATE" ); - ctxt.register_action( "RANDOMIZE_CHAR_NAME" ); - ctxt.register_action( "RANDOMIZE_CHAR_DESCRIPTION" ); - if( !MAP_SHARING::isSharing() && allow_reroll ) { - ctxt.register_action( "REROLL_CHARACTER" ); - ctxt.register_action( "REROLL_CHARACTER_WITH_SCENARIO" ); - } + ctxt.register_action( "PICK_RANDOM_NAME" ); ctxt.register_action( "CHANGE_GENDER" ); - ctxt.register_action( "CHANGE_OUTFIT" ); + ctxt.register_action( "PREV_TAB" ); + ctxt.register_action( "NEXT_TAB" ); ctxt.register_action( "HELP_KEYBINDINGS" ); - ctxt.register_action( "CHANGE_START_OF_CATACLYSM" ); - ctxt.register_action( "CHANGE_START_OF_GAME" ); - ctxt.register_action( "RESET_CALENDAR" ); - if( get_option( "SELECT_STARTING_CITY" ) ) { - ctxt.register_action( "CHOOSE_CITY" ); - } ctxt.register_action( "CHOOSE_LOCATION" ); - ctxt.register_action( "CONFIRM" ); + ctxt.register_action( "REROLL_CHARACTER" ); + ctxt.register_action( "REROLL_CHARACTER_WITH_SCENARIO" ); + ctxt.register_action( "ANY_INPUT" ); + ctxt.register_action( "QUIT" ); uilist select_location; select_location.text = _( "Select a starting location." ); - int offset = 1; - const std::string random_start_location_text = string_format( n_gettext( - "* Random location * (%d variant)", - "* Random location * (%d variants)", - get_scenario()->start_location_targets_count() ), get_scenario()->start_location_targets_count() ); - uilist_entry entry_random_start_location( RANDOM_START_LOC_ENTRY, true, -1, - random_start_location_text ); - select_location.entries.emplace_back( entry_random_start_location ); - for( const start_location &loc : start_locations::get_all() ) { - if( get_scenario()->allowed_start( loc.id ) ) { - std::string loc_name = loc.name(); - if( loc.targets_count() > 1 ) { - loc_name = string_format( n_gettext( "%s (%d variant)", - "%s (%d variants)", loc.targets_count() ), - loc_name, loc.targets_count() ); - } - - uilist_entry entry( loc.id.id().to_i(), true, -1, loc_name ); + int offset = 0; + for( const auto &loc : start_location::get_all() ) { + if( g->scen->allowed_start( loc.ident() ) ) { + uilist_entry entry( loc.ident().get_cid(), true, -1, loc.name() ); select_location.entries.emplace_back( entry ); - if( !you.random_start_location && loc.id.id() == you.start_location.id() ) { + if( loc.ident().get_cid() == you.start_location.get_cid() ) { select_location.selected = offset; } offset++; } } - if( you.random_start_location ) { - select_location.selected = 0; - } select_location.setup(); if( MAP_SHARING::isSharing() ) { you.name = MAP_SHARING::getUsername(); // set the current username as default character name @@ -4054,642 +2235,258 @@ void set_description( tab_manager &tabs, avatar &you, const bool allow_reroll, you.name = get_option( "DEF_CHAR_NAME" ); } - char_creation::description_selector current_selector = char_creation::NAME; + // do not switch IME mode now, but restore previous mode on return + ime_sentry sentry( ime_sentry::keep ); + do { + if( redraw ) { + //Draw the line between editable and non-editable stuff. + for( int i = 0; i < getmaxx( w ); ++i ) { + if( i == 0 ) { + mvwputch( w, point( i, 8 ), BORDER_COLOR, LINE_XXXO ); + } else if( i == getmaxx( w ) - 1 ) { + wputch( w, BORDER_COLOR, LINE_XOXX ); + } else { + wputch( w, BORDER_COLOR, LINE_OXOX ); + } + } + wrefresh( w ); + + wclear( w_stats ); + wclear( w_traits ); + wclear( w_skills ); + wclear( w_guide ); + + std::vector vStatNames; + mvwprintz( w_stats, point_zero, COL_HEADER, _( "Stats:" ) ); + vStatNames.push_back( _( "Strength:" ) ); + vStatNames.push_back( _( "Dexterity:" ) ); + vStatNames.push_back( _( "Intelligence:" ) ); + vStatNames.push_back( _( "Perception:" ) ); + int pos = 0; + for( size_t i = 0; i < vStatNames.size(); i++ ) { + pos = ( utf8_width( vStatNames[i] ) > pos ? + utf8_width( vStatNames[i] ) : pos ); + mvwprintz( w_stats, point( 0, i + 1 ), c_light_gray, vStatNames[i] ); + } + mvwprintz( w_stats, point( pos + 1, 1 ), c_light_gray, "%2d", you.str_max ); + mvwprintz( w_stats, point( pos + 1, 2 ), c_light_gray, "%2d", you.dex_max ); + mvwprintz( w_stats, point( pos + 1, 3 ), c_light_gray, "%2d", you.int_max ); + mvwprintz( w_stats, point( pos + 1, 4 ), c_light_gray, "%2d", you.per_max ); + wrefresh( w_stats ); - bool no_name_entered = false; - ui.on_redraw( [&]( ui_adaptor & ui ) { - const std::string help_text = assemble_description_help( ctxt, allow_reroll ); - const int new_iHelpHeight = foldstring( help_text, getmaxx( w ) - 4 ).size(); - if( new_iHelpHeight != iHelpHeight ) { - iHelpHeight = new_iHelpHeight; - init_windows( ui ); - } - werase( w ); - tabs.draw( w ); - draw_points( w, pool, you ); - - //Draw the line between editable and non-editable stuff. - for( int i = 0; i < getmaxx( w ); ++i ) { - if( i == 0 ) { - mvwputch( w, point( i, 9 ), BORDER_COLOR, LINE_XXXO ); - } else if( i == getmaxx( w ) - 1 ) { - wputch( w, BORDER_COLOR, LINE_XOXX ); - } else { - wputch( w, BORDER_COLOR, LINE_OXOX ); - } - } - wnoutrefresh( w ); - - werase( w_stats ); - std::vector vStatNames; - mvwprintz( w_stats, point_zero, COL_HEADER, _( "Stats:" ) ); - vStatNames.emplace_back( _( "Strength:" ) ); - vStatNames.emplace_back( _( "Dexterity:" ) ); - vStatNames.emplace_back( _( "Intelligence:" ) ); - vStatNames.emplace_back( _( "Perception:" ) ); - int pos = 0; - for( size_t i = 0; i < vStatNames.size(); i++ ) { - pos = ( utf8_width( vStatNames[i] ) > pos ? - utf8_width( vStatNames[i] ) : pos ); - mvwprintz( w_stats, point( 0, i + 1 ), c_light_gray, vStatNames[i] ); - } - mvwprintz( w_stats, point( pos + 1, 1 ), c_light_gray, "%2d", you.str_max ); - mvwprintz( w_stats, point( pos + 1, 2 ), c_light_gray, "%2d", you.dex_max ); - mvwprintz( w_stats, point( pos + 1, 3 ), c_light_gray, "%2d", you.int_max ); - mvwprintz( w_stats, point( pos + 1, 4 ), c_light_gray, "%2d", you.per_max ); - wnoutrefresh( w_stats ); - - if( isWide ) { - werase( w_traits ); mvwprintz( w_traits, point_zero, COL_HEADER, _( "Traits: " ) ); - std::vector current_traits = you.get_mutations_variants(); - std::sort( current_traits.begin(), current_traits.end(), trait_var_display_sort ); + std::vector current_traits = you.get_base_traits(); if( current_traits.empty() ) { wprintz( w_traits, c_light_red, _( "None!" ) ); } else { for( size_t i = 0; i < current_traits.size(); i++ ) { - const trait_and_var current_trait = current_traits[i]; + const auto current_trait = current_traits[i]; trim_and_print( w_traits, point( 0, i + 1 ), getmaxx( w_traits ) - 1, - current_trait.trait->get_display_color(), current_trait.name() ); + current_trait->get_display_color(), current_trait->name() ); } } - wnoutrefresh( w_traits ); - } + wrefresh( w_traits ); - if( isWide ) { - werase( w_skills ); mvwprintz( w_skills, point_zero, COL_HEADER, _( "Skills:" ) ); auto skillslist = Skill::get_skills_sorted_by( [&]( const Skill & a, const Skill & b ) { const int level_a = you.get_skill_level_object( a.ident() ).exercised_level(); const int level_b = you.get_skill_level_object( b.ident() ).exercised_level(); - return localized_compare( std::make_pair( -level_a, a.name() ), - std::make_pair( -level_b, b.name() ) ); + return level_a > level_b || ( level_a == level_b && a.name() < b.name() ); } ); int line = 1; bool has_skills = false; profession::StartingSkillList list_skills = you.prof->skills(); - - for( const Skill *&elem : skillslist ) { + for( auto &elem : skillslist ) { int level = you.get_skill_level( elem->ident() ); - - // Handle skills from professions - if( pool != pool_type::TRANSFER ) { - profession::StartingSkillList::iterator i = list_skills.begin(); - while( i != list_skills.end() ) { - if( i->first == elem->ident() ) { - level += i->second; - break; - } - ++i; - } - } - - // Handle skills from hobbies - int leftover_exp = 0; - int exp_to_level = 10000 * ( level + 1 ) * ( level + 1 ); - for( const profession *profession : you.hobbies ) { - profession::StartingSkillList hobby_skills = profession->skills(); - profession::StartingSkillList::iterator i = hobby_skills.begin(); - while( i != hobby_skills.end() ) { - if( i->first == elem->ident() ) { - int skill_exp_bonus = leftover_exp + calculate_cumulative_experience( i->second ); - // Calculate Level up to find final level and remaining exp - while( skill_exp_bonus >= exp_to_level ) { - level++; - skill_exp_bonus -= exp_to_level; - exp_to_level = 10000 * ( level + 1 ) * ( level + 1 ); - } - leftover_exp = skill_exp_bonus; - break; - } - ++i; + profession::StartingSkillList::iterator i = list_skills.begin(); + while( i != list_skills.end() ) { + if( i->first == ( elem )->ident() ) { + level += i->second; + break; } + ++i; } if( level > 0 ) { - const int exp_percent = 100 * leftover_exp / exp_to_level; mvwprintz( w_skills, point( 0, line ), c_light_gray, elem->name() + ":" ); - right_print( w_skills, line, 1, c_light_gray, string_format( "%d(%2d%%)", level, exp_percent ) ); + mvwprintz( w_skills, point( 23, line ), c_light_gray, "%-2d", static_cast( level ) ); line++; has_skills = true; } } - if( !has_skills ) { mvwprintz( w_skills, point( utf8_width( _( "Skills:" ) ) + 1, 0 ), c_light_red, _( "None!" ) ); } - wnoutrefresh( w_skills ); - } - - if( isWide ) { - werase( w_bionics ); - // Profession bionics description tab, active bionics shown first - auto prof_CBMs = you.prof->CBMs(); - std::sort( begin( prof_CBMs ), end( prof_CBMs ), []( const bionic_id & a, const bionic_id & b ) { - return a->activated && !b->activated; - } ); - mvwprintz( w_bionics, point_zero, COL_HEADER, _( "Bionics:" ) ); - if( prof_CBMs.empty() ) { - mvwprintz( w_bionics, point( utf8_width( _( "Bionics:" ) ) + 1, 0 ), c_light_red, _( "None!" ) ); - } else { - for( const auto &b : prof_CBMs ) { - const bionic_data &cbm = b.obj(); - - if( cbm.activated && cbm.has_flag( json_flag_BIONIC_TOGGLED ) ) { - wprintz( w_bionics, c_light_gray, string_format( _( "\n%s (toggled)" ), cbm.name ) ); - } else if( cbm.activated ) { - wprintz( w_bionics, c_light_gray, string_format( _( "\n%s (activated)" ), cbm.name ) ); - } else { - wprintz( w_bionics, c_light_gray, "\n" + cbm.name ); - } - } - } - wnoutrefresh( w_bionics ); - } - - // Proficiencies description tab - if( isWide ) { - werase( w_proficiencies ); - // Load in proficiencies from profession and hobbies - std::vector prof_proficiencies = you.prof->proficiencies(); - const std::vector &known_proficiencies = you._proficiencies->known_profs(); - prof_proficiencies.insert( prof_proficiencies.end(), known_proficiencies.begin(), - known_proficiencies.end() ); - for( const profession *profession : you.hobbies ) { - for( const proficiency_id &proficiency : profession->proficiencies() ) { - // Do not add duplicate proficiencies - if( std::find( prof_proficiencies.begin(), prof_proficiencies.end(), - proficiency ) == prof_proficiencies.end() ) { - prof_proficiencies.push_back( proficiency ); - } - } - } - - mvwprintz( w_proficiencies, point_zero, COL_HEADER, _( "Proficiencies:" ) ); - if( prof_proficiencies.empty() ) { - mvwprintz( w_proficiencies, point_south, c_light_red, _( "None!" ) ); - } else { - for( const proficiency_id &prof : prof_proficiencies ) { - wprintz( w_proficiencies, c_light_gray, "\n" + trim_by_length( prof->name(), 18 ) ); - } - } - wnoutrefresh( w_proficiencies ); - } - - // Helptext description window - werase( w_guide ); - fold_and_print( w_guide, point_zero, getmaxx( w_guide ), c_light_gray, help_text ); - wnoutrefresh( w_guide ); - - char_creation::draw_name( ui, w_name, you, current_selector == char_creation::NAME, - no_name_entered ); - char_creation::draw_gender( ui, w_gender, you, current_selector == char_creation::GENDER ); - char_creation::draw_outfit( ui, w_outfit, current_selector == char_creation::OUTFIT ); - char_creation::draw_age( ui, w_age, you, current_selector == char_creation::AGE ); - char_creation::draw_height( ui, w_height, you, current_selector == char_creation::HEIGHT ); - char_creation::draw_blood( ui, w_blood, you, current_selector == char_creation::BLOOD ); - char_creation::draw_location( ui, w_location, you, current_selector == char_creation::LOCATION ); - - werase( w_vehicle ); - // Player vehicle description tab - const vproto_id scen_veh = get_scenario()->vehicle(); - const vproto_id prof_veh = you.prof->vehicle(); - if( isWide ) { - if( scen_veh ) { - mvwprintz( w_vehicle, point_zero, c_light_gray, _( "Starting vehicle (scenario): " ) ); - wprintz( w_vehicle, c_white, "%s", scen_veh->name ); - } else if( prof_veh ) { - mvwprintz( w_vehicle, point_zero, c_light_gray, _( "Starting vehicle (profession): " ) ); - wprintz( w_vehicle, c_white, "%s", prof_veh->name ); - } else { - mvwprintz( w_vehicle, point_zero, c_light_gray, _( "Starting vehicle: " ) ); - wprintz( w_vehicle, c_light_red, _( "None!" ) ); - } - } else { - if( scen_veh ) { - mvwprintz( w_vehicle, point_zero, COL_HEADER, _( "Starting vehicle (scenario): " ) ); - wprintz( w_vehicle, c_light_gray, "\n%s", scen_veh->name ); - } else if( prof_veh ) { - mvwprintz( w_vehicle, point_zero, COL_HEADER, _( "Starting vehicle (profession): " ) ); - wprintz( w_vehicle, c_light_gray, "\n%s", prof_veh->name ); - } else { - mvwprintz( w_vehicle, point_zero, COL_HEADER, _( "Starting vehicle: " ) ); - wprintz( w_vehicle, c_light_red, _( "None!" ) ); - } - } - wnoutrefresh( w_vehicle ); - - werase( w_addictions ); - // Profession addictions description tab - std::vector prof_addictions = you.prof->addictions(); - for( const profession *profession : you.hobbies ) { - const std::vector &hobby_addictions = profession->addictions(); - for( const addiction &iter : hobby_addictions ) { - prof_addictions.push_back( iter ); - } - } - if( isWide ) { - mvwprintz( w_addictions, point_zero, c_light_gray, _( "Starting addictions: " ) ); - if( prof_addictions.empty() ) { - wprintz( w_addictions, c_light_red, _( "None!" ) ); - } else { - for( const addiction &a : prof_addictions ) { - const char *format = "%1$s (%2$d) "; - wprintz( w_addictions, c_white, string_format( format, a.type->get_name().translated(), - a.intensity ) ); - } - } - } else { - mvwprintz( w_addictions, point_zero, COL_HEADER, _( "Starting addictions: " ) ); - if( prof_addictions.empty() ) { - wprintz( w_addictions, c_light_red, _( "None!" ) ); + wrefresh( w_skills ); + + mvwprintz( w_guide, point( 0, getmaxy( w_guide ) - 1 ), c_green, + _( "Press %s to finish character creation or %s to go back." ), + ctxt.get_desc( "NEXT_TAB" ), + ctxt.get_desc( "PREV_TAB" ) ); + if( allow_reroll ) { + mvwprintz( w_guide, point( 0, getmaxy( w_guide ) - 2 ), c_green, + _( "Press %s to save character template, %s to re-roll or %s for random scenario." ), + ctxt.get_desc( "SAVE_TEMPLATE" ), + ctxt.get_desc( "REROLL_CHARACTER" ), + ctxt.get_desc( "REROLL_CHARACTER_WITH_SCENARIO" ) ); } else { - for( const addiction &a : prof_addictions ) { - const char *format = "%1$s (%2$d) "; - wprintz( w_addictions, c_light_gray, "\n" ); - wprintz( w_addictions, c_light_gray, string_format( format, a.type->get_name().translated(), - a.intensity ) ); - } - } - } - wnoutrefresh( w_addictions ); + mvwprintz( w_guide, point( 0, getmaxy( w_guide ) - 2 ), c_green, + _( "Press %s to save a template of this character." ), + ctxt.get_desc( "SAVE_TEMPLATE" ) ); + } + wrefresh( w_guide ); + + redraw = false; + } + + //We draw this stuff every loop because this is user-editable + mvwprintz( w_name, point_zero, c_light_gray, _( "Name:" ) ); + mvwprintz( w_name, point( namebar_pos, 0 ), c_light_gray, "_______________________________" ); + mvwprintz( w_name, point( namebar_pos, 0 ), c_white, you.name ); + wprintz( w_name, h_light_gray, "_" ); + + if( !MAP_SHARING::isSharing() ) { // no random names when sharing maps + // NOLINTNEXTLINE(cata-use-named-point-constants) + mvwprintz( w_name, point( 0, 1 ), c_light_gray, _( "Press %s to pick a random name." ), + ctxt.get_desc( "PICK_RANDOM_NAME" ) ); + } + wrefresh( w_name ); + + mvwprintz( w_gender, point_zero, c_light_gray, _( "Gender:" ) ); + mvwprintz( w_gender, point( male_pos, 0 ), ( you.male ? c_light_red : c_light_gray ), _( "Male" ) ); + mvwprintz( w_gender, point( female_pos, 0 ), ( you.male ? c_light_gray : c_light_red ), + _( "Female" ) ); + // NOLINTNEXTLINE(cata-use-named-point-constants) + mvwprintz( w_gender, point( 0, 1 ), c_light_gray, _( "Press %s to switch gender" ), + ctxt.get_desc( "CHANGE_GENDER" ) ); + wrefresh( w_gender ); + + const std::string location_prompt = string_format( _( "Press %s to select location." ), + ctxt.get_desc( "CHOOSE_LOCATION" ) ); + const int prompt_offset = utf8_width( location_prompt ); + werase( w_location ); + mvwprintz( w_location, point_zero, c_light_gray, location_prompt ); + mvwprintz( w_location, point( prompt_offset + 1, 0 ), c_light_gray, _( "Starting location:" ) ); + // ::find will return empty location if id was not found. Debug msg will be printed too. + mvwprintz( w_location, point( prompt_offset + utf8_width( _( "Starting location:" ) ) + 2, 0 ), + c_light_gray, you.start_location.obj().name() ); + wrefresh( w_location ); werase( w_scenario ); mvwprintz( w_scenario, point_zero, COL_HEADER, _( "Scenario: " ) ); - wprintz( w_scenario, c_light_gray, get_scenario()->gender_appropriate_name( you.male ) ); - wnoutrefresh( w_scenario ); + wprintz( w_scenario, c_light_gray, g->scen->gender_appropriate_name( you.male ) ); + wrefresh( w_scenario ); werase( w_profession ); mvwprintz( w_profession, point_zero, COL_HEADER, _( "Profession: " ) ); wprintz( w_profession, c_light_gray, you.prof->gender_appropriate_name( you.male ) ); - wnoutrefresh( w_profession ); - - werase( w_calendar ); - mvwprintz( w_calendar, point_zero, COL_HEADER, _( "Start of cataclysm:" ) ); - wprintz( w_calendar, c_light_gray, "\n" ); - wprintz( w_calendar, c_light_gray, to_string( get_scenario()->start_of_cataclysm() ) ); - wprintz( w_calendar, c_light_gray, "\n" ); - wprintz( w_calendar, COL_HEADER, _( "Start of game:" ) ); - wprintz( w_calendar, c_light_gray, "\n" ); - wprintz( w_calendar, c_light_gray, to_string( get_scenario()->start_of_game() ) ); - wnoutrefresh( w_calendar ); - - if( isWide ) { - werase( w_hobbies ); - mvwprintz( w_hobbies, point_zero, COL_HEADER, _( "Background: " ) ); - if( you.hobbies.empty() ) { - mvwprintz( w_hobbies, point_south, c_light_red, _( "None!" ) ); - } else { - for( const profession *prof : you.hobbies ) { - wprintz( w_hobbies, c_light_gray, "\n%s", prof->gender_appropriate_name( you.male ) ); - } - } - wnoutrefresh( w_hobbies ); - } - } ); - - int min_allowed_age = 16; - int max_allowed_age = 55; + wrefresh( w_profession ); - int min_allowed_height = Character::min_height(); - int max_allowed_height = Character::max_height(); - - do { - ui_manager::redraw(); const std::string action = ctxt.handle_input(); + if( action == "NEXT_TAB" ) { - if( pool == pool_type::ONE_POOL ) { - if( points_used_total( you ) > point_pool_total() ) { - popup( _( "Too many points allocated, change some features and try again." ) ); - continue; - } - } else if( pool == pool_type::MULTI_POOL ) { - multi_pool p( you ); - if( p.skill_points_left < 0 ) { + if( !points.is_valid() ) { + if( points.skill_points_left() < 0 ) { popup( _( "Too many points allocated, change some features and try again." ) ); - continue; - } - if( p.trait_points_left < 0 ) { + } else if( points.trait_points_left() < 0 ) { popup( _( "Too many trait points allocated, change some traits or lower some stats and try again." ) ); - continue; - } - if( p.stat_points_left < 0 ) { + } else if( points.stat_points_left() < 0 ) { popup( _( "Too many stat points allocated, lower some stats and try again." ) ); - continue; + } else { + popup( _( "Too many points allocated, change some features and try again." ) ); } - } - if( has_unspent_points( you ) && pool != pool_type::FREEFORM && - !query_yn( _( "Remaining points will be discarded, are you sure you want to proceed?" ) ) ) { + redraw = true; continue; - } - if( you.name.empty() ) { - no_name_entered = true; - ui_manager::redraw(); + } else if( points.has_spare() && + !query_yn( _( "Remaining points will be discarded, are you sure you want to proceed?" ) ) ) { + redraw = true; + continue; + } else if( you.name.empty() ) { + mvwprintz( w_name, point( namebar_pos, 0 ), h_light_gray, _( "_______NO NAME ENTERED!_______" ) ); + wrefresh( w_name ); if( !query_yn( _( "Are you SURE you're finished? Your name will be randomly generated." ) ) ) { + redraw = true; continue; } else { you.pick_name(); - tabs.complete = true; - break; + return tab_direction::FORWARD; } + } else if( query_yn( _( "Are you SURE you're finished?" ) ) ) { + return tab_direction::FORWARD; + } else { + redraw = true; + continue; } - if( query_yn( _( "Are you SURE you're finished?" ) ) ) { - tabs.complete = true; - break; - } - continue; - } else if( tabs.handle_input( action, ctxt ) ) { - break; // Tab has changed or user has quit the screen - } else if( action == "DOWN" ) { - switch( current_selector ) { - case char_creation::NAME: - current_selector = char_creation::GENDER; - break; - case char_creation::GENDER: - current_selector = char_creation::OUTFIT; - break; - case char_creation::OUTFIT: - current_selector = char_creation::HEIGHT; - break; - case char_creation::HEIGHT: - current_selector = char_creation::AGE; - break; - case char_creation::AGE: - current_selector = char_creation::BLOOD; - break; - case char_creation::BLOOD: - current_selector = char_creation::LOCATION; - break; - case char_creation::LOCATION: - current_selector = char_creation::NAME; - break; - } - } else if( action == "UP" ) { - switch( current_selector ) { - case char_creation::NAME: - current_selector = char_creation::LOCATION; - break; - case char_creation::LOCATION: - current_selector = char_creation::BLOOD; - break; - case char_creation::BLOOD: - current_selector = char_creation::AGE; - break; - case char_creation::AGE: - current_selector = char_creation::HEIGHT; - break; - case char_creation::HEIGHT: - current_selector = char_creation::OUTFIT; - break; - case char_creation::OUTFIT: - current_selector = char_creation::GENDER; - break; - case char_creation::GENDER: - current_selector = char_creation::NAME; - break; - } - } else if( action == "RIGHT" ) { - switch( current_selector ) { - case char_creation::HEIGHT: - if( you.base_height() < max_allowed_height ) { - you.mod_base_height( 1 ); - you.set_stored_kcal( you.get_healthy_kcal() ); - } - break; - case char_creation::AGE: - if( you.base_age() < max_allowed_age ) { - you.mod_base_age( 1 ); - } - break; - case char_creation::BLOOD: - if( !you.blood_rh_factor ) { - you.blood_rh_factor = true; - break; - } - if( static_cast( static_cast( you.my_blood_type ) + 1 ) != blood_type::num_bt ) { - you.my_blood_type = static_cast( static_cast( you.my_blood_type ) + 1 ); - you.blood_rh_factor = false; - } else { - you.my_blood_type = static_cast( 0 ); - you.blood_rh_factor = false; - } - break; - case char_creation::GENDER: - you.male = !you.male; - break; - case char_creation::OUTFIT: - outfit = !outfit; - break; - default: - break; - } - } else if( action == "LEFT" ) { - switch( current_selector ) { - case char_creation::HEIGHT: - if( you.base_height() > min_allowed_height ) { - you.mod_base_height( -1 ); - you.set_stored_kcal( you.get_healthy_kcal() ); - } - break; - case char_creation::AGE: - if( you.base_age() > min_allowed_age ) { - you.mod_base_age( -1 ); - } - break; - case char_creation::BLOOD: - if( you.blood_rh_factor ) { - you.blood_rh_factor = false; - break; - } - if( you.my_blood_type != static_cast( 0 ) ) { - you.my_blood_type = static_cast( static_cast( you.my_blood_type ) - 1 ); - you.blood_rh_factor = true; - } else { - you.my_blood_type = static_cast( static_cast( blood_type::num_bt ) - 1 ); - you.blood_rh_factor = true; - } - break; - case char_creation::GENDER: - you.male = !you.male; - break; - case char_creation::OUTFIT: - outfit = !outfit; - break; - default: - break; - } + } else if( action == "PREV_TAB" ) { + return tab_direction::BACKWARD; } else if( action == "REROLL_CHARACTER" && allow_reroll ) { - you.randomize( false ); - // Re-enter this tab again, but it forces a complete redrawing of it. - break; + points.init_from_options(); + you.randomize( false, points ); + // Return tab_direction::NONE so we re-enter this tab again, but it forces a complete redrawing of it. + return tab_direction::NONE; } else if( action == "REROLL_CHARACTER_WITH_SCENARIO" && allow_reroll ) { - you.randomize( true ); - // Re-enter this tab again, but it forces a complete redrawing of it. - break; + points.init_from_options(); + you.randomize( true, points ); + // Return tab_direction::NONE so we re-enter this tab again, but it forces a complete redrawing of it. + return tab_direction::NONE; } else if( action == "SAVE_TEMPLATE" ) { if( const auto name = query_for_template_name() ) { - you.save_template( *name, pool ); + ::save_template( you, *name, points ); } - } else if( action == "RANDOMIZE_CHAR_NAME" ) { + // redraw after saving template + draw_character_tabs( w, _( "DESCRIPTION" ) ); + draw_points( w, points ); + redraw = true; + } else if( action == "PICK_RANDOM_NAME" ) { if( !MAP_SHARING::isSharing() ) { // Don't allow random names when sharing maps. We don't need to check at the top as you won't be able to edit the name you.pick_name(); - no_name_entered = you.name.empty(); } - } else if( action == "RANDOMIZE_CHAR_DESCRIPTION" ) { - bool gender_selection = one_in( 2 ); - you.male = gender_selection; - outfit = gender_selection; - if( !MAP_SHARING::isSharing() ) { // Don't allow random names when sharing maps. We don't need to check at the top as you won't be able to edit the name - you.pick_name(); - no_name_entered = you.name.empty(); - } - you.set_base_age( rng( 16, 55 ) ); - you.randomize_height(); - you.randomize_blood(); - you.randomize_heartrate(); - } else if( action == "CHANGE_OUTFIT" ) { - outfit = !outfit; } else if( action == "CHANGE_GENDER" ) { you.male = !you.male; - } else if( action == "CHANGE_START_OF_CATACLYSM" ) { - const scenario *scen = get_scenario(); - scen->change_start_of_cataclysm( calendar_ui::select_time_point( scen->start_of_cataclysm(), - _( "Select cataclysm start date" ), calendar_ui::granularity::hour ) ); - } else if( action == "CHANGE_START_OF_GAME" ) { - const scenario *scen = get_scenario(); - scen->change_start_of_game( calendar_ui::select_time_point( scen->start_of_game(), - _( "Select game start date" ), calendar_ui::granularity::hour ) ); - } else if( action == "RESET_CALENDAR" ) { - get_scenario()->reset_calendar(); - } else if( action == "CHOOSE_CITY" ) { - std::vector cities( city::get_all() ); - const auto cities_cmp_population = []( const city & a, const city & b ) { - return std::tie( a.population, a.name ) > std::tie( b.population, b.name ); - }; - std::sort( cities.begin(), cities.end(), cities_cmp_population ); - uilist cities_menu; - ui::omap::setup_cities_menu( cities_menu, cities ); - std::optional c = ui::omap::select_city( cities_menu, cities, false ); - if( c.has_value() ) { - you.starting_city = c; - you.world_origin = c->pos_om; - } } else if( action == "CHOOSE_LOCATION" ) { + select_location.redraw(); select_location.query(); - if( select_location.ret == RANDOM_START_LOC_ENTRY ) { - you.random_start_location = true; - } else if( select_location.ret >= 0 ) { - for( const start_location &loc : start_locations::get_all() ) { - if( loc.id.id().to_i() == select_location.ret ) { - you.random_start_location = false; - you.start_location = loc.id; + if( select_location.ret >= 0 ) { + for( const auto &loc : start_location::get_all() ) { + if( loc.ident().get_cid() == select_location.ret ) { + you.start_location = loc.ident(); break; } } } - } else if( action == "CONFIRM" && - // Don't edit names when sharing maps - !MAP_SHARING::isSharing() ) { - - string_input_popup popup; - switch( current_selector ) { - case char_creation::NAME: { - popup.title( _( "Enter name. Cancel to delete all." ) ) - .text( you.name ) - .only_digits( false ); - you.name = popup.query_string(); - no_name_entered = you.name.empty(); - break; - } - case char_creation::AGE: { - popup.title( _( "Enter age in years. Minimum 16, maximum 55" ) ) - .text( string_format( "%d", you.base_age() ) ) - .only_digits( true ); - const int result = popup.query_int(); - if( result != 0 ) { - you.set_base_age( clamp( result, 16, 55 ) ); - } - break; - } - case char_creation::HEIGHT: { - popup.title( string_format( _( "Enter height in centimeters. Minimum %d, maximum %d" ), - min_allowed_height, - max_allowed_height ) ) - .text( string_format( "%d", you.base_height() ) ) - .only_digits( true ); - const int result = popup.query_int(); - if( result != 0 ) { - you.set_base_height( clamp( result, min_allowed_height, max_allowed_height ) ); - } - break; - } - case char_creation::BLOOD: { - uilist btype; - btype.text = _( "Select blood type" ); - btype.addentry( static_cast( blood_type::blood_O ), true, '1', "O" ); - btype.addentry( static_cast( blood_type::blood_A ), true, '2', "A" ); - btype.addentry( static_cast( blood_type::blood_B ), true, '3', "B" ); - btype.addentry( static_cast( blood_type::blood_AB ), true, '4', "AB" ); - btype.query(); - if( btype.ret < 0 ) { - break; - } - - uilist bfac; - bfac.text = _( "Select Rh factor" ); - bfac.addentry( 0, true, '-', _( "negative" ) ); - bfac.addentry( 1, true, '+', _( "positive" ) ); - bfac.query(); - if( bfac.ret < 0 ) { - break; - } - you.my_blood_type = static_cast( btype.ret ); - you.blood_rh_factor = static_cast( bfac.ret ); - break; - } - case char_creation::GENDER: { - uilist gselect; - gselect.text = _( "Select gender" ); - gselect.addentry( 0, true, '1', _( "Female" ) ); - gselect.addentry( 1, true, '2', _( "Male" ) ); - gselect.query(); - if( gselect.ret < 0 ) { - break; - } - you.male = static_cast( gselect.ret ); - break; - } - case char_creation::OUTFIT: { - uilist gselect; - gselect.text = _( "Select outfit" ); - gselect.addentry( 0, true, '1', _( "Female" ) ); - gselect.addentry( 1, true, '2', _( "Male" ) ); - gselect.query(); - if( gselect.ret < 0 ) { - break; - } - outfit = static_cast( gselect.ret ); - break; - } - case char_creation::LOCATION: { - select_location.query(); - if( select_location.ret == RANDOM_START_LOC_ENTRY ) { - you.random_start_location = true; - } else if( select_location.ret >= 0 ) { - for( const start_location &loc : start_locations::get_all() ) { - if( loc.id.id().to_i() == select_location.ret ) { - you.random_start_location = false; - you.start_location = loc.id; - break; - } - } - } - break; - } + werase( select_location.window ); + select_location.refresh(); + redraw = true; + } else if( action == "HELP_KEYBINDINGS" ) { + // Need to redraw since the help window obscured everything. + draw_character_tabs( w, _( "DESCRIPTION" ) ); + draw_points( w, points ); + redraw = true; + } else if( action == "ANY_INPUT" && + !MAP_SHARING::isSharing() ) { // Don't edit names when sharing maps + const int ch = ctxt.get_raw_input().get_first_input(); + utf8_wrapper wrap( you.name ); + if( ch == KEY_BACKSPACE ) { + if( !wrap.empty() ) { + wrap.erase( wrap.length() - 1, 1 ); + you.name = wrap.str(); + } + } else if( ch == KEY_F( 2 ) ) { + utf8_wrapper tmp( get_input_string_from_file() ); + if( !tmp.empty() && tmp.length() + wrap.length() < 30 ) { + you.name.append( tmp.str() ); + } + } else if( ch == '\n' ) { + // nope, we ignore this newline, don't want it in char names + } else { + wrap.append( ctxt.get_raw_input().text ); + you.name = wrap.str(); } + } else if( action == "QUIT" && query_yn( _( "Return to main menu?" ) ) ) { + return tab_direction::QUIT; } } while( true ); } @@ -4699,146 +2496,84 @@ std::vector Character::get_base_traits() const return std::vector( my_traits.begin(), my_traits.end() ); } -std::vector Character::get_mutations( bool include_hidden, - bool ignore_enchantments, const std::function &filter ) const +std::vector Character::get_mutations( bool include_hidden ) const { std::vector result; - result.reserve( my_mutations.size() + enchantment_cache->get_mutations().size() ); - for( const std::pair &t : my_mutations ) { - const mutation_branch &mut = t.first.obj(); - if( include_hidden || mut.player_display ) { - if( filter == nullptr || filter( mut ) ) { - result.push_back( t.first ); - } - } - } - if( !ignore_enchantments ) { - for( const trait_id &ench_trait : enchantment_cache->get_mutations() ) { - if( include_hidden || ench_trait->player_display ) { - bool found = false; - for( const trait_id &exist : result ) { - if( exist == ench_trait ) { - found = true; - break; - } - } - if( !found ) { - if( filter == nullptr || filter( ench_trait.obj() ) ) { - result.push_back( ench_trait ); - } - } - } - } - } - return result; -} - -std::vector Character::get_mutations_variants( bool include_hidden, - bool ignore_enchantments ) const -{ - std::vector result; - result.reserve( my_mutations.size() + enchantment_cache->get_mutations().size() ); - for( const std::pair &t : my_mutations ) { + for( auto &t : my_mutations ) { if( include_hidden || t.first.obj().player_display ) { - const std::string &variant = t.second.variant != nullptr ? t.second.variant->id : ""; - result.emplace_back( t.first, variant ); - } - } - if( !ignore_enchantments ) { - for( const trait_id &ench_trait : enchantment_cache->get_mutations() ) { - if( include_hidden || ench_trait->player_display ) { - bool found = false; - for( const trait_and_var &exist : result ) { - if( exist.trait == ench_trait ) { - found = true; - break; - } - } - if( !found ) { - result.emplace_back( ench_trait, "" ); - } - } + result.push_back( t.first ); } } return result; } -void Character::clear_mutations() +void Character::empty_traits() { - while( !my_traits.empty() ) { - my_traits.erase( *my_traits.begin() ); - } - while( !my_mutations.empty() ) { - const trait_id trait = my_mutations.begin()->first; - my_mutations.erase( my_mutations.begin() ); - mutation_loss_effect( trait ); + for( auto &mut : my_mutations ) { + on_mutation_loss( mut.first ); } + my_traits.clear(); + my_mutations.clear(); cached_mutations.clear(); - recalc_sight_limits(); - calc_encumbrance(); } void Character::empty_skills() { for( auto &sk : *_skills ) { - sk.second = SkillLevel(); + sk.second.level( 0 ); } } void Character::add_traits() { - // TODO: get rid of using get_avatar() here, use `this` instead - for( const trait_and_var &tr : get_avatar().prof->get_locked_traits() ) { - if( !has_trait( tr.trait ) ) { - toggle_trait_deps( tr.trait ); + points_left points = points_left(); + add_traits( points ); +} + +void Character::add_traits( points_left &points ) +{ + // TODO: get rid of using g->u here, use `this` instead + for( const trait_id &tr : g->u.prof->get_locked_traits() ) { + if( !has_trait( tr ) ) { + toggle_trait( tr ); + } else { + points.trait_points += tr->points; } } - for( const trait_id &tr : get_scenario()->get_locked_traits() ) { + for( const trait_id &tr : g->scen->get_locked_traits() ) { if( !has_trait( tr ) ) { - toggle_trait_deps( tr ); + toggle_trait( tr ); } } } trait_id Character::random_good_trait() { - return get_random_trait( []( const mutation_branch & mb ) { - return mb.points > 0; - } ); -} - -trait_id Character::random_bad_trait() -{ - return get_random_trait( []( const mutation_branch & mb ) { - return mb.points < 0; - } ); -} - -trait_id Character::get_random_trait( const std::function &func ) -{ - std::vector vTraits; + std::vector vTraitsGood; - for( const mutation_branch &traits_iter : mutation_branch::get_all() ) { - if( func( traits_iter ) && get_scenario()->traitquery( traits_iter.id ) ) { - vTraits.push_back( traits_iter.id ); + for( auto &traits_iter : mutation_branch::get_all() ) { + if( traits_iter.points >= 0 && g->scen->traitquery( traits_iter.id ) ) { + vTraitsGood.push_back( traits_iter.id ); } } - return random_entry( vTraits ); + return random_entry( vTraitsGood ); } -void Character::randomize_cosmetic_trait( const std::string &mutation_type ) +trait_id Character::random_bad_trait() { - trait_id trait = get_random_trait( [mutation_type]( const mutation_branch & mb ) { - return mb.points == 0 && mb.types.count( mutation_type ); - } ); + std::vector vTraitsBad; - if( !has_conflicting_trait( trait ) ) { - toggle_trait( trait ); + for( auto &traits_iter : mutation_branch::get_all() ) { + if( traits_iter.points < 0 && g->scen->traitquery( traits_iter.id ) ) { + vTraitsBad.push_back( traits_iter.id ); + } } + + return random_entry( vTraitsBad ); } -std::optional query_for_template_name() +cata::optional query_for_template_name() { static const std::set fname_char_blacklist = { #if defined(_WIN32) @@ -4859,102 +2594,93 @@ std::optional query_for_template_name() spop.description( desc ); spop.width( FULL_SCREEN_WIDTH - utf8_width( title ) - 8 ); for( int character : fname_char_blacklist ) { - spop.add_callback( character, []() { + spop.callbacks[ character ] = []() { return true; - } ); + }; } spop.query_string( true ); if( spop.canceled() ) { - return std::nullopt; + return cata::nullopt; } else { return spop.text(); } } -void avatar::character_to_template( const std::string &name ) +void save_template( const avatar &u, const std::string &name, const points_left &points ) { - save_template( name, pool_type::TRANSFER ); -} - -void Character::add_default_background() -{ - for( const profession_group &prof_grp : profession_group::get_all() ) { - if( prof_grp.get_id() == profession_group_adult_basic_background ) { - for( const profession_id &hobb : prof_grp.get_professions() ) { - hobbies.insert( &hobb.obj() ); - } - } + std::string native = utf8_to_native( name ); +#if defined(_WIN32) + if( native.find_first_of( "\"*/:<>?\\|" + "\x01\x02\x03\x04\x05\x06\x07\x08\x09" // NOLINT(cata-text-style) + "\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12" // NOLINT(cata-text-style) + "\x13\x14\x15\x16\x17\x18\x19\x1A\x1B" + "\x1C\x1D\x1E\x1F" + ) != std::string::npos ) { + popup( _( "Conversion of your filename to your native character set resulted in some unsafe characters, please try an alphanumeric filename instead" ) ); + return; } -} +#endif -void avatar::save_template( const std::string &name, pool_type pool ) -{ - write_to_file( PATH_INFO::templatedir() + name + ".template", [&]( std::ostream & fout ) { + write_to_file( FILENAMES["templatedir"] + native + ".template", [&]( std::ostream & fout ) { JsonOut jsout( fout, true ); jsout.start_array(); jsout.start_object(); - jsout.member( "limit", pool ); - jsout.member( "random_start_location", random_start_location ); - if( !random_start_location ) { - jsout.member( "start_location", start_location ); - } + jsout.member( "stat_points", points.stat_points ); + jsout.member( "trait_points", points.trait_points ); + jsout.member( "skill_points", points.skill_points ); + jsout.member( "limit", points.limit ); + jsout.member( "start_location", u.start_location ); jsout.end_object(); - serialize( jsout ); + u.serialize( jsout ); jsout.end_array(); }, _( "player template" ) ); } -bool avatar::load_template( const std::string &template_name, pool_type &pool ) +bool avatar::load_template( const std::string &template_name, points_left &points ) { - return read_from_file_json( PATH_INFO::templatedir_path() / ( template_name + ".template" ), [&]( - const JsonValue & jv ) { - // Legacy templates are an object. Current templates are an array of 0, 1, or 2 objects. - JsonObject legacy_template; - if( jv.test_array() ) { + return read_from_file_json( FILENAMES["templatedir"] + utf8_to_native( template_name ) + + ".template", [&]( JsonIn & jsin ) { + + if( jsin.test_array() ) { // not a legacy template - JsonArray template_array = jv; - if( template_array.empty() ) { + jsin.start_array(); + + if( jsin.end_array() ) { return; } - JsonObject jobj = template_array.get_object( 0 ); - - jobj.get_int( "stat_points", 0 ); - jobj.get_int( "trait_points", 0 ); - jobj.get_int( "skill_points", 0 ); + JsonObject jobj = jsin.get_object(); - pool = static_cast( jobj.get_int( "limit" ) ); + points.stat_points = jobj.get_int( "stat_points" ); + points.trait_points = jobj.get_int( "trait_points" ); + points.skill_points = jobj.get_int( "skill_points" ); + points.limit = static_cast( jobj.get_int( "limit" ) ); - random_start_location = jobj.get_bool( "random_start_location", true ); const std::string jobj_start_location = jobj.get_string( "start_location", "" ); - // get_scenario()->allowed_start( loc.ident() ) is checked once scenario loads in avatar::load() - for( const class start_location &loc : start_locations::get_all() ) { - if( loc.id.str() == jobj_start_location ) { - random_start_location = false; - this->start_location = loc.id; + // g->scen->allowed_start( loc.ident() ) is checked once scenario loads in avatar::load() + for( const auto &loc : start_location::get_all() ) { + if( loc.ident().str() == jobj_start_location ) { + this->start_location = loc.ident(); break; } } - if( template_array.size() == 1 ) { + if( jsin.end_array() ) { return; } - legacy_template = template_array.get_object( 1 ); + } else { + points.stat_points = 0; + points.trait_points = 0; + points.skill_points = 0; } - deserialize( legacy_template ); - - // If stored_calories the template is under a million (kcals < 1000), assume it predates the - // kilocalorie-to-literal-calorie conversion and is off by a factor of 1000. - if( get_stored_kcal() < 1000 ) { - set_stored_kcal( 1000 * get_stored_kcal() ); - } + deserialize( jsin ); if( MAP_SHARING::isSharing() ) { // just to make sure we have the right name @@ -4970,17 +2696,19 @@ void reset_scenario( avatar &u, const scenario *scen ) const auto permitted = scen->permitted_professions(); const auto default_prof = *std::min_element( permitted.begin(), permitted.end(), psorter ); - u.random_start_location = true; + u.start_location = scen->start_location(); u.str_max = 8; u.dex_max = 8; u.int_max = 8; u.per_max = 8; - set_scenario( scen ); + g->scen = scen; u.prof = &default_prof.obj(); - - u.hobbies.clear(); - u.add_default_background(); - u.clear_mutations(); + for( auto &t : u.get_mutations() ) { + if( t.obj().hp_modifier != 0 ) { + u.toggle_trait( t ); + } + } + u.empty_traits(); u.recalc_hp(); u.empty_skills(); u.add_traits();