Skip to content
Permalink
Tree: 446c2f4930
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
2523 lines (2169 sloc) 91.3 KB
#include "veh_interact.h"
#include <string>
#include "vehicle.h"
#include "overmapbuffer.h"
#include "game.h"
#include "player.h"
#include "action.h"
#include "map.h"
#include "output.h"
#include "catacharset.h"
#include "string_formatter.h"
#include "map_selector.h"
#include "crafting.h"
#include "options.h"
#include "debug.h"
#include "skill.h"
#include "messages.h"
#include "translations.h"
#include "veh_type.h"
#include "ui.h"
#include "itype.h"
#include "cata_utility.h"
#include "vehicle_selector.h"
#include "fault.h"
#include "npc.h"
#include "string_input_popup.h"
#include "veh_utils.h"
#include <cmath>
#include <list>
#include <functional>
#include <iterator>
#include <algorithm>
#include <numeric>
#include <cassert>
#ifdef _MSC_VER
#include <math.h>
#define ISNAN _isnan
#else
#define ISNAN std::isnan
#endif
static inline const char * status_color( bool status )
{
static const char *good = "green";
static const char *bad = "red";
return status ? good : bad;
}
// cap JACK requirements to support arbitrarily large vehicles
static double jack_qality( const vehicle &veh )
{
const units::quantity<double, units::mass::unit_type> mass = std::min( veh.total_mass(), JACK_LIMIT );
return ceil( mass / TOOL_LIFT_FACTOR );
}
/** Can part currently be reloaded with anything? */
static auto can_refill = []( const vehicle_part &pt ) { return pt.can_reload(); };
namespace {
const std::string repair_hotkeys("r1234567890");
const quality_id LIFT( "LIFT" );
const quality_id JACK( "JACK" );
const skill_id skill_mechanics( "mechanics" );
} // namespace
void act_vehicle_siphon(vehicle* veh);
player_activity veh_interact::serialize_activity()
{
const auto *pt = sel_vehicle_part;
const auto *vp = sel_vpart_info;
if( sel_cmd == 'q' || sel_cmd == ' ' || !vp ) {
return player_activity();
}
int time = 1000;
switch( sel_cmd ) {
case 'i':
time = vp->install_time( g->u );
break;
case 'r':
if( pt->is_broken() ) {
time = vp->install_time( g->u );
} else {
assert( pt->base.max_damage() > 0 ); // why repairing part that cannot be damaged?
time = vp->repair_time( g->u ) * double( pt->base.damage() ) / pt->base.max_damage();
}
break;
case 'o':
time = vp->removal_time( g->u );
break;
case 'c':
time = vp->removal_time( g->u ) + vp->install_time( g->u );
break;
}
player_activity res( activity_id( "ACT_VEHICLE" ), time, (int) sel_cmd );
// if we're working on an existing part, use that part as the reference point
// otherwise (e.g. installing a new frame), just use part 0
point q = veh->coord_translate( pt ? pt->mount : veh->parts[0].mount );
res.values.push_back( veh->global_x() + q.x ); // values[0]
res.values.push_back( veh->global_y() + q.y ); // values[1]
res.values.push_back( ddx ); // values[2]
res.values.push_back( ddy ); // values[3]
res.values.push_back( -ddx ); // values[4]
res.values.push_back( -ddy ); // values[5]
res.values.push_back( veh->index_of_part( pt ) ); // values[6]
res.str_values.push_back( vp->get_id().str() );
res.targets.emplace_back( std::move( target ) );
return res;
}
player_activity veh_interact::run( vehicle &veh, int x, int y )
{
veh_interact vehint( veh, x, y );
vehint.do_main_loop();
g->refresh_all();
return vehint.serialize_activity();
}
vehicle_part &veh_interact::select_part( const vehicle &veh, const part_selector &sel, const std::string &title )
{
static vehicle_part null_part;
vehicle_part *res = &null_part;
auto act = [&]( const vehicle_part &pt ) {
res = const_cast<vehicle_part *>( &pt );
return false; // avoid redraw
};
int opts = std::count_if( veh.parts.cbegin(), veh.parts.cend(), sel );
if( opts == 1 ) {
act( *std::find_if( veh.parts.cbegin(), veh.parts.cend(), sel ) );
} else if( opts != 0 ) {
veh_interact vehint( const_cast<vehicle &>( veh ) );
vehint.set_title( title.empty() ? _( "Select part" ) : title );
vehint.overview( sel, act );
g->refresh_all();
}
return *res;
}
static const trait_id trait_DEBUG_HS( "DEBUG_HS" );
/**
* Creates a blank veh_interact window.
*/
veh_interact::veh_interact( vehicle &veh, int x, int y )
: ddx( x ), ddy( y ), veh( &veh ), main_context( "VEH_INTERACT" )
{
// Only build the shapes map and the wheel list once
for( const auto &e : vpart_info::all() ) {
const vpart_info &vp = e.second;
vpart_shapes[ vp.name() + vp.item ].push_back( &vp );
if( vp.has_flag( "WHEEL" ) ) {
wheel_types.push_back( &vp );
}
}
main_context.register_directions();
main_context.register_action("QUIT");
main_context.register_action("INSTALL");
main_context.register_action("REPAIR");
main_context.register_action("MEND");
main_context.register_action("REFILL");
main_context.register_action("REMOVE");
main_context.register_action("RENAME");
main_context.register_action("SIPHON");
main_context.register_action("TIRE_CHANGE");
main_context.register_action("ASSIGN_CREW");
main_context.register_action("RELABEL");
main_context.register_action("PREV_TAB");
main_context.register_action("NEXT_TAB");
main_context.register_action("CONFIRM");
main_context.register_action("HELP_KEYBINDINGS");
main_context.register_action("FILTER");
countDurability();
cache_tool_availability();
allocate_windows();
}
veh_interact::~veh_interact() = default;
void veh_interact::allocate_windows()
{
// grid window
const int grid_w = TERMX - 2; // exterior borders take 2
const int grid_h = TERMY - 2; // exterior borders take 2
w_grid = catacurses::newwin( grid_h, grid_w, 1, 1 );
int mode_h = 1;
int name_h = 1;
int stats_h = 6;
page_size = grid_h - ( mode_h + stats_h + name_h ) - 2;
int pane_y = 1 + mode_h + 1;
int pane_w = ( grid_w / 3 ) - 1;
int disp_w = grid_w - ( pane_w * 2 ) - 2;
int disp_h = page_size * 0.45;
int parts_h = page_size - disp_h;
int parts_y = pane_y + disp_h;
int name_y = pane_y + page_size + 1;
int stats_y = name_y + name_h;
int list_x = 1 + disp_w + 1;
int msg_x = list_x + pane_w + 1;
// make the windows
w_mode = catacurses::newwin( mode_h, grid_w, 1, 1 );
w_msg = catacurses::newwin( page_size, pane_w, pane_y, msg_x );
w_disp = catacurses::newwin( disp_h, disp_w, pane_y, 1 );
w_parts = catacurses::newwin( parts_h, disp_w, parts_y, 1 );
w_list = catacurses::newwin( page_size, pane_w, pane_y, list_x );
w_stats = catacurses::newwin( stats_h, grid_w, stats_y, 1 );
w_name = catacurses::newwin( name_h, grid_w, name_y, 1 );
display_grid();
display_name();
display_stats();
display_veh();
move_cursor(0, 0); // display w_disp & w_parts
}
void veh_interact::set_title( const std::string &msg ) const
{
werase( w_mode );
nc_color col = c_light_gray;
print_colored_text( w_mode, 0, 1, col, col, msg );
wrefresh( w_mode );
}
bool veh_interact::format_reqs( std::ostringstream& msg, const requirement_data &reqs,
const std::map<skill_id, int> &skills, int moves ) const {
const auto inv = g->u.crafting_inventory();
bool ok = reqs.can_make_with_inventory( inv );
msg << _( "<color_white>Time required:</color>\n" );
//@todo: better have a from_moves function
msg << "> " << to_string_approx( time_duration::from_turns( moves / 100 ) ) << "\n";
msg << _( "<color_white>Skills required:</color>\n" );
for( const auto& e : skills ) {
bool hasSkill = g->u.get_skill_level( e.first ) >= e.second;
if( !hasSkill ) {
ok = false;
}
//~ %1$s represents the internal color name which shouldn't be translated, %2$s is skill name, and %3$i is skill level
msg << string_format( _( "> <color_%1$s>%2$s %3$i</color>\n" ), status_color( hasSkill ),
e.first.obj().name().c_str(), e.second );
}
if( skills.empty() ) {
msg << string_format( "> <color_%1$s>%2$s</color>", status_color( true ), _( "NONE" ) ) << "\n";
}
auto comps = reqs.get_folded_components_list( getmaxx( w_msg ) - 2, c_white, inv );
std::copy( comps.begin(), comps.end(), std::ostream_iterator<std::string>( msg, "\n" ) );
auto tools = reqs.get_folded_tools_list( getmaxx( w_msg ) - 2, c_white, inv );
std::copy( tools.begin(), tools.end(), std::ostream_iterator<std::string>( msg, "\n" ) );
return ok;
}
void veh_interact::do_main_loop()
{
bool finish = false;
while( !finish ) {
overview();
display_mode();
const std::string action = main_context.handle_input();
werase( w_msg );
wrefresh( w_msg );
std::string msg;
bool redraw = false;
int dx, dy;
if (main_context.get_direction(dx, dy, action)) {
move_cursor(dx, dy);
} else if (action == "QUIT") {
finish = true;
} else if (action == "INSTALL") {
redraw = do_install( msg );
} else if (action == "REPAIR") {
redraw = do_repair( msg );
} else if (action == "MEND") {
redraw = do_mend( msg );
} else if (action == "REFILL") {
redraw = do_refill( msg );
} else if (action == "REMOVE") {
redraw = do_remove( msg );
} else if (action == "RENAME") {
redraw = do_rename( msg );
} else if (action == "SIPHON") {
redraw = do_siphon( msg );
// Siphoning may have started a player activity. If so, we should close the
// vehicle dialog and continue with the activity.
finish = !g->u.activity.is_null();
} else if (action == "TIRE_CHANGE") {
redraw = do_tirechange( msg );
} else if (action == "ASSIGN_CREW") {
redraw = do_assign_crew( msg );
} else if (action == "RELABEL") {
redraw = do_relabel( msg );
} else if (action == "NEXT_TAB") {
move_fuel_cursor(1);
} else if (action == "PREV_TAB") {
move_fuel_cursor(-1);
}
if (sel_cmd != ' ') {
finish = true;
}
if( !finish && redraw ) {
display_grid();
display_name();
display_stats();
display_veh();
// Horrible hack warning:
// Part display doesn't have a dedicated display function
// Siphon menu obscures it, so it has to be redrawn
move_cursor( 0, 0 );
}
if( !msg.empty() ) {
werase( w_msg );
fold_and_print( w_msg, 0, 1, getmaxx( w_msg ) - 2, c_light_red, msg );
wrefresh( w_msg );
}
}
}
void veh_interact::cache_tool_availability()
{
crafting_inv = g->u.crafting_inventory();
has_wrench = crafting_inv.has_quality( quality_id( "WRENCH" ) );
has_wheel = crafting_inv.has_components( "wheel", 1 ) ||
crafting_inv.has_components( "wheel_wide", 1 ) ||
crafting_inv.has_components( "wheel_armor", 1 ) ||
crafting_inv.has_components( "wheel_bicycle", 1 ) ||
crafting_inv.has_components( "wheel_motorbike", 1 ) ||
crafting_inv.has_components( "wheel_small", 1 );
max_lift = std::max( { g->u.max_quality( LIFT ),
map_selector( g->u.pos(), PICKUP_RANGE ).max_quality( LIFT ),
vehicle_selector(g->u.pos(), 2, true, *veh ).max_quality( LIFT ) } );
max_jack = std::max( { g->u.max_quality( JACK ),
map_selector( g->u.pos(), PICKUP_RANGE ).max_quality( JACK ),
vehicle_selector(g->u.pos(), 2, true, *veh ).max_quality( JACK ) } );
const double qual = jack_qality( *veh );
has_jack = g->u.has_quality( JACK, qual ) ||
map_selector( g->u.pos(), PICKUP_RANGE ).has_quality( JACK, qual ) ||
vehicle_selector( g->u.pos(), 2, true, *veh ).has_quality( JACK, qual );
}
/**
* Checks if the player is able to perform some command, and returns a nonzero
* error code if they are unable to perform it. The return from this function
* should be passed into the various do_whatever functions further down.
* @param mode The command the player is trying to perform (i.e. 'r' for repair).
* @return CAN_DO if the player has everything they need,
* INVALID_TARGET if the command can't target that square,
* LACK_TOOLS if the player lacks tools,
* NOT_FREE if something else obstructs the action,
* LACK_SKILL if the player's skill isn't high enough,
* LOW_MORALE if the player's morale is too low while trying to perform
* an action requiring a minimum morale,
* UNKNOWN_TASK if the requested operation is unrecognized.
*/
task_reason veh_interact::cant_do (char mode)
{
bool enough_morale = true;
bool valid_target = false;
bool has_tools = false;
bool part_free = true;
bool has_skill = true;
switch (mode) {
case 'i': // install mode
enough_morale = g->u.has_morale_to_craft();
valid_target = !can_mount.empty() && 0 == veh->tags.count("convertible");
//tool checks processed later
has_tools = true;
break;
case 'r': // repair mode
enough_morale = g->u.has_morale_to_craft();
valid_target = !need_repair.empty() && cpart >= 0;
has_tools = true; // checked later
break;
case 'm': // mend mode
enough_morale = g->u.has_morale_to_craft();
valid_target = std::any_of( veh->parts.begin(), veh->parts.end(), []( const vehicle_part &pt ) {
return !pt.faults().empty();
} );
has_tools = true; // checked later
break;
case 'f':
return std::any_of( veh->parts.begin(), veh->parts.end(), can_refill ) ? CAN_DO : INVALID_TARGET;
case 'o': // remove mode
enough_morale = g->u.has_morale_to_craft();
valid_target = cpart >= 0 && 0 == veh->tags.count("convertible");
part_free = parts_here.size() > 1 || (cpart >= 0 && veh->can_unmount(cpart));
//tool and skill checks processed later
has_tools = true;
has_skill = true;
break;
case 's': // siphon mode
valid_target = false;
for( auto & e : veh->fuels_left() ) {
if( item::find_type( e.first )->phase == LIQUID ) {
valid_target = true;
break;
}
}
has_tools = crafting_inv.has_tools( "hose", 1 );
break;
case 'c': // change tire
valid_target = wheel != NULL;
///\EFFECT_STR allows changing tires on heavier vehicles without a jack
has_tools = has_wrench && has_wheel && ( g->u.can_lift( *veh ) || has_jack );
break;
case 'w': // assign crew
if( g->allies().empty() ) {
return INVALID_TARGET;
}
return std::any_of( veh->parts.begin(), veh->parts.end(), []( const vehicle_part &e ) {
return e.is_seat();
} ) ? CAN_DO : INVALID_TARGET;
case 'a': // relabel
valid_target = cpart >= 0;
has_tools = true;
break;
default:
return UNKNOWN_TASK;
}
if( abs( veh->velocity ) > 100 || g->u.controlling_vehicle ) {
return MOVING_VEHICLE;
}
if( !enough_morale ) {
return LOW_MORALE;
}
if( !valid_target ) {
return INVALID_TARGET;
}
if( !has_tools ) {
return LACK_TOOLS;
}
if( !part_free ) {
return NOT_FREE;
}
if( !has_skill ) {
return LACK_SKILL;
}
return CAN_DO;
}
bool veh_interact::is_drive_conflict() {
bool install_muscle_engine = (sel_vpart_info->fuel_type == "muscle");
bool has_muscle_engine = veh->has_engine_type("muscle", false);
bool can_install = !(has_muscle_engine && install_muscle_engine);
if (!can_install) {
werase (w_msg);
fold_and_print(w_msg, 0, 1, getmaxx( w_msg ) - 2, c_light_red,
_("Only one muscle powered engine can be installed."));
wrefresh (w_msg);
}
return !can_install;
}
bool veh_interact::can_install_part() {
if( sel_vpart_info == NULL ) {
werase (w_msg);
wrefresh (w_msg);
return false;
}
if( is_drive_conflict() ) {
return false;
}
if( sel_vpart_info->has_flag( "FUNNEL" ) ) {
if( std::none_of( parts_here.begin(), parts_here.end(), [&]( const int e ) {
return veh->parts[e].is_tank();
} ) ) {
werase( w_msg );
fold_and_print( w_msg, 0, 1, getmaxx( w_msg ) - 2, c_light_red,
_( "Funnels need to be installed over a tank." ) );
wrefresh( w_msg );
return false;
}
}
bool is_engine = sel_vpart_info->has_flag("ENGINE");
bool install_muscle_engine = (sel_vpart_info->fuel_type == "muscle");
//count current engines, muscle engines don't require higher skill
int engines = 0;
int dif_eng = 0;
if (is_engine && !install_muscle_engine) {
for (size_t p = 0; p < veh->parts.size(); p++) {
if (veh->part_flag (p, "ENGINE") &&
veh->part_info(p).fuel_type != "muscle")
{
engines++;
dif_eng = dif_eng / 2 + 8;
}
}
}
int dif_steering = 0;
if (sel_vpart_info->has_flag("STEERABLE")) {
std::set<int> axles;
for (auto &p : veh->steering) {
if (!veh->part_flag(p, "TRACKED")) {
// tracked parts don't contribute to axle complexity
axles.insert(veh->parts[p].mount.x);
}
}
if (axles.size() > 0 && axles.count(-ddx) == 0) {
// Installing more than one steerable axle is hard
// (but adding a wheel to an existing axle isn't)
dif_steering = axles.size() + 5;
}
}
const auto reqs = sel_vpart_info->install_requirements();
std::ostringstream msg;
bool ok = format_reqs( msg, reqs, sel_vpart_info->install_skills, sel_vpart_info->install_time( g->u ) );
msg << _( "<color_white>Additional requirements:</color>\n" );
if( dif_eng > 0 ) {
if( g->u.get_skill_level( skill_mechanics ) < dif_eng ) {
ok = false;
}
msg << string_format( _( "> <color_%1$s>%2$s %3$i</color> for extra engines." ),
status_color( g->u.get_skill_level( skill_mechanics ) >= dif_eng ),
skill_mechanics.obj().name().c_str(), dif_eng ) << "\n";
}
if( dif_steering > 0 ) {
if( g->u.get_skill_level( skill_mechanics ) < dif_steering ) {
ok = false;
}
msg << string_format( _( "> <color_%1$s>%2$s %3$i</color> for extra steering axles." ),
status_color( g->u.get_skill_level( skill_mechanics ) >= dif_steering ),
skill_mechanics.obj().name().c_str(), dif_steering ) << "\n";
}
int lvl, str;
quality_id qual;
bool use_aid, use_str;
item base( sel_vpart_info->item );
if( base.is_wheel() ) {
qual = JACK;
lvl = jack_qality( *veh );
str = veh->lift_strength();
use_aid = max_jack >= lvl;
use_str = g->u.can_lift( *veh );
} else {
qual = LIFT;
lvl = std::ceil( units::quantity<double, units::mass::unit_type>( base.weight() ) / TOOL_LIFT_FACTOR );
str = base.lift_strength();
use_aid = max_lift >= lvl;
use_str = g->u.can_lift( base );
}
if( !( use_aid || use_str ) ) {
ok = false;
}
msg << string_format( _( "> <color_%1$s>1 tool with %2$s %3$i</color> <color_white>OR</color> <color_%4$s>strength %5$i</color>" ),
status_color( use_aid ), qual.obj().name.c_str(), lvl,
status_color( use_str ), str ) << "\n";
werase( w_msg );
fold_and_print( w_msg, 0, 1, getmaxx( w_msg ) - 2, c_light_gray, msg.str() );
wrefresh( w_msg );
return ok || g->u.has_trait( trait_DEBUG_HS );
}
/**
* Moves list of fuels up or down.
* @param delta -1 if moving up,
* 1 if moving down
*/
void veh_interact::move_fuel_cursor(int delta)
{
int max_fuel_indicators = (int)veh->get_printable_fuel_types().size();
int height = 5;
fuel_index += delta;
if(fuel_index < 0) {
fuel_index = 0;
} else if(fuel_index > max_fuel_indicators - height) {
fuel_index = std::max(max_fuel_indicators - height, 0);
}
display_stats();
}
bool veh_interact::do_install( std::string &msg )
{
switch( cant_do( 'i' ) ) {
case LOW_MORALE:
msg = _( "Your morale is too low to construct..." );
return false;
case INVALID_TARGET:
msg = _( "Cannot install any part here." );
return false;
case MOVING_VEHICLE:
msg = _( "You can't install parts while driving." );
return false;
default: break;
}
set_title( _( "Choose new part to install here:" ) );
std::array<std::string,8> tab_list = { { pgettext("Vehicle Parts|","All"),
pgettext("Vehicle Parts|","Cargo"),
pgettext("Vehicle Parts|","Light"),
pgettext("Vehicle Parts|","Util"),
pgettext("Vehicle Parts|","Hull"),
pgettext("Vehicle Parts|","Internal"),
pgettext("Vehicle Parts|","Other"),
pgettext("Vehicle Parts|","Filter")} };
std::array<std::string,8> tab_list_short = { { pgettext("Vehicle Parts|","A"),
pgettext("Vehicle Parts|","C"),
pgettext("Vehicle Parts|","L"),
pgettext("Vehicle Parts|","U"),
pgettext("Vehicle Parts|","H"),
pgettext("Vehicle Parts|","I"),
pgettext("Vehicle Parts|","O"),
pgettext("Vehicle Parts|","F")} };
std::array <std::function<bool(const vpart_info*)>,8> tab_filters; // filter for each tab, last one
tab_filters[0] = [&](const vpart_info *) { return true; }; // All
tab_filters[1] = [&](const vpart_info *p) { auto &part = *p;
return part.has_flag(VPFLAG_CARGO) && // Cargo
!part.has_flag("TURRET"); };
tab_filters[2] = [&](const vpart_info *p) { auto &part = *p;
return part.has_flag(VPFLAG_LIGHT) || // Light
part.has_flag(VPFLAG_CONE_LIGHT) ||
part.has_flag(VPFLAG_CIRCLE_LIGHT) ||
part.has_flag(VPFLAG_DOME_LIGHT) ||
part.has_flag(VPFLAG_AISLE_LIGHT) ||
part.has_flag(VPFLAG_ATOMIC_LIGHT); };
tab_filters[3] = [&](const vpart_info *p) { auto &part = *p;
return part.has_flag("TRACK") || //Util
part.has_flag(VPFLAG_FRIDGE) ||
part.has_flag("KITCHEN") ||
part.has_flag("WELDRIG") ||
part.has_flag("CRAFTRIG") ||
part.has_flag("CHEMLAB") ||
part.has_flag("FORGE") ||
part.has_flag("HORN") ||
part.has_flag("BEEPER") ||
part.has_flag("WATCH") ||
part.has_flag("ALARMCLOCK") ||
part.has_flag(VPFLAG_RECHARGE) ||
part.has_flag("VISION") ||
part.has_flag("POWER_TRANSFER") ||
part.has_flag("FAUCET") ||
part.has_flag("STEREO") ||
part.has_flag("CHIMES") ||
part.has_flag("MUFFLER") ||
part.has_flag("REMOTE_CONTROLS") ||
part.has_flag("CURTAIN") ||
part.has_flag("SEATBELT") ||
part.has_flag("SECURITY") ||
part.has_flag("SEAT") ||
part.has_flag("BED") ||
part.has_flag("DOOR_MOTOR") ||
part.has_flag("WATER_PURIFIER"); };
tab_filters[4] = [&](const vpart_info *p) { auto &part = *p;
return(part.has_flag(VPFLAG_OBSTACLE) || // Hull
part.has_flag("ROOF") ||
part.has_flag(VPFLAG_ARMOR)) &&
!part.has_flag("WHEEL") &&
!tab_filters[3](p); };
tab_filters[5] = [&](const vpart_info *p) { auto &part = *p;
return part.has_flag(VPFLAG_ENGINE) || // Internals
part.has_flag(VPFLAG_ALTERNATOR) ||
part.has_flag(VPFLAG_CONTROLS) ||
part.location == "fuel_source" ||
part.location == "on_battery_mount" ||
(part.location.empty() && part.has_flag("FUEL_TANK")); };
// Other: everything that's not in the other filters
tab_filters[tab_filters.size()-2] = [&](const vpart_info *part) {
for (size_t i=1; i < tab_filters.size()-2; i++ ) {
if( tab_filters[i](part) ) return false;
}
return true; };
std::string filter; // The user specified filter
tab_filters[7] = [&](const vpart_info *p){ return lcmatch( p->name(), filter ); };
// full list of mountable parts, to be filtered according to tab
std::vector<const vpart_info*> tab_vparts = can_mount;
int pos = 0;
size_t tab = 0;
while (true) {
display_list(pos, tab_vparts, 2);
// draw tab menu
int tab_x = 0;
for( size_t i=0; i < tab_list.size(); i++ ){
std::string tab_name = (tab == i) ? tab_list[i] : tab_list_short[i]; // full name for selected tab
tab_x += (tab == i); // add a space before selected tab
draw_subtab(w_list, tab_x, tab_name, tab == i, false);
tab_x += ( 1 + utf8_width(tab_name) + (tab == i) ); // one space padding and add a space after selected tab
}
wrefresh(w_list);
sel_vpart_info = tab_vparts.empty() ? nullptr : tab_vparts[pos]; // filtered list can be empty
display_details( sel_vpart_info );
bool can_install = can_install_part();
const std::string action = main_context.handle_input();
if ( action == "FILTER" ){
string_input_popup()
.title( _( "Search for part" ) )
.width( 50 )
.description( _( "Filter" ) )
.max_length( 100 )
.edit( filter );
tab = 7; // Move to the user filter tab.
display_grid();
display_stats();
display_veh(); // Fix the (currently) mangled windows
move_cursor(0,0); // Wake up the vehicle display
}
if (action == "REPAIR" ){
filter.clear();
tab = 0;
}
if (action == "INSTALL" || action == "CONFIRM"){
if (can_install) {
const auto &shapes = vpart_shapes[ sel_vpart_info->name() + sel_vpart_info->item ];
int selected_shape = -1;
if ( shapes.size() > 1 ) { // more than one shape available, display selection
std::vector<uimenu_entry> shape_ui_entries;
for ( size_t i = 0; i < shapes.size(); i++ ) {
uimenu_entry entry = uimenu_entry( i, true, UIMENU_INVALID,
shapes[i]->name() );
entry.extratxt.left = 1;
entry.extratxt.sym = special_symbol( shapes[i]->sym );
entry.extratxt.color = shapes[i]->color;
shape_ui_entries.push_back( entry );
}
selected_shape = uimenu( true, getbegx( w_list ), getmaxx( w_list ), getbegy( w_list ),
_("Choose shape:"), shape_ui_entries ).ret;
} else { // only one shape available, default to first one
selected_shape = 0;
}
if( 0 <= selected_shape && (size_t) selected_shape < shapes.size() ) {
sel_vpart_info = shapes[selected_shape];
sel_cmd = 'i';
return true; // force redraw
}
}
} else if (action == "QUIT") {
sel_vpart_info = NULL;
werase (w_list);
wrefresh (w_list);
werase (w_msg);
wrefresh(w_msg);
break;
} else if (action == "PREV_TAB" || action == "NEXT_TAB"|| action == "FILTER" || action == "REPAIR" ) {
tab_vparts.clear();
pos = 0;
if(action == "PREV_TAB") {
tab = ( tab < 1 ) ? tab_list.size() - 1 : tab - 1;
} else if (action == "NEXT_TAB") {
tab = ( tab < tab_list.size() - 1 ) ? tab + 1 : 0;
}
copy_if(can_mount.begin(), can_mount.end(), back_inserter(tab_vparts), tab_filters[tab]);
}
else {
move_in_list(pos, action, tab_vparts.size(), 2);
}
}
//destroy w_details
werase(w_details);
w_details = catacurses::window();
//restore windows that had been covered by w_details
display_stats();
display_name();
return false;
}
bool veh_interact::move_in_list(int &pos, const std::string &action, const int size, const int header) const
{
int lines_per_page = page_size - header;
if (action == "PREV_TAB" || action == "LEFT") {
pos -= lines_per_page;
} else if (action == "NEXT_TAB" || action == "RIGHT") {
pos += lines_per_page;
} else if (action == "UP") {
pos--;
} else if (action == "DOWN") {
pos++;
} else {
// Anything else -> no movement
return false;
}
if (pos < 0) {
pos = size - 1;
} else if (pos >= size) {
pos = 0;
}
return true;
}
bool veh_interact::do_repair( std::string &msg )
{
switch( cant_do( 'r' ) ) {
case LOW_MORALE:
msg = _( "Your morale is too low to repair..." );
return false;
case INVALID_TARGET:
{
vehicle_part *most_repairable = get_most_repariable_part();
if( most_repairable ) {
move_cursor( most_repairable->mount.y + ddy, -( most_repairable->mount.x + ddx ) );
return false;
} else {
msg = _( "There are no damaged parts on this vehicle." );
return false;
}
}
case MOVING_VEHICLE:
msg = _( "You can't repair stuff while driving." );
return false;
default: break;
}
set_title( _( "Choose a part here to repair:" ) );
int pos = 0;
while (true) {
vehicle_part &pt = veh->parts[parts_here[need_repair[pos]]];
const vpart_info &vp = pt.info();
std::ostringstream msg;
bool ok;
if( pt.is_broken() ) {
ok = format_reqs( msg, vp.install_requirements(), vp.install_skills, vp.install_time( g->u ) );
} else {
if( !vp.repair_requirements().is_empty() ) {
ok = format_reqs( msg, vp.repair_requirements() * pt.base.damage(), vp.repair_skills,
vp.repair_time( g->u ) * double( pt.base.damage() ) / pt.base.max_damage() );
} else {
msg << "<color_light_red>" << _( "This part cannot be repaired" ) << "</color>";
ok = false;
}
}
werase (w_msg);
fold_and_print( w_msg, 0, 1, getmaxx( w_msg ) - 2, c_light_gray, msg.str() );
wrefresh (w_msg);
werase (w_parts);
veh->print_part_desc(w_parts, 0, getmaxy( w_parts ) - 1, getmaxx( w_parts ), cpart, need_repair[pos]);
wrefresh (w_parts);
const std::string action = main_context.handle_input();
if( ( action == "REPAIR" || action == "CONFIRM" ) && ok ) {
sel_vehicle_part = &pt;
sel_vpart_info = &vp;
sel_cmd = 'r';
break;
} else if (action == "QUIT") {
werase (w_parts);
veh->print_part_desc (w_parts, 0, getmaxy( w_parts ) - 1, getmaxx( w_parts ), cpart, -1);
wrefresh (w_parts);
werase (w_msg);
wrefresh(w_msg);
break;
} else {
move_in_list(pos, action, need_repair.size());
}
}
return false;
}
bool veh_interact::do_mend( std::string &msg )
{
switch( cant_do( 'm' ) ) {
case LOW_MORALE:
msg = _( "Your morale is too low to mend..." );
return false;
case INVALID_TARGET:
msg = _( "No faulty parts require mending." );
return false;
case MOVING_VEHICLE:
msg = _( "You can't mend stuff while driving." );
return false;
default: break;
}
set_title( _( "Choose a part here to mend:" ) );
auto sel = []( const vehicle_part &pt ) { return !pt.faults().empty(); };
auto act = [&]( const vehicle_part &pt ) {
g->u.mend_item( veh->part_base( veh->index_of_part( &pt ) ) );
sel_cmd = 'q';
return true; // force redraw
};
return overview( sel, act );
}
bool veh_interact::do_refill( std::string &msg )
{
if( cant_do( 'f' ) ) {
msg = _( "No parts can currently be refilled" );
return false;
}
set_title( _( "Select part to refill:" ) );
auto act = [&]( const vehicle_part &pt ) {
auto validate = [&]( const item &obj ) {
if( pt.is_tank() ) {
// cannot refill using active liquids (those that rot) due to #18570
if( obj.is_watertight_container() && !obj.contents.empty() && !obj.contents.front().active ) {
return pt.can_reload( obj.contents.front().typeId() );
}
} else if( pt.is_reactor() ) {
return pt.can_reload( obj.typeId() );
}
return false;
};
target = g->inv_map_splice( validate, string_format( _( "Refill %s" ), pt.name().c_str() ), 1 );
if( target ) {
sel_vehicle_part = &pt;
sel_vpart_info = &pt.info();
sel_cmd = 'f';
}
return true; // force redraw
};
return overview( can_refill, act );
}
bool veh_interact::overview( std::function<bool(const vehicle_part &pt)> enable,
std::function<bool(vehicle_part &pt)> action )
{
struct part_option {
part_option( const std::string &key, vehicle_part *part, char hotkey,
std::function<void( const vehicle_part &pt, const catacurses::window &w, int y )> details ) :
key( key ), part( part ), hotkey( hotkey ), details( details ) {}
part_option( const std::string &key, vehicle_part *part, char hotkey,
std::function<void( const vehicle_part &pt, const catacurses::window &w, int y )> details,
std::function<void( const vehicle_part &pt )> message ) :
key( key ), part( part ), hotkey( hotkey ), details( details ), message( message ) {}
std::string key;
vehicle_part *part;
/** Can @param action be run for this entry? */
char hotkey;
/** Writes any extra details for this entry */
std::function<void( const vehicle_part &pt, const catacurses::window &w, int y )> details;
/** Writes to message window when part is selected */
std::function<void( const vehicle_part &pt )> message;
};
std::vector<part_option> opts;
std::map<std::string, std::function<void( const catacurses::window &, int )>> headers;
headers["ENGINE"] = []( const catacurses::window &w, int y ) {
trim_and_print( w, y, 1, getmaxx( w ) - 2, c_light_gray, _( "Engines" ) );
right_print ( w, y, 1, c_light_gray, _( "Fuel Use" ) );
};
headers["TANK"] = []( const catacurses::window &w, int y ) {
trim_and_print( w, y, 1, getmaxx( w ) - 2, c_light_gray, _( "Tanks" ) );
right_print ( w, y, 1, c_light_gray, _( "Contents Qty" ) );
};
headers["BATTERY"] = []( const catacurses::window &w, int y ) {
trim_and_print( w, y, 1, getmaxx( w ) - 2, c_light_gray, _( "Batteries" ) );
right_print ( w, y, 1, c_light_gray, _( "Capacity Status" ) );
};
headers["REACTOR"] = []( const catacurses::window &w, int y ) {
trim_and_print( w, y, 1, getmaxx( w ) - 2, c_light_gray, _( "Reactors" ) );
right_print ( w, y, 1, c_light_gray, _( "Contents Qty" ) );
};
headers["TURRET"] = []( const catacurses::window &w, int y ) {
trim_and_print( w, y, 1, getmaxx( w ) - 2, c_light_gray, _( "Turrets" ) );
right_print ( w, y, 1, c_light_gray, _( "Ammo Qty" ) );
};
headers["SEAT"] = []( const catacurses::window &w, int y ) {
trim_and_print( w, y, 1, getmaxx( w ) - 2, c_light_gray, _( "Seats" ) );
right_print ( w, y, 1, c_light_gray, _( "Who" ) );
};
char hotkey = 'a';
for( auto &pt : veh->parts ) {
if( pt.is_engine() && !pt.is_broken() ) {
// if tank contains something then display the contents in milliliters
auto details = []( const vehicle_part &pt, const catacurses::window &w, int y ) {
right_print( w, y, 1, item::find_type( pt.ammo_current() )->color,
string_format( "%s <color_light_gray>%3s</color>",
pt.ammo_current() != "null" ? item::nname( pt.ammo_current() ).c_str() : "",
pt.enabled ? _( "Yes" ) : _( "No" ) ) );
};
// display engine faults (if any)
auto msg = [&]( const vehicle_part &pt ) {
werase( w_msg );
int y = 0;
for( const auto& e : pt.faults() ) {
y += fold_and_print( w_msg, y, 1, getmaxx( w_msg ) - 2, c_red,
_( "Faulty %1$s" ), e.obj().name().c_str() );
y += fold_and_print( w_msg, y, 3, getmaxx( w_msg ) - 4, c_light_gray, e.obj().description() );
y++;
}
wrefresh( w_msg );
};
opts.emplace_back( "ENGINE", &pt, action && enable && enable( pt ) ? hotkey++ : '\0', details, msg );
}
}
for( auto &pt : veh->parts ) {
if( pt.is_tank() && !pt.is_broken() ) {
auto details = []( const vehicle_part &pt, const catacurses::window &w, int y ) {
if( pt.ammo_current() != "null" ) {
auto stack = units::legacy_volume_factor / item::find_type( pt.ammo_current() )->stack_size;
right_print( w, y, 1, item::find_type( pt.ammo_current() )->color,
string_format( "%s %5.1fL", item::nname( pt.ammo_current() ),
round_up( to_liter( pt.ammo_remaining() * stack ), 1 ) ) );
}
};
opts.emplace_back( "TANK", &pt, action && enable && enable( pt ) ? hotkey++ : '\0', details );
}
}
for( auto &pt : veh->parts ) {
if( pt.is_battery() && !pt.is_broken() ) {
// always display total battery capacity and percentage charge
auto details = []( const vehicle_part &pt, const catacurses::window &w, int y ) {
int pct = ( double( pt.ammo_remaining() ) / pt.ammo_capacity() ) * 100;
right_print( w, y, 1, item::find_type( pt.ammo_current() )->color,
string_format( "%i %3i%%", pt.ammo_capacity(), pct ) );
};
opts.emplace_back( "BATTERY", &pt, action && enable && enable( pt ) ? hotkey++ : '\0', details );
}
}
auto details_ammo = []( const vehicle_part &pt, const catacurses::window &w, int y ) {
if( pt.ammo_remaining() ) {
right_print( w, y, 1, item::find_type( pt.ammo_current() )->color,
string_format( "%s %5i", item::nname( pt.ammo_current() ).c_str(), pt.ammo_remaining() ) );
}
};
for( auto &pt : veh->parts ) {
if( pt.is_reactor() && !pt.is_broken() ) {
opts.emplace_back( "REACTOR", &pt, action && enable && enable( pt ) ? hotkey++ : '\0', details_ammo );
}
}
for( auto &pt : veh->parts ) {
if( pt.is_turret() && !pt.is_broken() ) {
opts.emplace_back( "TURRET", &pt, action && enable && enable( pt ) ? hotkey++ : '\0', details_ammo );
}
}
for( auto &pt : veh->parts ) {
auto details = []( const vehicle_part &pt, const catacurses::window &w, int y ) {
const npc *who = pt.crew();
if( who ) {
right_print( w, y, 1, pt.passenger_id == who->getID() ? c_green : c_light_gray, who->name );
}
};
if( pt.is_seat() && !pt.is_broken() ) {
opts.emplace_back( "SEAT", &pt, action && enable && enable( pt ) ? hotkey++ : '\0', details );
}
}
int pos = -1;
if( enable && action ) {
do {
if( ++pos >= int( opts.size() ) ) {
pos = -1;
break; // nothing could be selected
}
} while( !opts[pos].hotkey );
}
bool redraw = false;
while( true ) {
werase( w_list );
std::string last;
int y = 0;
for( int idx = 0; idx != int( opts.size() ); ++idx ) {
const auto &pt = *opts[idx].part;
// if this is a new section print a header row
if( last != opts[idx].key ) {
y += last.empty() ? 0 : 1;
headers[opts[idx].key]( w_list, y );
y += 2;
last = opts[idx].key;
}
bool highlighted = false;
// No action means no selecting, just highlight relevant ones
if( pos < 0 && enable && !action ) {
highlighted = enable( pt );
} else if( pos == idx ) {
highlighted = true;
}
// print part name
nc_color col = opts[idx].hotkey ? c_white : c_dark_gray;
trim_and_print( w_list, y, 1, getmaxx( w_list ) - 1,
highlighted ? hilite( col ) : col,
"<color_dark_gray>%c </color>%s",
opts[idx].hotkey ? opts[idx].hotkey : ' ', pt.name().c_str() );
// print extra columns (if any)
opts[idx].details( pt, w_list, y );
y++;
}
wrefresh( w_list );
if( !std::any_of( opts.begin(), opts.end(), []( const part_option &e ) { return e.hotkey; } ) ) {
return false; // nothing is selectable
}
move_cursor( opts[pos].part->mount.y + ddy, -( opts[pos].part->mount.x + ddx ) );
if( opts[pos].message ) {
opts[pos].message( *opts[pos].part );
}
const std::string input = main_context.handle_input();
if( input == "CONFIRM" && opts[pos].hotkey ) {
redraw = action( *opts[pos].part );
break;
} else if( input == "QUIT" ) {
break;
} else if( input == "UP" ) {
do {
if( --pos < 0 ) {
pos = opts.size() - 1;
}
} while( !opts[pos].hotkey );
} else if( input == "DOWN" ) {
do {
if( ++pos >= int( opts.size() ) ) {
pos = 0;
}
} while( !opts[pos].hotkey );
} else {
// did we try and activate a hotkey option?
char hotkey = main_context.get_raw_input().get_first_input();
if( hotkey ) {
auto iter = std::find_if( opts.begin(), opts.end(), [&hotkey]( const part_option &e ) {
return e.hotkey == hotkey;
} );
if( iter != opts.end() ) {
action( *iter->part );
break;
}
}
}
}
werase( w_list );
wrefresh( w_list );
return redraw;
}
vehicle_part *veh_interact::get_most_damaged_part() const
{
auto part_damage_comparison = []( const vehicle_part &a, const vehicle_part &b )
{
return !b.removed && b.base.damage() > a.base.damage();
};
auto high_damage_iterator = std::max_element( veh->parts.begin(),
veh->parts.end(),
part_damage_comparison );
if( high_damage_iterator == veh->parts.end() ||
high_damage_iterator->removed ) {
return nullptr;
}
return &( *high_damage_iterator );
}
vehicle_part *veh_interact::get_most_repariable_part() const
{
auto &part = veh_utils::most_repairable_part( *veh, g->u );
return part ? &part : nullptr;
}
bool veh_interact::can_remove_part( int idx ) {
sel_vehicle_part = &veh->parts[idx];
sel_vpart_info = &sel_vehicle_part->info();
const auto reqs = sel_vpart_info->removal_requirements();
std::ostringstream msg;
bool ok = format_reqs( msg, reqs, sel_vpart_info->removal_skills, sel_vpart_info->removal_time( g->u ) );
msg << _( "<color_white>Additional requirements:</color>\n" );
int lvl, str;
quality_id qual;
bool use_aid, use_str;
item base( sel_vpart_info->item );
if( base.is_wheel() ) {
qual = JACK;
lvl = jack_qality( *veh );
str = veh->lift_strength();
use_aid = max_jack >= lvl;
use_str = g->u.can_lift( *veh );
} else {
qual = LIFT;
lvl = ceil( units::quantity<double, units::mass::unit_type>( base.weight() ) / TOOL_LIFT_FACTOR );
str = base.lift_strength();
use_aid = max_lift >= lvl;
use_str = g->u.can_lift( base );
}
if( !( use_aid || use_str ) ) {
ok = false;
}
msg << string_format( _( "> <color_%1$s>1 tool with %2$s %3$i</color> <color_white>OR</color> <color_%4$s>strength %5$i</color>" ),
status_color( use_aid ), qual.obj().name.c_str(), lvl,
status_color( use_str ), str ) << "\n";
if( !veh->can_unmount( idx ) ) {
msg << string_format( _( "> <color_%1$s>%2$s</color>" ), status_color( false ), _( "Remove attached parts first" ) ) << "\n";
ok = false;
}
werase( w_msg );
fold_and_print( w_msg, 0, 1, getmaxx( w_msg ) - 2, c_light_gray, msg.str() );
wrefresh( w_msg );
return ok || g->u.has_trait( trait_DEBUG_HS );
}
bool veh_interact::do_remove( std::string &msg )
{
switch( cant_do( 'o' ) ) {
case LOW_MORALE:
msg = _( "Your morale is too low to construct..." );
return false;
case INVALID_TARGET:
msg = _( "No parts here." );
return false;
case NOT_FREE:
msg = _( "You cannot remove that part while something is attached to it." );
return false;
case MOVING_VEHICLE:
msg = _( "Better not remove something while driving." );
return false;
default: break;
}
set_title( _( "Choose a part here to remove:" ) );
int pos = 0;
for( size_t i = 0; i < parts_here.size(); i++ ) {
if( can_remove_part( parts_here[ i ] ) ) {
pos = i;
break;
}
}
while (true) {
//redraw list of parts
werase (w_parts);
veh->print_part_desc (w_parts, 0, getmaxy( w_parts ) - 1, getmaxx( w_parts ), cpart, pos);
wrefresh (w_parts);
int part = parts_here[ pos ];
bool can_remove = can_remove_part( part );
auto sel = [&]( const vehicle_part &pt ) { return &pt == &veh->parts[part]; };
overview( sel );
//read input
const std::string action = main_context.handle_input();
if (can_remove && (action == "REMOVE" || action == "CONFIRM")) {
sel_cmd = 'o';
break;
} else if (action == "QUIT") {
werase (w_parts);
veh->print_part_desc (w_parts, 0, getmaxy( w_parts ) - 1, getmaxx( w_parts ), cpart, -1);
wrefresh (w_parts);
werase (w_msg);
wrefresh(w_msg);
break;
} else {
move_in_list(pos, action, parts_here.size());
}
}
return false;
}
bool veh_interact::do_siphon( std::string &msg )
{
switch( cant_do( 's' ) ) {
case INVALID_TARGET:
msg = _( "The vehicle has no liquid fuel left to siphon." );
return false;
case LACK_TOOLS:
msg = _( "You need a <color_red>hose</color> to siphon liquid fuel." );
return false;
case MOVING_VEHICLE:
msg = _( "You can't siphon from a moving vehicle." );
return false;
default: break;
}
act_vehicle_siphon( veh );
return true; // force redraw
}
bool veh_interact::do_tirechange( std::string &msg )
{
switch( cant_do( 'c' ) ) {
case INVALID_TARGET:
msg = _( "There is no wheel to change here." );
return false;
case LACK_TOOLS:
msg = string_format( _( "To change a wheel you need a <color_%1$s>wrench</color>, a <color_%2$s>wheel</color>, and either "
"<color_%3$s>lifting equipment</color> or <color_%4$s>%5$d</color> strength." ),
status_color( has_wrench ), status_color( has_wheel ), status_color( has_jack ),
status_color( g->u.can_lift( *veh ) ), veh->lift_strength() );
return false;
case MOVING_VEHICLE:
msg = _( "Who is driving while you work?" );
return false;
default: break;
}
set_title( _( "Choose wheel to use as replacement:" ) );
int pos = 0;
while (true) {
sel_vpart_info = wheel_types[pos];
bool is_wheel = sel_vpart_info->has_flag("WHEEL");
display_list (pos, wheel_types);
bool has_comps = crafting_inv.has_components( sel_vpart_info->item, 1 );
werase (w_msg);
wrefresh (w_msg);
const std::string action = main_context.handle_input();
if( ( action == "TIRE_CHANGE" || action == "CONFIRM" ) &&
is_wheel && has_comps && has_wrench && ( g->u.can_lift( *veh ) || has_jack ) ) {
sel_cmd = 'c';
break;
} else if (action == "QUIT") {
werase (w_list);
wrefresh (w_list);
werase (w_msg);
break;
} else {
move_in_list(pos, action, wheel_types.size());
}
}
return false;
}
bool veh_interact::do_assign_crew( std::string &msg )
{
if( cant_do( 'w' ) != CAN_DO ) {
msg = _( "Need at least one seat and an ally to assign crew members." );
return false;
}
set_title( _( "Assign crew positions:" ) );
auto sel = []( const vehicle_part &pt ) { return pt.is_seat(); };
auto act = [&]( vehicle_part &pt ) {
uimenu menu;
menu.text = _( "Select crew member" );
menu.return_invalid = true;
if( pt.crew() ) {
menu.addentry( 0, true, 'c', _( "Clear assignment" ) );
}
for( const npc *e : g->allies() ) {
menu.addentry( e->getID(), true, -1, e->name );
}
menu.query();
if( menu.ret == 0 ) {
pt.unset_crew();
} else if( menu > 0 ) {
const auto &who = *g->critter_by_id<npc>( menu.ret );
veh->assign_seat( pt, who );
}
return true; // force redraw
};
return overview( sel, act );
}
bool veh_interact::do_rename( std::string & )
{
std::string name = string_input_popup()
.title( _( "Enter new vehicle name:" ) )
.width( 20 )
.query_string();
if( name.length() > 0 ) {
veh->name = name;
if( veh->tracking_on ) {
overmap_buffer.remove_vehicle( veh );
// Add the vehicle again, this time with the new name
overmap_buffer.add_vehicle( veh );
}
}
// refresh w_disp & w_part windows:
move_cursor( 0, 0 );
return false;
}
bool veh_interact::do_relabel( std::string &msg )
{
if( cant_do( 'a' ) == INVALID_TARGET ) {
msg = _( "There are no parts here to label." );
return false;
}
std::string text = string_input_popup()
.title( _( "New label:" ) )
.width( 20 )
.text( veh->get_label( -ddx, -ddy ) )
.query_string();
veh->set_label(-ddx, -ddy, text); // empty input removes the label
// refresh w_disp & w_part windows:
move_cursor( 0, 0 );
return false;
}
/**
* Returns the first part on the vehicle at the given position.
* @param dx The x-coordinate, relative to the viewport's 0-point (?)
* @param dy The y-coordinate, relative to the viewport's 0-point (?)
* @return The first vehicle part at the specified coordinates.
*/
int veh_interact::part_at (int dx, int dy)
{
int vdx = -ddx - dy;
int vdy = dx - ddy;
return veh->part_displayed_at(vdx, vdy);
}
/**
* Checks to see if you can potentially install this part at current position.
* Affects coloring in display_list() and is also used to
* sort can_mount so potentially installable parts come first.
*/
bool veh_interact::can_potentially_install(const vpart_info &vpart)
{
return g->u.has_trait( trait_DEBUG_HS ) || vpart.install_requirements().can_make_with_inventory( crafting_inv );
}
/**
* Moves the cursor on the vehicle editing window.
* @param dx How far to move the cursor on the x-axis.
* @param dy How far to move the cursor on the y-axis.
*/
void veh_interact::move_cursor (int dx, int dy)
{
const int hw = getmaxx(w_disp) / 2;
const int hh = getmaxy(w_disp) / 2;
ddx += dy;
ddy -= dx;
display_veh();
// Update the current active component index to the new position.
cpart = part_at (0, 0);
int vdx = -ddx;
int vdy = -ddy;
point q = veh->coord_translate (point(vdx, vdy));
tripoint vehp = veh->global_pos3() + q;
const bool has_critter = g->critter_at( vehp );
bool obstruct = g->m.impassable_ter_furn( vehp );
vehicle *oveh = g->m.veh_at( vehp );
if( oveh != nullptr && oveh != veh ) {
obstruct = true;
}
nc_color col = cpart >= 0 ? veh->part_color (cpart) : c_black;
long sym = cpart >= 0 ? veh->part_sym( cpart ) : ' ';
mvwputch (w_disp, hh, hw, obstruct ? red_background(col) : hilite(col),
special_symbol(sym));
wrefresh (w_disp);
werase (w_parts);
veh->print_part_desc (w_parts, 0, getmaxy( w_parts ) - 1, getmaxx( w_parts ), cpart, -1);
wrefresh (w_parts);
can_mount.clear();
if (!obstruct) {
int divider_index = 0;
for( const auto &e : vpart_info::all() ) {
const vpart_info &vp = e.second;
if( has_critter && vp.has_flag( VPFLAG_OBSTACLE ) ) {
continue;
}
if( veh->can_mount( vdx, vdy, vp.get_id() ) ) {
if ( vp.get_id() != vpart_shapes[ vp.name()+ vp.item][0]->get_id() )
continue; // only add first shape to install list
if (can_potentially_install(vp)) {
can_mount.insert( can_mount.begin() + divider_index++, &vp );
} else {
can_mount.push_back( &vp );
}
}
}
}
need_repair.clear();
parts_here.clear();
wheel = NULL;
if (cpart >= 0) {
parts_here = veh->parts_at_relative(veh->parts[cpart].mount.x, veh->parts[cpart].mount.y);
for (size_t i = 0; i < parts_here.size(); i++) {
auto &pt = veh->parts[parts_here[i]];
if( pt.base.damage() > 0 && pt.info().is_repairable() ) {
need_repair.push_back( i );
}
if( pt.info().has_flag( "WHEEL" ) ) {
wheel = &pt;
}
}
}
werase (w_msg);
wrefresh (w_msg);
}
void veh_interact::display_grid()
{
// border window
catacurses::window w_border = catacurses::newwin( TERMY, TERMX, 0, 0 );
draw_border( w_border );
// match grid lines
const int y_mode = getmaxy( w_mode ) + 1;
mvwputch( w_border, y_mode, 0, BORDER_COLOR, LINE_XXXO ); // |-
mvwputch( w_border, y_mode, TERMX - 1, BORDER_COLOR, LINE_XOXX ); // -|
const int y_list = getbegy( w_list ) + getmaxy( w_list );
mvwputch( w_border, y_list, 0, BORDER_COLOR, LINE_XXXO ); // |-
mvwputch( w_border, y_list, TERMX - 1, BORDER_COLOR, LINE_XOXX ); // -|
wrefresh( w_border );
w_border = catacurses::window(); //@todo: move code using w_border into a separate scope
const int grid_w = getmaxx(w_grid);
// Two lines dividing the three middle sections.
for (int i = 1 + getmaxy( w_mode ); i < (1 + getmaxy( w_mode ) + page_size ); ++i) {
mvwputch(w_grid, i, getmaxx( w_disp ), BORDER_COLOR, LINE_XOXO); // |
mvwputch(w_grid, i, getmaxx( w_disp ) + 1 + getmaxx( w_list) , BORDER_COLOR, LINE_XOXO); // |
}
// Two lines dividing the vertical menu sections.
for (int i = 0; i < grid_w; ++i) {
mvwputch( w_grid, getmaxy( w_mode ), i, BORDER_COLOR, LINE_OXOX ); // -
mvwputch( w_grid, getmaxy( w_mode ) + 1 + page_size, i, BORDER_COLOR, LINE_OXOX ); // -
}
// Fix up the line intersections.
mvwputch(w_grid, getmaxy( w_mode ), getmaxx( w_disp ), BORDER_COLOR, LINE_OXXX);
mvwputch(w_grid, getmaxy( w_mode ) + 1 + page_size, getmaxx( w_disp ), BORDER_COLOR, LINE_XXOX); // _|_
mvwputch(w_grid, getmaxy( w_mode ), getmaxx( w_disp ) + 1 + getmaxx( w_list ), BORDER_COLOR, LINE_OXXX);
mvwputch(w_grid, getmaxy( w_mode ) + 1 + page_size, getmaxx( w_disp ) + 1 + getmaxx( w_list ), BORDER_COLOR, LINE_XXOX); // _|_
wrefresh(w_grid);
}
/**
* Draws the viewport with the vehicle.
*/
void veh_interact::display_veh ()
{
werase(w_disp);
const int hw = getmaxx(w_disp) / 2;
const int hh = getmaxy(w_disp) / 2;
if (debug_mode) {
// show CoM, pivot in debug mode
const point &pivot = veh->pivot_point();
const point &com = veh->local_center_of_mass();
mvwprintz(w_disp, 0, 0, c_green, "CoM %d,%d", com.x, com.y);
mvwprintz(w_disp, 1, 0, c_red, "Pivot %d,%d", pivot.x, pivot.y);
int com_sx = com.y + ddy + hw;
int com_sy = -(com.x + ddx) + hh;
int pivot_sx = pivot.y + ddy + hw;
int pivot_sy = -(pivot.x + ddx) + hh;
for (int x = 0; x < getmaxx(w_disp); ++x) {
if (x <= com_sx) {
mvwputch (w_disp, com_sy, x, c_green, LINE_OXOX);
}
if (x >= pivot_sx) {
mvwputch (w_disp, pivot_sy, x, c_red, LINE_OXOX);
}
}
for (int y = 0; y < getmaxy(w_disp); ++y) {
if (y <= com_sy) {
mvwputch (w_disp, y, com_sx, c_green, LINE_XOXO);
}
if (y >= pivot_sy) {
mvwputch (w_disp, y, pivot_sx, c_red, LINE_XOXO);
}
}
}
//Iterate over structural parts so we only hit each square once
std::vector<int> structural_parts = veh->all_parts_at_location("structure");
for( auto &structural_part : structural_parts ) {
const int p = structural_part;
long sym = veh->part_sym (p);
nc_color col = veh->part_color (p);
int x = veh->parts[p].mount.y + ddy;
int y = -(veh->parts[p].mount.x + ddx);
if (x == 0 && y == 0) {
col = hilite(col);
cpart = p;
}
mvwputch (w_disp, hh + y, hw + x, col, special_symbol(sym));
}
wrefresh (w_disp);
}
static std::string wheel_state_description( const vehicle &veh )
{
bool is_boat = !veh.all_parts_with_feature(VPFLAG_FLOATS).empty();
bool is_land = !veh.all_parts_with_feature(VPFLAG_WHEEL).empty();
bool suf_land = veh.sufficient_wheel_config( false );
bool bal_land = veh.balanced_wheel_config( false );
bool suf_boat = veh.sufficient_wheel_config( true );
bool bal_boat = veh.balanced_wheel_config( true );
float steer = veh.steering_effectiveness();
std::string wheel_status;
if( !suf_land && is_boat ) {
wheel_status = _( "<color_light_red>disabled</color>" );
} else if( !suf_land ) {
wheel_status = _( "<color_light_red>lack</color>" );
} else if( !bal_land ) {
wheel_status = _( "<color_light_red>unbalanced</color>" );
} else if( steer < 0 ) {
wheel_status = _( "<color_light_red>no steering</color>" );
} else if( steer < 0.033 ) {
wheel_status = _( "<color_light_red>broken steering</color>" );
} else if( steer < 0.5 ) {
wheel_status = _( "<color_light_red>poor steering</color>" );
} else {
wheel_status = _( "<color_light_green>enough</color>" );
}
std::string boat_status;
if( !suf_boat ) {
boat_status = _( "<color_light_red>leaks</color>" );
} else if( !bal_boat ) {
boat_status = _( "<color_light_red>unbalanced</color>" );
} else {
boat_status = _( "<color_blue>swims</color>" );
}
if( is_boat && is_land ) {
return string_format( _( "Wheels/boat: %s/%s" ), wheel_status.c_str(), boat_status.c_str() );
}
if( is_boat ) {
return string_format( _( "Boat: %s" ), boat_status.c_str() );
}
return string_format( _( "Wheels: %s" ), wheel_status.c_str() );
}
/**
* Displays the vehicle's stats at the bottom of the window.
*/
void veh_interact::display_stats()
{
werase(w_stats);
const int extraw = ((TERMX - FULL_SCREEN_WIDTH) / 4) * 2; // see exec()
int x[18], y[18], w[18]; // 3 columns * 6 rows = 18 slots max
std::vector<int> cargo_parts = veh->all_parts_with_feature("CARGO");
units::volume total_cargo = 0;
units::volume free_cargo = 0;
for( const auto &p : cargo_parts ) {
total_cargo += veh->max_volume(p);
free_cargo += veh->free_volume(p);
}
const int second_column = 33 + (extraw / 4);
const int third_column = 65 + (extraw / 2);
for (int i = 0; i < 18; i++) {
if (i < 6) { // First column
x[i] = 1;
y[i] = i;
w[i] = second_column - 2;
} else if (i < 13) { // Second column
x[i] = second_column;
y[i] = i - 6;
w[i] = third_column - second_column - 1;
} else { // Third column
x[i] = third_column;
y[i] = i - 13;
w[i] = extraw - third_column - 2;
}
}
fold_and_print( w_stats, y[0], x[0], w[0], c_light_gray,
_( "Safe/Top Speed: <color_light_green>%3d</color>/<color_light_red>%3d</color> %s" ),
int( convert_velocity( veh->safe_velocity( false ), VU_VEHICLE ) ),
int( convert_velocity( veh->max_velocity( false ), VU_VEHICLE ) ),
velocity_units( VU_VEHICLE ) );
//TODO: extract accelerations units to its own function
fold_and_print( w_stats, y[1], x[1], w[1], c_light_gray,
//~ /t means per turn
_( "Acceleration: <color_light_blue>%3d</color> %s/t" ),
int( convert_velocity( veh->acceleration( false ), VU_VEHICLE ) ),
velocity_units( VU_VEHICLE ) );
fold_and_print( w_stats, y[2], x[2], w[2], c_light_gray,
_( "Mass: <color_light_blue>%5.0f</color> %s" ),
convert_weight( veh->total_mass() ), weight_units() );
fold_and_print( w_stats, y[3], x[3], w[3], c_light_gray,
_( "Cargo Volume: <color_light_gray>%s/%s</color> %s" ),
format_volume( total_cargo - free_cargo ).c_str(),
format_volume( total_cargo ).c_str(),
volume_units_abbr() );
// Write the overall damage
mvwprintz(w_stats, y[4], x[4], c_light_gray, _("Status:"));
x[4] += utf8_width(_("Status:")) + 1;
fold_and_print(w_stats, y[4], x[4], w[4], totalDurabilityColor, totalDurabilityText);
fold_and_print( w_stats, y[5], x[5], w[5], c_light_gray, wheel_state_description( *veh ).c_str() );
//This lambda handles printing parts in the "Most damaged" and "Needs repair" cases
//for the veh_interact ui
auto print_part = [&]( const char * str, int slot, vehicle_part *pt )
{
mvwprintz( w_stats, y[slot], x[slot], c_light_gray, str);
auto iw = utf8_width( str ) + 1;
x[slot] += iw;
w[slot] -= iw;
const auto hoff = fold_and_print( w_stats, y[slot], x[slot], w[slot],
pt->is_broken() ? c_dark_gray : pt->base.damage_color(), pt->name() );
// If fold_and_print did write on the next line(s), shift the following entries,
// hoff == 1 is already implied and expected - one line is consumed at least.
for( size_t i = slot + 1; i < sizeof( y ) / sizeof( y[0] ); ++i ) {
y[i] += hoff - 1;
}
};
vehicle_part *mostDamagedPart = get_most_damaged_part();
vehicle_part *most_repairable = get_most_repariable_part();
// Write the most damaged part
if( mostDamagedPart ) {
char const *damaged_header = mostDamagedPart == most_repairable ?
_( "Most damaged:" ) : _( "Most damaged (can't repair):" );
print_part( damaged_header, 6, mostDamagedPart );
}
// Write the part that needs repair the most.
if( most_repairable && most_repairable != mostDamagedPart ) {
char const * needsRepair = _( "Needs repair:" );
print_part( needsRepair, 7, most_repairable );
}
bool is_boat = !veh->all_parts_with_feature(VPFLAG_FLOATS).empty();
fold_and_print(w_stats, y[8], x[8], w[8], c_light_gray,
_("K aerodynamics: <color_light_blue>%3d</color>%%"),
int(veh->k_aerodynamics() * 100));
fold_and_print(w_stats, y[9], x[9], w[9], c_light_gray,
_("K friction: <color_light_blue>%3d</color>%%"),
int(veh->k_friction() * 100));
fold_and_print(w_stats, y[10], x[10], w[10], c_light_gray,
_("K mass: <color_light_blue>%3d</color>%%"),
int(veh->k_mass() * 100));
fold_and_print( w_stats, y[11], x[11], w[11], c_light_gray,
_("Offroad: <color_light_blue>%3d</color>%%"),
int( veh->k_traction( veh->wheel_area( is_boat ) * 0.5f ) * 100 ) );
// Print fuel percentage & type name only if it fits in the window, 10 is width of "E...F 100%"
veh->print_fuel_indicators (w_stats, y[13], x[13], fuel_index, true,
( x[ 13 ] + 10 < getmaxx( w_stats ) ),
( x[ 13 ] + 10 < getmaxx( w_stats ) ) );
wrefresh(w_stats);
}
void veh_interact::display_name()
{
werase(w_name);
mvwprintz(w_name, 0, 1, c_light_gray, _("Name: "));
mvwprintz(w_name, 0, 1 + utf8_width(_("Name: ")), c_light_green, veh->name.c_str());
wrefresh(w_name);
}
/**
* Prints the list of usable commands, and highlights the hotkeys used to activate them.
*/
void veh_interact::display_mode()
{
werase (w_mode);
size_t esc_pos = display_esc(w_mode);
// broken indentation preserved to avoid breaking git history for large number of lines
const std::array<std::string, 10> actions = { {
{ _("<i>nstall") },
{ _("<r>epair") },
{ _("<m>end" ) },
{ _("re<f>ill") },
{ _("rem<o>ve") },
{ _("<s>iphon") },
{ _("<c>hange tire") },
{ _("cre<w>") },
{ _("r<e>name") },
{ _("l<a>bel") },
} };
const std::array<bool, std::tuple_size<decltype(actions)>::value> enabled = { {
!cant_do('i'),
!cant_do('r'),
!cant_do('m'),
!cant_do('f'),
!cant_do('o'),
!cant_do('s'),
!cant_do('c'),
!cant_do('w'),
true, // 'rename' is always available
!cant_do('a'),
} };
int pos[std::tuple_size<decltype(actions)>::value + 1];
pos[0] = 1;
for (size_t i = 0; i < actions.size(); i++) {
pos[i + 1] = pos[i] + utf8_width(actions[i]) - 2;
}
int spacing = int((esc_pos - 1 - pos[actions.size()]) / actions.size());
int shift = int((esc_pos - pos[actions.size()] - spacing * (actions.size() - 1)) / 2) - 1;
for (size_t i = 0; i < actions.size(); i++) {
shortcut_print(w_mode, 0, pos[i] + spacing * i + shift,
enabled[i] ? c_light_gray : c_dark_gray, enabled[i] ? c_light_green : c_green,
actions[i]);
}
wrefresh (w_mode);
}
size_t veh_interact::display_esc( const catacurses::window &win )
{
std::string backstr = _("<ESC>-back");
size_t pos = getmaxx(win) - utf8_width(backstr) + 2; // right text align
shortcut_print(win, 0, pos, c_light_gray, c_light_green, backstr);
wrefresh(win);
return pos;
}
/**
* Draws the list of parts that can be mounted in the selected square. Used
* when installing new parts or changing tires.
* @param pos The current cursor position in the list.
* @param list The list to display parts from.
* @param header Number of lines occupied by the list header
*/
void veh_interact::display_list(size_t pos, std::vector<const vpart_info*> list, const int header)
{
werase (w_list);
int lines_per_page = page_size - header;
size_t page = pos / lines_per_page;
for (size_t i = page * lines_per_page; i < (page + 1) * lines_per_page && i < list.size(); i++) {
const vpart_info &info = *list[i];
int y = i - page * lines_per_page + header;
mvwputch( w_list, y, 1, info.color, special_symbol( info.sym ) );
nc_color col = can_potentially_install( info ) ? c_white : c_dark_gray;
trim_and_print( w_list, y, 3, getmaxx( w_list ) - 3, pos == i ? hilite( col ) : col,
info.name().c_str() );
}
wrefresh (w_list);
}
/**
* Used when installing parts.
* Opens up w_details containing info for part currently selected in w_list.
*/
void veh_interact::display_details( const vpart_info *part )
{
if( !w_details ) { // create details window first if required
// covers right part of w_name and w_stats in vertical/hybrid
const int details_y = getbegy(w_name);
const int details_x = getbegx(w_list);
const int details_h = 7;
const int details_w = getbegx(w_grid) + getmaxx(w_grid) - details_x;
// clear rightmost blocks of w_stats to avoid overlap
int stats_col_2 = 33;
int stats_col_3 = 65 + ((TERMX - FULL_SCREEN_WIDTH) / 4);
int clear_x = getmaxx( w_stats ) - details_w + 1 >= stats_col_3 ? stats_col_3 : stats_col_2;
for( int i = 0; i < getmaxy( w_stats ); i++) {
mvwhline(w_stats, i, clear_x, ' ', getmaxx( w_stats ) - clear_x);
}
wrefresh(w_stats);
w_details = catacurses::newwin( details_h, details_w, details_y, details_x );
}
else {
werase(w_details);
}
wborder(w_details, LINE_XOXO, LINE_XOXO, LINE_OXOX, LINE_OXOX, LINE_OXXO, LINE_OOXX, LINE_XXOO, LINE_XOOX);
if ( part == NULL ) {
wrefresh(w_details);
return;
}
int details_w = getmaxx(w_details);
int column_width = details_w / 2; // displays data in two columns
int col_1 = 2;
int col_2 = col_1 + column_width;
int line = 0;
bool small_mode = column_width < 20 ? true : false;
// line 0: part name
fold_and_print( w_details, line, col_1, details_w, c_light_green, part->name() );
// line 1: (column 1) durability (column 2) damage mod
fold_and_print(w_details, line+1, col_1, column_width, c_white,
"%s: <color_light_gray>%d</color>",
small_mode ? _("Dur") : _("Durability"),
part->durability);
fold_and_print(w_details, line+1, col_2, column_width, c_white,
"%s: <color_light_gray>%d%%</color>",
small_mode ? _("Dmg") : _("Damage"),
part->dmg_mod);
// line 2: (column 1) weight (column 2) folded volume (if applicable)
fold_and_print(w_details, line+2, col_1, column_width, c_white,
"%s: <color_light_gray>%.1f%s</color>",
small_mode ? _("Wgt") : _("Weight"),
convert_weight( item::find_type( part->item )->weight ),
weight_units());
if ( part->folded_volume != 0 ) {
fold_and_print(w_details, line+2, col_2, column_width, c_white,
"%s: <color_light_gray>%s %s</color>",
small_mode ? _("FoldVol") : _("Folded Volume"),
format_volume( part->folded_volume ).c_str(),
volume_units_abbr() );
}
// line 3: (column 1) size, bonus, wheel diameter (if applicable) (column 2) epower, wheel width (if applicable)
if( part->size > 0 && part->has_flag( VPFLAG_CARGO ) ) {
fold_and_print( w_details, line+3, col_1, column_width, c_white,
"%s: <color_light_gray>%d</color>", small_mode ? _( "Cap" ) : _( "Capacity" ),
to_milliliter( part->size ) );
}
if( part->bonus > 0 ) {
std::string label;
if( part->has_flag( VPFLAG_SEATBELT ) ) {
label = small_mode ? _( "Str" ) : _( "Strength" );
} else if( part->has_flag( "HORN" ) ) {
label = _( "Noise" );
} else if( part->has_flag( "MUFFLER" ) ) {
label = small_mode ? _( "NoisRed" ) : _( "Noise Reduction" );
} else if( part->has_flag( VPFLAG_EXTENDS_VISION ) ) {
label = _( "Range" );
} else if( part->has_flag( VPFLAG_LIGHT ) || part->has_flag( VPFLAG_CONE_LIGHT ) ||
part->has_flag( VPFLAG_CIRCLE_LIGHT ) || part->has_flag( VPFLAG_DOME_LIGHT ) ||
part->has_flag( VPFLAG_AISLE_LIGHT ) || part->has_flag( VPFLAG_EVENTURN ) ||
part->has_flag( VPFLAG_ODDTURN ) || part->has_flag( VPFLAG_ATOMIC_LIGHT ) ) {
label = _( "Light" );
}
if( !label.empty() ) {
fold_and_print( w_details, line+3, col_1, column_width, c_white,
"%s: <color_light_gray>%d</color>", label.c_str(),
part->bonus );
}
}
if( part->has_flag( VPFLAG_WHEEL ) ) {
cata::optional<islot_wheel> whl = item::find_type( part->item )->wheel;
fold_and_print( w_details, line+3, col_1, column_width, c_white,
"%s: <color_light_gray>%d\"</color>", small_mode ? _( "Dia" ) : _( "Wheel Diameter" ),
whl->diameter );
fold_and_print( w_details, line+3, col_2, column_width, c_white,
"%s: <color_light_gray>%d\"</color>", small_mode ? _( "Wdt" ) : _( "Wheel Width" ),
whl->width );
}
if ( part->epower != 0 ) {
fold_and_print(w_details, line+3, col_2, column_width, c_white,
"%s: %c<color_light_gray>%d</color>",
small_mode ? _("Bat") : _("Battery"),
part->epower < 0 ? '-' : '+',
abs(part->epower));
}
// line 4 [horizontal]: fuel_type (if applicable)
// line 4 [vertical/hybrid]: (column 1) fuel_type (if applicable) (column 2) power (if applicable)
// line 5 [horizontal]: power (if applicable)
if ( part->fuel_type != "null" ) {
fold_and_print( w_details, line+4, col_1, column_width,
c_white, _("Charge: <color_light_gray>%s</color>"),
item::nname( part->fuel_type ).c_str() );
}
if ( part->power != 0 ) {
fold_and_print( w_details, line + 4, col_2, column_width, c_white, _( "Power: <color_light_gray>%d</color>" ), part->power );
}
// line 5 [vertical/hybrid] 6 [horizontal]: flags
std::vector<std::string> flags = { { "OPAQUE", "OPENABLE", "BOARDABLE" } };
std::vector<std::string> flag_labels = { { _("opaque"), _("openable"), _("boardable") } };
std::string label;
for ( size_t i = 0; i < flags.size(); i++ ) {
if ( part->has_flag(flags[i]) ) {
label += ( label.empty() ? "" : " " ) + flag_labels[i];
}
}
fold_and_print(w_details, line + 5, col_1, details_w, c_yellow, label);
wrefresh(w_details);
}
void veh_interact::countDurability()
{
int qty = std::accumulate( veh->parts.begin(), veh->parts.end(), 0,
[]( int lhs, const vehicle_part &rhs ) {
return lhs + std::max( rhs.base.damage(), 0 );
} );
int total = std::accumulate( veh->parts.begin(), veh->parts.end(), 0,
[]( int lhs, const vehicle_part &rhs ) {
return lhs + rhs.base.max_damage();
} );
double pct = double( qty ) / double( total );
if( pct < 0.05 ) {
totalDurabilityText = _( "like new" );
totalDurabilityColor = c_light_green;
} else if( pct < 0.33 ) {
totalDurabilityText = _( "dented" );
totalDurabilityColor = c_yellow;
} else if( pct < 0.66 ) {
totalDurabilityText = _( "battered" );
totalDurabilityColor = c_magenta;
} else if( pct < 1.00 ) {
totalDurabilityText = _( "wrecked" );
totalDurabilityColor = c_red;
} else {
totalDurabilityText = _( "destroyed" );
totalDurabilityColor = c_dark_gray;
}
}
/**
* Given a vpart id, gives the choice of inventory and nearby items to consume
* for install/repair/etc. Doesn't use consume_items in crafting.cpp, as it got
* into weird cases and doesn't consider properties like HP. The
* item will be removed by this function.
* @param vpid The id of the vpart type to look for.
* @return The item that was consumed.
*/
item consume_vpart_item( const vpart_id &vpid )
{
std::vector<bool> candidates;
const itype_id itid = vpid.obj().item;
if(g->u.has_trait( trait_DEBUG_HS )) {
return item(itid, calendar::turn);
}
inventory map_inv;
map_inv.form_from_map( g->u.pos(), PICKUP_RANGE );
if( g->u.has_amount( itid, 1 ) ) {
candidates.push_back( true );
}
if( map_inv.has_components( itid, 1 ) ) {
candidates.push_back( false );
}
// bug?
if(candidates.empty()) {
debugmsg("Part not found!");
return item();
}
int selection;
// no choice?
if(candidates.size() == 1) {
selection = 0;
} else {
// popup menu!?
std::vector<std::string> options;
for( const auto &candidate : candidates ) {
const vpart_info &info = vpid.obj();
if( candidate ) {
// In inventory.
options.emplace_back( info.name() );
} else {
// Nearby.
options.emplace_back( info.name() + _(" (nearby)" ) );
}
}
selection = menu_vec(false, _("Use which gizmo?"), options);
selection -= 1;
}
std::list<item> item_used;
//remove item from inventory. or map.
if( candidates[selection] ) {
item_used = g->u.use_amount( itid, 1 );
} else {
long quantity = 1;
item_used = g->m.use_amount( g->u.pos(), PICKUP_RANGE, itid, quantity );
}
remove_ammo( item_used, g->u );
return item_used.front();
}
void act_vehicle_siphon(vehicle* veh) {
std::vector<itype_id> fuels;
for( auto & e : veh->fuels_left() ) {
const itype *type = item::find_type( e.first );
if( type->phase != LIQUID ) {
// This skips battery and plutonium cells
continue;
}
fuels.push_back( e.first );
}
if( fuels.empty() ) {
add_msg(m_info, _("The vehicle has no liquid fuel left to siphon."));
return;
}
itype_id fuel;
if( fuels.size() > 1 ) {
uimenu smenu;
smenu.text = _("Siphon what?");
for( auto & fuel : fuels ) {
smenu.addentry( item::nname( fuel ) );
}
smenu.addentry(_("Never mind"));
smenu.query();
if( static_cast<size_t>( smenu.ret ) >= fuels.size() ) {
add_msg(m_info, _("Never mind."));
return;
}
fuel = fuels[smenu.ret];
} else {
fuel = fuels.front();
}
g->u.siphon( *veh, fuel );
}
/**
* Called when the activity timer for installing parts, repairing, etc times
* out and the action is complete.
*/
void veh_interact::complete_vehicle()
{
if (g->u.activity.values.size() < 7) {
debugmsg ("Invalid activity ACT_VEHICLE values:%d", g->u.activity.values.size());
return;
}
vehicle *veh = g->m.veh_at( tripoint( g->u.activity.values[0], g->u.activity.values[1], g->u.posz() ) );
if (!veh) {
debugmsg ("Activity ACT_VEHICLE: vehicle not found");
return;
}
int dx = g->u.activity.values[4];
int dy = g->u.activity.values[5];
int vehicle_part = g->u.activity.values[6];
const vpart_id part_id( g->u.activity.str_values[0] );
const vpart_info &vpinfo = part_id.obj();
// cmd = Install Repair reFill remOve Siphon Changetire reName relAbel
switch( (char) g->u.activity.index ) {
case 'i': {
auto inv = g->u.crafting_inventory();
const auto reqs = vpinfo.install_requirements();
if( !reqs.can_make_with_inventory( inv ) ) {
add_msg( m_info, _( "You don't meet the requirements to install the %s." ), vpinfo.name().c_str() );
break;
}
// consume items extracting a match for the parts base item
item base;
for( const auto& e : reqs.get_components() ) {
for( auto& obj : g->u.consume_items( e ) ) {
if( obj.typeId() == vpinfo.item ) {
base = obj;
}
}
}
if( base.is_null() ) {
if( !g->u.has_trait( trait_DEBUG_HS ) ) {
add_msg( m_info, _( "Could not find base part in requirements for %s." ), vpinfo.name().c_str() );
break;
} else {
base = item( vpinfo.item );
}
}
for( const auto& e : reqs.get_tools() ) {
g->u.consume_tools( e );
}
g->u.invalidate_crafting_inventory();
int partnum = !base.is_null() ? veh->install_part( dx, dy, part_id, std::move( base ) ) : -1;
if(partnum < 0) {
debugmsg ("complete_vehicle install part fails dx=%d dy=%d id=%d", dx, dy, part_id.c_str());
break;
}
// Need map-relative coordinates to compare to output of look_around.
// Need to call coord_translate() directly since it's a new part.
const point q = veh->coord_translate( point( dx, dy ) );
if ( vpinfo.has_flag("CONE_LIGHT") ) {
// Stash offset and set it to the location of the part so look_around will start there.
int px = g->u.view_offset.x;
int py = g->u.view_offset.y;
g->u.view_offset.x = veh->global_x() + q.x - g->u.posx();
g->u.view_offset.y = veh->global_y() + q.y - g->u.posy();
popup(_("Choose a facing direction for the new headlight. Press space to continue."));
tripoint headlight_target = g->look_around(); // Note: no way to cancel
// Restore previous view offsets.
g->u.view_offset.x = px;
g->u.view_offset.y = py;
int dir = 0;
if(headlight_target.x == INT_MIN) {
dir = 0;
} else {
int delta_x = headlight_target.x - (veh->global_x() + q.x);
int delta_y = headlight_target.y - (veh->global_y() + q.y);
const double PI = 3.14159265358979f;
dir = int(atan2(static_cast<float>(delta_y), static_cast<float>(delta_x)) * 180.0 / PI);
dir -= veh->face.dir();
while(dir < 0) {
dir += 360;
}
while(dir > 360) {
dir -= 360;
}
}
veh->parts[partnum].direction = dir;
}
const tripoint vehp = { q.x + veh->global_x(), q.y + veh->global_y(), g->u.posz() };
//@todo: allow boarding for non-players as well.
player * const pl = g->critter_at<player>( vehp );
if( vpinfo.has_flag( VPFLAG_BOARDABLE ) && pl ) {
g->m.board_vehicle( vehp, pl );
}
add_msg( m_good, _("You install a %1$s into the %2$s." ), veh->parts[ partnum ].name().c_str(), veh->name.c_str() );
for( const auto &sk : vpinfo.install_skills ) {
g->u.practice( sk.first, veh_utils::calc_xp_gain( vpinfo, sk.first ) );
}
break;
}
case 'r': {
veh_utils::repair_part( *veh, veh->parts[ vehicle_part ], g->u );
break;
}
case 'f': {
if( g->u.activity.targets.empty() || !g->u.activity.targets.front() ) {
debugmsg( "Activity ACT_VEHICLE: missing refill source" );
break;
}
auto &src = g->u.activity.targets.front();
auto &pt = veh->parts[ vehicle_part ];
if( pt.is_tank() && src->is_watertight_container() && !src->contents.empty() ) {
pt.base.fill_with( src->contents.front() );
if ( pt.ammo_remaining() != pt.ammo_capacity() ) {
//~ 1$s vehicle name, 2$s tank name
add_msg( m_good, _( "You refill the %1$s's %2$s." ),
veh->name.c_str(), pt.name().c_str() );
} else {
//~ 1$s vehicle name, 2$s tank name
add_msg( m_good, _( "You completely refill the %1$s's %2$s." ),
veh->name.c_str(), pt.name().c_str() );
}
if( src->contents.front().charges == 0 ) {
src->contents.erase( src->contents.begin() );
} else {
add_msg( m_good, _( "There's some left over!" ) );
}
} else if( pt.is_reactor() ) {
auto qty = src->charges;
pt.base.reload( g->u, std::move( src ), qty );
//~ 1$s vehicle name, 2$s reactor name
add_msg( m_good, _( "You refuel the %1$s's %2$s." ),
veh->name.c_str(), pt.name().c_str() );
} else {
debugmsg( "vehicle part is not reloadable" );
break;
}
veh->invalidate_mass();
break;
}
case 'o': {
auto inv = g->u.crafting_inventory();
const auto reqs = vpinfo.removal_requirements();
if( !reqs.can_make_with_inventory( inv ) ) {
add_msg( m_info, _( "You don't meet the requirements to remove the %s." ), vpinfo.name().c_str() );
break;
}
for( const auto& e : reqs.get_components() ) {
g->u.consume_items( e );
}
for( const auto& e : reqs.get_tools() ) {
g->u.consume_tools( e );
}
g->u.invalidate_crafting_inventory();
// Dump contents of part at player's feet, if any.
vehicle_stack contents = veh->get_items( vehicle_part );
for( auto iter = contents.begin(); iter != contents.end(); ) {
g->m.add_item_or_charges( g->u.posx(), g->u.posy(), *iter );
iter = contents.erase( iter );
}
// Power cables must remove parts from the target vehicle, too.
if (veh->part_flag(vehicle_part, "POWER_TRANSFER")) {
veh->remove_remote_part(vehicle_part);
}
bool broken = veh->parts[ vehicle_part ].is_broken();
if (!broken) {
g->m.add_item_or_charges( g->u.pos(), veh->parts[vehicle_part].properties_to_item() );
for( const auto &sk : vpinfo.install_skills ) {
// removal is half as educational as installation
g->u.practice( sk.first, veh_utils::calc_xp_gain( vpinfo, sk.first ) / 2 );
}
} else {
veh->break_part_into_pieces(vehicle_part, g->u.posx(), g->u.posy());
}
if (veh->parts.size() < 2) {
add_msg (_("You completely dismantle the %s."), veh->name.c_str());
g->u.activity.set_to_null();
g->m.destroy_vehicle (veh);
} else {
if (broken) {
add_msg( _( "You remove the broken %1$s from the %2$s." ),
veh->parts[ vehicle_part ].name().c_str(), veh->name.c_str() );
} else {
add_msg( _( "You remove the %1$s from the %2$s." ),
veh->parts[ vehicle_part ].name().c_str(), veh->name.c_str() );
}
veh->remove_part (vehicle_part);
veh->part_removal_cleanup();
}
break;
}
case 'c':
std::vector<int> parts = veh->parts_at_relative( dx, dy );
if( parts.size() ) {
item removed_wheel;
int replaced_wheel = veh->part_with_feature( parts[0], "WHEEL", false );
if( replaced_wheel == -1 ) {
debugmsg( "no wheel to remove when changing wheels." );
return;
}
bool broken = veh->parts[ replaced_wheel ].is_broken();
removed_wheel = veh->parts[replaced_wheel].properties_to_item();
veh->remove_part( replaced_wheel );
veh->part_removal_cleanup();
int partnum = veh->install_part( dx, dy, part_id, consume_vpart_item( part_id ) );
if( partnum < 0 ) {
debugmsg ("complete_vehicle tire change fails dx=%d dy=%d id=%d", dx, dy, part_id.c_str());
}
// Place the removed wheel on the map last so consume_vpart_item() doesn't pick it.
if ( !broken ) {
g->m.add_item_or_charges( g->u.posx(), g->u.posy(), removed_wheel );
}
add_msg( _( "You replace one of the %1$s's tires with a %2$s." ),
veh->name.c_str(), veh->parts[ partnum ].name().c_str() );
}
break;
}
g->u.invalidate_crafting_inventory();
}
You can’t perform that action at this time.