diff --git a/src/ranged.cpp b/src/ranged.cpp index 71962c72576a0..190d1e2935ecc 100644 --- a/src/ranged.cpp +++ b/src/ranged.cpp @@ -25,6 +25,7 @@ #include "item.h" #include "itype.h" #include "line.h" +#include "magic.h" #include "map.h" #include "messages.h" #include "monster.h" @@ -1044,92 +1045,430 @@ std::vector Character::get_aim_types( const item &gun ) const return aim_types; } -// TODO: Shunt redundant drawing code elsewhere -std::vector target_handler::target_ui( player &pc, target_mode mode, - item *relevant, int range, const itype *ammo, - const target_callback &on_mode_change, - const target_callback &on_ammo_change ) +void update_targets( player &pc, int range, std::vector &targets, int &idx, + tripoint &src, tripoint &dst ) { - // TODO: this should return a reference to a static vector which is cleared on each call. - static const std::vector empty_result{}; - std::vector ret; + targets = pc.get_targetable_creatures( range ); + + // Convert and check last_target_pos is a valid aim point + cata::optional local_last_tgt_pos = cata::nullopt; + if( pc.last_target_pos ) { + local_last_tgt_pos = g->m.getlocal( *pc.last_target_pos ); + if( rl_dist( src, *local_last_tgt_pos ) > range ) { + local_last_tgt_pos = cata::nullopt; + } + } - int sight_dispersion = 0; - if( !relevant ) { - relevant = &pc.weapon; + targets.erase( std::remove_if( targets.begin(), targets.end(), [&]( const Creature * e ) { + return pc.attitude_to( *e ) == Creature::Attitude::A_FRIENDLY; + } ), targets.end() ); + + if( targets.empty() ) { + idx = -1; + + if( pc.last_target_pos ) { + + if( local_last_tgt_pos ) { + dst = *local_last_tgt_pos; + } + if( ( pc.last_target.expired() || !pc.sees( *pc.last_target.lock().get() ) ) && + pc.has_activity( activity_id( "ACT_AIM" ) ) ) { + //We lost our target. Stop auto aiming. + pc.cancel_activity(); + } + + } else { + auto adjacent = closest_tripoints_first( range, dst ); + const auto target_spot = std::find_if( adjacent.begin(), adjacent.end(), + [&pc]( const tripoint & pt ) { + return g->m.tr_at( pt ).id == tr_practice_target && pc.sees( pt ); + } ); + + if( target_spot != adjacent.end() ) { + dst = *target_spot; + } + } + return; + } + + std::sort( targets.begin(), targets.end(), [&]( const Creature * lhs, const Creature * rhs ) { + return rl_dist( lhs->pos(), pc.pos() ) < rl_dist( rhs->pos(), pc.pos() ); + } ); + + // TODO: last_target should be member of target_handler + const auto iter = std::find( targets.begin(), targets.end(), pc.last_target.lock().get() ); + + if( iter != targets.end() ) { + idx = std::distance( targets.begin(), iter ); + dst = targets[idx]->pos(); + pc.last_target_pos = cata::nullopt; } else { - sight_dispersion = pc.effective_dispersion( relevant->sight_dispersion() ); + idx = 0; + dst = local_last_tgt_pos ? *local_last_tgt_pos : targets[0]->pos(); + pc.last_target.reset(); + } +} + +// magic mod +std::vector target_handler::target_ui( spell_id sp ) +{ + return target_ui( g->u.magic.get_spell( sp ) ); +} +// does not have a targeting mode because we know this is the spellcasting version of this function +std::vector target_handler::target_ui( spell &casting ) +{ + player &pc = g->u; + if( !casting.can_cast( pc ) ) { + pc.add_msg_if_player( m_bad, _( "You don't have enough %s to cast this spell" ), + casting.energy_string() ); } + bool compact = TERMY < 41; + bool tiny = TERMY < 31; + + // Default to the maximum window size we can use. + int height = 31; + int top = 0; + if( tiny ) { + // If we're extremely short on space, use the whole sidebar. + top = 0; + height = TERMY; + } else if( compact ) { + // Cover up more low-value ui elements if we're tight on space. + top -= 4; + height = 25; + } + top = 0; + catacurses::window w_target = catacurses::newwin( height, 45, top, TERMX - 45 ); + + // TODO: this should return a reference to a static vector which is cleared on each call. + static const std::vector empty_result{}; + std::vector ret; tripoint src = pc.pos(); tripoint dst = pc.pos(); std::vector t; int target = 0; + int range = static_cast( casting.range() ); - auto update_targets = [&]( int range, std::vector &targets, int &idx, tripoint & dst ) { - targets = pc.get_targetable_creatures( range ); + update_targets( pc, range, t, target, src, dst ); - // Convert and check last_target_pos is a valid aim point - cata::optional local_last_tgt_pos = cata::nullopt; - if( pc.last_target_pos ) { - local_last_tgt_pos = g->m.getlocal( *pc.last_target_pos ); - if( rl_dist( src, *local_last_tgt_pos ) > range ) { - local_last_tgt_pos = cata::nullopt; + input_context ctxt( "TARGET" ); + ctxt.set_iso( true ); + // "ANY_INPUT" should be added before any real help strings + // Or strings will be written on window border. + ctxt.register_action( "ANY_INPUT" ); + ctxt.register_directions(); + ctxt.register_action( "COORDINATE" ); + ctxt.register_action( "SELECT" ); + ctxt.register_action( "FIRE" ); + ctxt.register_action( "NEXT_TARGET" ); + ctxt.register_action( "PREV_TARGET" ); + ctxt.register_action( "LEVEL_UP" ); + ctxt.register_action( "LEVEL_DOWN" ); + ctxt.register_action( "CENTER" ); + ctxt.register_action( "TOGGLE_SNAP_TO_TARGET" ); + ctxt.register_action( "HELP_KEYBINDINGS" ); + ctxt.register_action( "QUIT" ); + + std::vector aim_types; + + int num_instruction_lines = draw_targeting_window( w_target, casting.name(), + TARGET_MODE_SPELL, ctxt, aim_types, tiny ); + + bool snap_to_target = get_option( "SNAP_TO_TARGET" ); + + const auto set_last_target = [&pc]( const tripoint & dst ) { + pc.last_target_pos = g->m.getabs( dst ); + if( const Creature *const critter_ptr = g->critter_at( dst, true ) ) { + pc.last_target = g->shared_from( *critter_ptr ); + } else { + pc.last_target.reset(); + } + }; + + const auto confirm_non_enemy_target = [&pc]( const tripoint & dst ) { + if( dst == pc.pos() ) { + return true; + } + if( npc *const who_ = g->critter_at( dst ) ) { + const npc &who = *who_; + if( who.guaranteed_hostile() ) { + return true; } + return query_yn( _( "Really attack %s?" ), who.name.c_str() ); } + return true; + }; - targets.erase( std::remove_if( targets.begin(), targets.end(), [&]( const Creature * e ) { - return pc.attitude_to( *e ) == Creature::Attitude::A_FRIENDLY; - } ), targets.end() ); + const tripoint old_offset = pc.view_offset; + do { + ret = g->m.find_clear_path( src, dst ); - if( targets.empty() ) { - idx = -1; + // This chunk of code handles shifting the aim point around + // at maximum range when using circular distance. + // The range > 1 check ensures that you can always at least hit adjacent squares. + if( trigdist && range > 1 && round( trig_dist( src, dst ) ) > range ) { + bool cont = true; + tripoint cp = dst; + for( size_t i = 0; i < ret.size() && cont; i++ ) { + if( round( trig_dist( src, ret[i] ) ) > range ) { + ret.resize( i ); + cont = false; + } else { + cp = ret[i]; + } + } + dst = cp; + } + tripoint center; + if( snap_to_target ) { + center = dst; + } else { + center = pc.pos() + pc.view_offset; + } + // Clear the target window. + for( int i = 1; i <= getmaxy( w_target ) - num_instruction_lines - 2; i++ ) { + // Clear width excluding borders. + for( int j = 1; j <= getmaxx( w_target ) - 2; j++ ) { + mvwputch( w_target, i, j, c_white, ' ' ); + } + } + g->draw_ter( center, true ); + int line_number = 1; + Creature *critter = g->critter_at( dst, true ); + const int relative_elevation = dst.z - pc.pos().z; + mvwprintz( w_target, line_number++, 1, c_light_green, _( "Casting: %s (Level %u)" ), casting.name(), + casting.get_level() ); + if( casting.energy_source() == hp_energy ) { + line_number += fold_and_print( w_target, line_number, 1, getmaxx( w_target ) - 2, c_light_gray, + _( "Cost: %s %s" ), casting.energy_cost_string( pc ), casting.energy_string() ); + } else { + line_number += fold_and_print( w_target, line_number, 1, getmaxx( w_target ) - 2, c_light_gray, + _( "Cost: %s %s (Current: %s)" ), casting.energy_cost_string( pc ), casting.energy_string(), + casting.energy_cur_string( pc ) ); + } + nc_color clr = c_light_gray; + print_colored_text( w_target, line_number++, 1, clr, clr, + casting.colorized_fail_percent( pc ) ); + if( dst != src ) { + // Only draw those tiles which are on current z-level + auto ret_this_zlevel = ret; + ret_this_zlevel.erase( std::remove_if( ret_this_zlevel.begin(), ret_this_zlevel.end(), + [¢er]( const tripoint & pt ) { + return pt.z != center.z; + } ), ret_this_zlevel.end() ); + // Only draw a highlighted trajectory if we can see the endpoint. + // Provides feedback to the player, and avoids leaking information + // about tiles they can't see. + g->draw_line( dst, center, ret_this_zlevel ); - if( pc.last_target_pos ) { + // Print to target window + mvwprintw( w_target, line_number++, 1, _( "Range: %d/%d Elevation: %d Targets: %d" ), + rl_dist( src, dst ), range, relative_elevation, t.size() ); - if( local_last_tgt_pos ) { - dst = *local_last_tgt_pos; - } - if( ( pc.last_target.expired() || !pc.sees( *pc.last_target.lock().get() ) ) && - pc.has_activity( activity_id( "ACT_AIM" ) ) ) { - //We lost our target. Stop auto aiming. - pc.cancel_activity(); - } + } else { + mvwprintw( w_target, line_number++, 1, _( "Range: %d Elevation: %d Targets: %d" ), range, + relative_elevation, t.size() ); + } + if( casting.aoe() > 0 ) { + nc_color color = c_light_gray; + if( casting.effect() == "projectile_attack" || casting.effect() == "target_attack" ) { + line_number += fold_and_print( w_target, line_number, 1, getmaxx( w_target ) - 2, color, + _( "Effective Spell Radius: %i%s" ), casting.aoe(), rl_dist( src, + dst ) <= casting.aoe() ? colorize( _( " WARNING! IN RANGE" ), c_red ) : "" ); + } else if( casting.effect() == "cone_attack" ) { + line_number += fold_and_print( w_target, line_number, 1, getmaxx( w_target ) - 2, color, + _( "Cone Arc: %i degrees" ), casting.aoe() ); + } else if( casting.effect() == "line_attack" ) { + line_number += fold_and_print( w_target, line_number, 1, getmaxx( w_target ) - 2, color, + _( "Line width: %i" ), casting.aoe() ); + } + } + mvwprintz( w_target, line_number++, 1, c_light_red, _( "Damage: %i" ), casting.damage() ); + line_number += fold_and_print( w_target, line_number, 1, getmaxx( w_target ) - 2, clr, + casting.description() ); + // Skip blank lines if we're short on space. + if( !compact ) { + line_number++; + } - } else { - auto adjacent = closest_tripoints_first( range, dst ); - const auto target_spot = std::find_if( adjacent.begin(), adjacent.end(), - [&pc]( const tripoint & pt ) { - return g->m.tr_at( pt ).id == tr_practice_target && pc.sees( pt ); - } ); - - if( target_spot != adjacent.end() ) { - dst = *target_spot; - } + if( critter && critter != &pc && pc.sees( *critter ) ) { + // The 12 is 2 for the border and 10 for aim bars. + // Just print the monster name if we're short on space. + int available_lines = compact ? 1 : ( height - num_instruction_lines - line_number - 12 ); + line_number = critter->print_info( w_target, line_number, available_lines, 1 ); + } else { + mvwputch( g->w_terrain, POSY + dst.y - center.y, POSX + dst.x - center.x, c_red, '*' ); + } + + wrefresh( g->w_terrain ); + draw_targeting_window( w_target, casting.name(), + TARGET_MODE_SPELL, ctxt, aim_types, tiny ); + wrefresh( w_target ); + + catacurses::refresh(); + + std::string action; + if( pc.activity.id() == activity_id( "ACT_AIM" ) && pc.activity.str_values[0] != "AIM" ) { + // If we're in 'aim and shoot' mode, + // skip retrieving input and go straight to the action. + action = pc.activity.str_values[0]; + } else { + action = ctxt.handle_input(); + } + // Clear the activity if any, we'll re-set it later if we need to. + pc.cancel_activity(); + + tripoint targ( 0, 0, 0 ); + cata::optional mouse_pos; + // Our coordinates will either be determined by coordinate input(mouse), + // by a direction key, or by the previous value. + if( action == "SELECT" && ( mouse_pos = ctxt.get_coordinates( g->w_terrain ) ) ) { + targ = *mouse_pos; + targ.x -= dst.x; + targ.y -= dst.y; + targ.z -= dst.z; + } else if( const cata::optional vec = ctxt.get_direction( action ) ) { + targ.x = vec->x; + targ.y = vec->y; + } else { + targ.x = 0; + targ.y = 0; + } + + if( g->m.has_zlevels() && ( action == "LEVEL_UP" || action == "LEVEL_DOWN" ) ) { + // Just determine our delta-z. + const int dz = action == "LEVEL_UP" ? 1 : -1; + + // Shift the view up or down accordingly. + // We need to clamp the offset, but it needs to be clamped such that + // the player position plus the offset is still in range, since the player + // might be at Z+10 and looking down to Z-10, which is an offset greater than + // OVERMAP_DEPTH or OVERMAP_HEIGHT + const int potential_result = pc.pos().z + pc.view_offset.z + dz; + if( potential_result <= OVERMAP_HEIGHT && potential_result >= -OVERMAP_DEPTH ) { + pc.view_offset.z += dz; } - return; + + // Set our cursor z to our view z. This accounts for cases where + // our view and our target are on different z-levels (e.g. when + // we cycle targets on different z-levels but do not have SNAP_TO_TARGET + // enabled). This will ensure that we don't just chase the cursor up or + // down, never catching up. + dst.z = clamp( pc.pos().z + pc.view_offset.z, -OVERMAP_DEPTH, OVERMAP_HEIGHT ); + + // We need to do a bunch of redrawing and cache updates since we're + // looking at a different z-level. + g->refresh_all(); } - std::sort( targets.begin(), targets.end(), [&]( const Creature * lhs, const Creature * rhs ) { - return rl_dist( lhs->pos(), pc.pos() ) < rl_dist( rhs->pos(), pc.pos() ); - } ); + /* More drawing to terrain */ + if( targ != tripoint_zero ) { + const Creature *critter = g->critter_at( dst, true ); + if( critter != nullptr ) { + g->draw_critter( *critter, center ); + } else if( g->m.pl_sees( dst, -1 ) ) { + g->m.drawsq( g->w_terrain, pc, dst, false, true, center ); + } else { + mvwputch( g->w_terrain, POSY, POSX, c_black, 'X' ); + } - // TODO: last_target should be member of target_handler - const auto iter = std::find( targets.begin(), targets.end(), pc.last_target.lock().get() ); + // constrain by range + dst.x = std::min( std::max( dst.x + targ.x, src.x - range ), src.x + range ); + dst.y = std::min( std::max( dst.y + targ.y, src.y - range ), src.y + range ); + dst.z = std::min( std::max( dst.z + targ.z, src.z - range ), src.z + range ); - if( iter != targets.end() ) { - idx = std::distance( targets.begin(), iter ); - dst = targets[ idx ]->pos(); + } else if( ( action == "PREV_TARGET" ) && ( target != -1 ) ) { + int newtarget = find_target( t, dst ) - 1; + if( newtarget < 0 ) { + newtarget = t.size() - 1; + } + dst = t[newtarget]->pos(); + } else if( ( action == "NEXT_TARGET" ) && ( target != -1 ) ) { + int newtarget = find_target( t, dst ) + 1; + if( newtarget == static_cast( t.size() ) ) { + newtarget = 0; + } + dst = t[newtarget]->pos(); + } else if( action == "FIRE" ) { + if( casting.damage() > 0 && !confirm_non_enemy_target( dst ) ) { + continue; + } + target = find_target( t, dst ); + break; + } else if( action == "CENTER" ) { + dst = src; + set_last_target( dst ); + ret.clear(); + } else if( action == "TOGGLE_SNAP_TO_TARGET" ) { + snap_to_target = !snap_to_target; + } else if( action == "QUIT" ) { // return empty vector (cancel) + ret.clear(); pc.last_target_pos = cata::nullopt; - } else { - idx = 0; - dst = local_last_tgt_pos ? *local_last_tgt_pos : targets[ 0 ]->pos(); - pc.last_target.reset(); + target = -1; + break; } - }; - update_targets( range, t, target, dst ); + // Make player's sprite flip to face the current target + if( dst.x > src.x ) { + g->u.facing = FD_RIGHT; + } else if( dst.x < src.x ) { + g->u.facing = FD_LEFT; + } + + } while( true ); + + pc.view_offset = old_offset; + + if( ret.empty() || ret.back() == pc.pos() ) { + return ret; + } + + set_last_target( ret.back() ); + + const auto lt_ptr = pc.last_target.lock(); + if( npc *const guy = dynamic_cast( lt_ptr.get() ) ) { + if( casting.damage() > 0 ) { + if( !guy->guaranteed_hostile() ) { + // TODO: get rid of this. Or combine it with effect_hit_by_player + guy->hit_by_player = true; // used for morale penalty + } + // TODO: should probably go into the on-hit code? + guy->make_angry(); + } + } else if( monster *const mon = dynamic_cast( lt_ptr.get() ) ) { + // TODO: get rid of this. Or move into the on-hit code? + mon->add_effect( effect_hit_by_player, 10_minutes ); + } + wrefresh( w_target ); + return ret; +} + +// TODO: Shunt redundant drawing code elsewhere +std::vector target_handler::target_ui( player &pc, target_mode mode, + item *relevant, int range, const itype *ammo, + const target_callback &on_mode_change, + const target_callback &on_ammo_change ) +{ + // TODO: this should return a reference to a static vector which is cleared on each call. + static const std::vector empty_result{}; + std::vector ret; + + int sight_dispersion = 0; + if( !relevant ) { + relevant = &pc.weapon; + } else { + sight_dispersion = pc.effective_dispersion( relevant->sight_dispersion() ); + } + + tripoint src = pc.pos(); + tripoint dst = pc.pos(); + + std::vector t; + int target = 0; + + update_targets( pc, range, t, target, src, dst ); double recoil_pc = pc.recoil; tripoint recoil_pos = dst; diff --git a/src/ranged.h b/src/ranged.h index 8160ceb1f516a..4808fb9d1d20b 100644 --- a/src/ranged.h +++ b/src/ranged.h @@ -3,9 +3,13 @@ #include #include +#include "string_id.h" class item; class player; +class spell; +class spell_type; +using spell_id = string_id; struct itype; struct tripoint; @@ -22,7 +26,8 @@ enum target_mode : int { TARGET_MODE_TURRET, TARGET_MODE_TURRET_MANUAL, TARGET_MODE_REACH, - TARGET_MODE_THROW_BLIND + TARGET_MODE_THROW_BLIND, + TARGET_MODE_SPELL }; // TODO: move callbacks to a new struct and define some constructors for ease of use @@ -64,6 +69,9 @@ class target_handler const itype *ammo = nullptr, const target_callback &on_mode_change = target_callback(), const target_callback &on_ammo_change = target_callback() ); + // magic version of target_ui + std::vector target_ui( spell_id sp ); + std::vector target_ui( spell &casting ); }; int range_with_even_chance_of_good_hit( int dispersion );