/**
 * Attempts to harm a creature with a projectile.
 *
 * @param source Pointer to the creature who shot the projectile.
 * @param attack A structure describing the attack and its results.
 */
void Creature::deal_projectile_attack( Creature *source, dealt_projectile_attack &attack )
{
    const double missed_by = attack.missed_by;
    if( missed_by >= 1.0 ) {
        // Total miss
        return;
    }

    const projectile &proj = attack.proj;
    dealt_damage_instance &dealt_dam = attack.dealt_dam;
    const auto &proj_effects = proj.proj_effects;

    const bool u_see_this = g->u.sees(*this);

    const int avoid_roll = dodge_roll();
    // Do dice(10, speed) instead of dice(speed, 10) because speed could potentially be > 10000
    const int diff_roll = dice( 10, proj.speed );
    // Partial dodge, capped at [0.0, 1.0], added to missed_by
    const double dodge_rescaled = avoid_roll / static_cast<double>( diff_roll );
    const double goodhit = missed_by + std::max( 0.0, std::min( 1.0, dodge_rescaled ) ) ;

    if( goodhit >= 1.0 ) {
        // "Avoid" rather than "dodge", because it includes removing self from the line of fire
        //  rather than just Matrix-style bullet dodging
        if( source != nullptr && g->u.sees( *source ) ) {
            add_msg_player_or_npc(
                m_warning,
                _("You avoid %s projectile!"),
                _("<npcname> avoids %s projectile."),
                source->disp_name(true).c_str() );
        } else {
            add_msg_player_or_npc(
                m_warning,
                _("You avoid an incoming projectile!"),
                _("<npcname> avoids an incoming projectile.") );
        }

        attack.missed_by = 1.0; // Arbitrary value
        return;
    }

    // Bounce applies whether it does damage or not.
    if( proj.proj_effects.count( "BOUNCE" ) ) {
        add_effect( effect_bounced, 1_turns );
    }

    body_part bp_hit;
    double hit_value = missed_by + rng_float(-0.5, 0.5);
    // Headshots considered elsewhere
    if( hit_value <= 0.4 ) {
        bp_hit = bp_torso;
    } else if (one_in(4)) {
        if( one_in(2)) {
            bp_hit = bp_leg_l;
        } else {
            bp_hit = bp_leg_r;
        }
    } else {
        if( one_in(2)) {
            bp_hit = bp_arm_l;
        } else {
            bp_hit = bp_arm_r;
        }
    }

    double damage_mult = 1.0;

    std::string message = "";
    game_message_type gmtSCTcolor = m_neutral;

    if( goodhit < accuracy_headshot ) {
        message = _("Headshot!");
        gmtSCTcolor = m_headshot;
        damage_mult *= rng_float(1.95, 2.05);
        bp_hit = bp_head; // headshot hits the head, of course

    } else if( goodhit < accuracy_critical ) {
        message = _("Critical!");
        gmtSCTcolor = m_critical;
        damage_mult *= rng_float(1.5, 2.0);

    } else if( goodhit < accuracy_goodhit ) {
        message = _("Good hit!");
        gmtSCTcolor = m_good;
        damage_mult *= rng_float(1, 1.5);

    } else if( goodhit < accuracy_standard ) {
        damage_mult *= rng_float(0.5, 1);

    } else if( goodhit < accuracy_grazing ) {
        message = _("Grazing hit.");
        gmtSCTcolor = m_grazing;
        damage_mult *= rng_float(0, .25);
    }

    if( source != nullptr && !message.empty() ) {
        source->add_msg_if_player(m_good, message.c_str());
    }

    attack.missed_by = goodhit;

    // copy it, since we're mutating
    damage_instance impact = proj.impact;
    if( damage_mult > 0.0f && proj_effects.count( "NO_DAMAGE_SCALING" ) ) {
        damage_mult = 1.0f;
    }

    impact.mult_damage(damage_mult);

    if( proj_effects.count( "NOGIB" ) > 0 ) {
        float dmg_ratio = (float)impact.total_damage() / get_hp_max( player::bp_to_hp( bp_hit ) );
        if( dmg_ratio > 1.25f ) {
            impact.mult_damage( 1.0f / dmg_ratio );
        }
    }

    dealt_dam = deal_damage(source, bp_hit, impact);
    dealt_dam.bp_hit = bp_hit;

    // Apply ammo effects to target.
    if (proj.proj_effects.count("FLAME")) {
        if (made_of( material_id( "veggy" ) ) || made_of( material_id( "cotton" ) ) ||
            made_of( material_id( "wool" ) ) || made_of( material_id( "paper" ) ) ||
            made_of( material_id( "wood" ) ) ) {
            add_effect( effect_onfire, rng( 8_turns, 20_turns ), bp_hit );
        } else if (made_of( material_id( "flesh" ) ) || made_of( material_id( "iflesh" ) ) ) {
            add_effect( effect_onfire, rng( 5_turns, 10_turns ), bp_hit );
        }
    } else if (proj.proj_effects.count("INCENDIARY") ) {
        if (made_of( material_id( "veggy" ) ) || made_of( material_id( "cotton" ) ) ||
            made_of( material_id( "wool" ) ) || made_of( material_id( "paper" ) ) ||
            made_of( material_id( "wood" ) ) ) {
            add_effect( effect_onfire, rng( 2_turns, 6_turns ), bp_hit );
        } else if ( (made_of( material_id( "flesh" ) ) || made_of( material_id( "iflesh" ) ) ) &&
                    one_in(4) ) {
            add_effect( effect_onfire, rng( 1_turns, 4_turns ), bp_hit );
        }
    } else if (proj.proj_effects.count("IGNITE")) {
        if (made_of( material_id( "veggy" ) ) || made_of( material_id( "cotton" ) ) ||
            made_of( material_id( "wool" ) ) || made_of( material_id( "paper" ) ) ||
            made_of( material_id( "wood" ) ) ) {
            add_effect( effect_onfire, 6_turns, bp_hit );
        } else if (made_of( material_id( "flesh" ) ) || made_of( material_id( "iflesh" ) ) ) {
            add_effect( effect_onfire, 10_turns, bp_hit );
        }
    }

    if( bp_hit == bp_head && proj_effects.count( "BLINDS_EYES" ) ) {
        // TODO: Change this to require bp_eyes
        add_env_effect( effect_blind, bp_eyes, 5, rng( 3_turns, 10_turns ) );
    }

    if( proj_effects.count( "APPLY_SAP" ) ) {
        add_effect( effect_sap, 1_turns * dealt_dam.total_damage() );
    }

    int stun_strength = 0;
    if (proj.proj_effects.count("BEANBAG")) {
        stun_strength = 4;
    }
    if (proj.proj_effects.count("LARGE_BEANBAG")) {
        stun_strength = 16;
    }
    if( stun_strength > 0 ) {
        switch( get_size() ) {
        case MS_TINY:
            stun_strength *= 4;
            break;
        case MS_SMALL:
            stun_strength *= 2;
            break;
        case MS_MEDIUM:
        default:
            break;
        case MS_LARGE:
            stun_strength /= 2;
            break;
        case MS_HUGE:
            stun_strength /= 4;
            break;
        }
        add_effect( effect_stunned, 1_turns * rng( stun_strength / 2, stun_strength ) );
    }

    if(u_see_this) {
        if( damage_mult == 0 ) {
            if( source != nullptr ) {
                add_msg( source->is_player() ? _("You miss!") : _("The shot misses!") );
            }
        } else if( dealt_dam.total_damage() == 0 ) {
            //~ 1$ - monster name, 2$ - character's bodypart or monster's skin/armor
            add_msg( _("The shot reflects off %1$s %2$s!"), disp_name(true).c_str(),
                     is_monster() ?
                        skin_name().c_str() :
                        body_part_name_accusative(bp_hit).c_str() );
        } else if( is_player() ) {
                //monster hits player ranged
                //~ Hit message. 1$s is bodypart name in accusative. 2$d is damage value.
                add_msg_if_player(m_bad, _( "You were hit in the %1$s for %2$d damage." ),
                                  body_part_name_accusative(bp_hit).c_str(),
                                  dealt_dam.total_damage());
        } else if( source != nullptr ) {
            if( source->is_player() ) {
                //player hits monster ranged
                SCT.add(posx(), posy(),
                        direction_from(0, 0, posx() - source->posx(), posy() - source->posy()),
                        get_hp_bar(dealt_dam.total_damage(), get_hp_max(), true).first,
                        m_good, message, gmtSCTcolor);

                if (get_hp() > 0) {
                    SCT.add(posx(), posy(),
                            direction_from(0, 0, posx() - source->posx(), posy() - source->posy()),
                            get_hp_bar(get_hp(), get_hp_max(), true).first, m_good,
                            //~ "hit points", used in scrolling combat text
                            _("hp"), m_neutral, "hp");
                } else {
                    SCT.removeCreatureHP();
                }

                add_msg(m_good, _("You hit %s for %d damage."),
                        disp_name().c_str(), dealt_dam.total_damage());
            } else if( u_see_this ) {
                //~ 1$ - shooter, 2$ - target
                add_msg(_("%1$s shoots %2$s."),
                        source->disp_name().c_str(), disp_name().c_str());
            }
        }
    }

    check_dead_state();
    attack.hit_critter = this;
    attack.missed_by = goodhit;
}
示例#2
0
/**
 * Attempts to harm a creature with a projectile.
 *
 * @param source Pointer to the creature who shot the projectile.
 * @param missed_by Deviation of the projectile.
 * @param proj Reference to the projectile hitting the creature.
 * @param dealt_dam A reference storing the damage dealt.
 * @return 0 signals that the projectile should stop,
 *         1 signals that the projectile should not stop (i.e. dodged, passed through).
 */
int Creature::deal_projectile_attack(Creature *source, double missed_by,
                                     const projectile &proj, dealt_damage_instance &dealt_dam)
{
    bool u_see_this = g->u.sees(*this);
    body_part bp_hit;

    // do 10,speed because speed could potentially be > 10000
    if (dodge_roll() >= dice(10, proj.speed)) {
        if (is_player())
            add_msg(_("You dodge %s projectile!"),
                    source->disp_name(true).c_str());
        else if (u_see_this)
            add_msg(_("%s dodges %s projectile."),
                    disp_name().c_str(), source->disp_name(true).c_str());
        return 1;
    }

    // Bounce applies whether it does damage or not.
    if (proj.proj_effects.count("BOUNCE")) {
        add_effect("bounced", 1);
    }

    double hit_value = missed_by + rng_float(-0.5, 0.5);
    // headshots considered elsewhere
    if (hit_value <= 0.4) {
        bp_hit = bp_torso;
    } else if (one_in(4)) {
        if( one_in(2)) {
            bp_hit = bp_leg_l;
        } else {
            bp_hit = bp_leg_r;
        }
    } else {
        if( one_in(2)) {
            bp_hit = bp_arm_l;
        } else {
            bp_hit = bp_arm_r;
        }
    }

    double monster_speed_penalty = std::max(double(get_speed()) / 80., 1.0);
    double goodhit = missed_by / monster_speed_penalty;
    double damage_mult = 1.0;

    std::string message = "";
    game_message_type gmtSCTcolor = m_neutral;

    if (goodhit <= .1) {
        message = _("Headshot!");
        source->add_msg_if_player(m_good, message.c_str());
        gmtSCTcolor = m_headshot;
        damage_mult *= rng_float(2.45, 3.35);
        bp_hit = bp_head; // headshot hits the head, of course
    } else if (goodhit <= .2) {
        message = _("Critical!");
        source->add_msg_if_player(m_good, message.c_str());
        gmtSCTcolor = m_critical;
        damage_mult *= rng_float(1.75, 2.3);
    } else if (goodhit <= .4) {
        message = _("Good hit!");
        source->add_msg_if_player(m_good, message.c_str());
        gmtSCTcolor = m_good;
        damage_mult *= rng_float(1, 1.5);
    } else if (goodhit <= .6) {
        damage_mult *= rng_float(0.5, 1);
    } else if (goodhit <= .8) {
        message = _("Grazing hit.");
        source->add_msg_if_player(m_good, message.c_str());
        gmtSCTcolor = m_grazing;
        damage_mult *= rng_float(0, .25);
    } else {
        damage_mult *= 0;
    }

    // copy it, since we're mutating
    damage_instance impact = proj.impact;
    if( item(proj.ammo->id, 0).has_flag("NOGIB") ) {
        impact.add_effect("NOGIB");
    }
    impact.mult_damage(damage_mult);

    dealt_dam = deal_damage(source, bp_hit, impact);
    dealt_dam.bp_hit = bp_hit;

    // Apply ammo effects to target.
    const std::string target_material = get_material();
    if (proj.proj_effects.count("FLAME")) {
        if (0 == target_material.compare("veggy") || 0 == target_material.compare("cotton") ||
            0 == target_material.compare("wool") || 0 == target_material.compare("paper") ||
            0 == target_material.compare("wood" ) ) {
            add_effect("onfire", rng(8, 20));
        } else if (0 == target_material.compare("flesh") || 0 == target_material.compare("iflesh") ) {
            add_effect("onfire", rng(5, 10));
        }
    } else if (proj.proj_effects.count("INCENDIARY") ) {
        if (0 == target_material.compare("veggy") || 0 == target_material.compare("cotton") ||
            0 == target_material.compare("wool") || 0 == target_material.compare("paper") ||
            0 == target_material.compare("wood") ) {
            add_effect("onfire", rng(2, 6));
        } else if ( (0 == target_material.compare("flesh") || 0 == target_material.compare("iflesh") ) &&
                    one_in(4) ) {
            add_effect("onfire", rng(1, 4));
        }
    } else if (proj.proj_effects.count("IGNITE")) {
        if (0 == target_material.compare("veggy") || 0 == target_material.compare("cotton") ||
            0 == target_material.compare("wool") || 0 == target_material.compare("paper") ||
            0 == target_material.compare("wood") ) {
            add_effect("onfire", rng(6, 6));
        } else if (0 == target_material.compare("flesh") || 0 == target_material.compare("iflesh") ) {
            add_effect("onfire", rng(10, 10));
        }
    }
    int stun_strength = 0;
    if (proj.proj_effects.count("BEANBAG")) {
        stun_strength = 4;
    }
    if(proj.proj_effects.count("WHIP")) {
        stun_strength = rng(4, 10);
    }
    if (proj.proj_effects.count("LARGE_BEANBAG")) {
        stun_strength = 16;
    }
    if( stun_strength > 0 ) {
        switch( get_size() ) {
        case MS_TINY:
            stun_strength *= 4;
            break;
        case MS_SMALL:
            stun_strength *= 2;
            break;
        case MS_MEDIUM:
        default:
            break;
        case MS_LARGE:
            stun_strength /= 2;
            break;
        case MS_HUGE:
            stun_strength /= 4;
            break;
        }
        add_effect( "stunned", rng(stun_strength / 2, stun_strength) );
    }

    if(u_see_this) {
        if (damage_mult == 0) {
            if(source != NULL) {
                add_msg(source->is_player() ? _("You miss!") : _("The shot misses!"));
            }
        } else if (dealt_dam.total_damage() == 0) {
            add_msg(_("The shot reflects off %s %s!"), disp_name(true).c_str(),
                    skin_name().c_str());
        } else if (source != NULL) {
            if (source->is_player()) {
                //player hits monster ranged
                SCT.add(posx(), posy(),
                        direction_from(0, 0, posx() - source->posx(), posy() - source->posy()),
                        get_hp_bar(dealt_dam.total_damage(), get_hp_max(), true).first,
                        m_good, message, gmtSCTcolor);

                if (get_hp() > 0) {
                    SCT.add(posx(), posy(),
                            direction_from(0, 0, posx() - source->posx(), posy() - source->posy()),
                            get_hp_bar(get_hp(), get_hp_max(), true).first, m_good,
                            //~ “hit points”, used in scrolling combat text
                            _("hp"), m_neutral, "hp");
                } else {
                    SCT.removeCreatureHP();
                }

                add_msg(m_good, _("You hit %s for %d damage."),
                        disp_name().c_str(), dealt_dam.total_damage());

            } else if(this->is_player()) {
                //monster hits player ranged
                //~ Hit message. 1$s is bodypart name in accusative. 2$d is damage value.
                add_msg_if_player(m_bad, _( "You were hit in the %1$s for %2$d damage." ),
                                  body_part_name_accusative(bp_hit).c_str( ),
                                  dealt_dam.total_damage());
            } else if( u_see_this ) {
                add_msg(_("%s shoots %s."),
                        source->disp_name().c_str(), disp_name().c_str());
            }
        }
    }
    check_dead_state();
    return 0;
}
示例#3
0
hp_part Character::body_window( const std::string &menu_header,
                                bool show_all, bool precise,
                                int normal_bonus, int head_bonus, int torso_bonus,
                                bool bleed, bool bite, bool infect ) const
{
    WINDOW *hp_window = newwin(10, 31, (TERMY - 10) / 2, (TERMX - 31) / 2);
    draw_border(hp_window);

    trim_and_print( hp_window, 1, 1, getmaxx(hp_window) - 2, c_ltred, menu_header.c_str() );
    const int y_off = 2; // 1 for border, 1 for header

    /* This struct estabiles some kind of connection between the hp_part (which can be healed and
     * have HP) and the body_part. Note that there are more body_parts than hp_parts. For example:
     * Damage to bp_head, bp_eyes and bp_mouth is all applied on the HP of hp_head. */
    struct healable_bp {
        mutable bool allowed;
        body_part bp;
        hp_part hp;
        std::string name; // Translated name as it appears in the menu.
        int bonus;
    };
    /* The array of the menu entries show to the player. The entries are displayed in this order,
     * it may be changed here. */
    std::array<healable_bp, num_hp_parts> parts = { {
        { false, bp_head, hp_head, _("Head"), head_bonus },
        { false, bp_torso, hp_torso, _("Torso"), torso_bonus },
        { false, bp_arm_l, hp_arm_l, _("Left Arm"), normal_bonus },
        { false, bp_arm_r, hp_arm_r, _("Right Arm"), normal_bonus },
        { false, bp_leg_l, hp_leg_l, _("Left Leg"), normal_bonus },
        { false, bp_leg_r, hp_leg_r, _("Right Leg"), normal_bonus },
    } };

    for( size_t i = 0; i < parts.size(); i++ ) {
        const auto &e = parts[i];
        const body_part bp = e.bp;
        const hp_part hp = e.hp;
        const int maximal_hp = hp_max[hp];
        const int current_hp = hp_cur[hp];
        const int bonus = e.bonus;
        // This will c_ltgray if the part does not have any effects cured by the item
        // (e.g. it cures only bites, but the part does not have a bite effect)
        const nc_color state_col = limb_color( bp, bleed, bite, infect );
        const bool has_curable_effect = state_col != c_ltgray;
        // The same as in the main UI sidebar. Independent of the capability of the healing item!
        const nc_color all_state_col = limb_color( bp, true, true, true );
        const bool has_any_effect = all_state_col != c_ltgray;
        // Broken means no HP can be restored, it requires surgical attention.
        const bool limb_is_broken = current_hp == 0;
        // This considers only the effects that can *not* be removed.
        const nc_color new_state_col = limb_color( bp, !bleed, !bite, !infect );

        if( show_all ) {
            e.allowed = true;
        } else if( has_curable_effect ) {
            e.allowed = true;
        } else if( limb_is_broken ) {
            continue;
        } else if( current_hp < maximal_hp && e.bonus != 0 ) {
            e.allowed = true;
        } else {
            continue;
        }

        const int line = i + y_off;

        const nc_color color = show_all ? c_green : state_col;
        mvwprintz( hp_window, line, 1, color, "%d: %s", i + 1, e.name.c_str() );

        const auto print_hp = [&]( const int x, const nc_color col, const int hp ) {
            const auto bar = get_hp_bar( hp, maximal_hp, false );
            if( hp == 0 ) {
                mvwprintz( hp_window, line, x, col, "-----" );
            } else if( precise ) {
                mvwprintz( hp_window, line, x, col, "%5d", hp );
            } else {
                mvwprintz( hp_window, line, x, col, bar.first.c_str() );
            }
        };

        if( !limb_is_broken ) {
            // Drop the bar color, use the state color instead
            const nc_color color = has_any_effect ? all_state_col : c_green;
            print_hp( 15, color, current_hp );
        } else {
            // But still could be infected or bleeding
            const nc_color color = has_any_effect ? all_state_col : c_dkgray;
            print_hp( 15, color, 0 );
        }

        if( !limb_is_broken ) {
            const int new_hp = std::max( 0, std::min( maximal_hp, current_hp + bonus ) );

            if( new_hp == current_hp && !has_curable_effect ) {
                // Nothing would change
                continue;
            }

            mvwprintz( hp_window, line, 20, c_dkgray, " -> " );

            const nc_color color = has_any_effect ? new_state_col : c_green;
            print_hp( 24, color, new_hp );
        } else {
            const nc_color color = has_any_effect ? new_state_col : c_dkgray;
            mvwprintz( hp_window, line, 20, c_dkgray, " -> " );
            print_hp( 24, color, 0 );
        }
    }
    mvwprintz( hp_window, parts.size() + y_off, 1, c_ltgray, _("%d: Exit"), parts.size() + 1 );

    wrefresh(hp_window);
    char ch;
    hp_part healed_part = num_hp_parts;
    do {
        ch = getch();
        const size_t index = ch - '1';
        if( index < parts.size() && parts[index].allowed ) {
            healed_part = parts[index].hp;
            break;
        } else if( index == parts.size() || ch == KEY_ESCAPE) {
            healed_part = num_hp_parts;
            break;
        }
    } while (ch < '1' || ch > '7');
    werase(hp_window);
    wrefresh(hp_window);
    delwin(hp_window);
    refresh();

    return healed_part;
}
示例#4
0
hp_part Character::body_window( const std::string &menu_header,
                                bool show_all, bool precise,
                                int normal_bonus, int head_bonus, int torso_bonus,
                                int bleed, int bite, int infect ) const
{
    WINDOW *hp_window = newwin(10, 31, (TERMY - 10) / 2, (TERMX - 31) / 2);
    draw_border(hp_window);

    trim_and_print( hp_window, 1, 1, getmaxx(hp_window) - 2, c_ltred, menu_header.c_str() );
    nc_color color = c_ltgray;
    bool allowed_result[num_hp_parts] = { false };

    const auto check_part = [&]( hp_part part, std::string part_name,
                                 int heal_val, int line_num ) {
        body_part bp = player::hp_to_bp( part );
        if( show_all ||
            hp_cur[part] < hp_max[part] ||
            has_effect("infected", bp) ||
            has_effect("bite", bp) ||
            has_effect("bleed", bp) ) {
            nc_color color = show_all ? c_green :
                limb_color( bp, bleed, bite, infect );
            if( color != c_ltgray || heal_val != 0 ) {
                mvwprintz( hp_window, line_num, 1, color, part_name.c_str() );
                allowed_result[part] = true;
            }
        }
    };

    check_part( hp_head,  _("1: Head"),      head_bonus,   2 );
    check_part( hp_torso, _("2: Torso"),     torso_bonus,  3 );
    check_part( hp_arm_l, _("3: Left Arm"),  normal_bonus, 4 );
    check_part( hp_arm_r, _("4: Right Arm"), normal_bonus, 5 );
    check_part( hp_leg_l, _("5: Left Leg"),  normal_bonus, 6 );
    check_part( hp_leg_r, _("6: Right Leg"), normal_bonus, 7 );
    mvwprintz( hp_window, 8, 1, c_ltgray, _("7: Exit") );
    std::string health_bar;
    for( int i = 0; i < num_hp_parts; i++ ) {
        if( !allowed_result[i] ) {
            continue;
        }

        body_part bp = body_part( i );

        // Have printed the name of the body part, can select it
        int current_hp = hp_cur[i];
        if( current_hp != 0 ) {
            std::tie( health_bar, color ) = get_hp_bar(current_hp, hp_max[i], false);
            // Drop the bar color, use the state color instead
            const nc_color state_col = limb_color( bp, true, true, true );
            color = state_col != c_ltgray ? state_col : c_green;
            if( precise ) {
                mvwprintz(hp_window, i + 2, 15, color, "%5d", current_hp);
            } else {
                mvwprintz(hp_window, i + 2, 15, color, health_bar.c_str());
            }
        } else {
            // curhp is 0; requires surgical attention
            // But still could be infected or bleeding
            const nc_color state_col = limb_color( bp, true, true, true );
            color = state_col != c_ltgray ? state_col : c_dkgray;
            mvwprintz(hp_window, i + 2, 15, color, "-----");
        }

        if( current_hp != 0 ) {
            switch( hp_part( i ) ) {
                case hp_head:
                    current_hp += head_bonus;
                    break;
                case hp_torso:
                    current_hp += torso_bonus;
                    break;
                default:
                    current_hp += normal_bonus;
                    break;
            }

            if( current_hp > hp_max[i] ) {
                current_hp = hp_max[i];
            } else if (current_hp < 0) {
                current_hp = 0;
            }

            if( current_hp == hp_cur[i] &&
                ( infect <= 0 || !has_effect( "infected", bp ) ) &&
                ( bite <= 0 || !has_effect( "bite", bp ) ) &&
                ( bleed <= 0 || !has_effect( "bleed", bp ) ) ) {
                // Nothing would change
                continue;
            }

            mvwprintz( hp_window, i + 2, 20, c_dkgray, " -> " );
            std::tie( health_bar, color ) = get_hp_bar( current_hp, hp_max[i], false );
            
            const nc_color state_col = limb_color( bp, bleed > 0, bite > 0, infect > 0 );
            color = state_col != c_ltgray ? state_col : c_green;
            if( precise ) {
                mvwprintz( hp_window, i + 2, 24, color, "%5d", current_hp );
            } else {
                mvwprintz( hp_window, i + 2, 24, color, health_bar.c_str() );
            }
        } else {
            // curhp is 0; requires surgical attention
            const nc_color state_col = limb_color( bp, bleed > 0, bite > 0, infect > 0 );
            color = state_col != c_ltgray ? state_col : c_dkgray;
            mvwprintz(hp_window, i + 2, 24, color, "-----");
        }
    }
    wrefresh(hp_window);
    char ch;
    hp_part healed_part = num_hp_parts;
    do {
        ch = getch();
        if (ch == '1') {
            healed_part = hp_head;
        } else if (ch == '2') {
            healed_part = hp_torso;
        } else if (ch == '3') {
            healed_part = hp_arm_l;
        } else if (ch == '4') {
            healed_part = hp_arm_r;
        } else if (ch == '5') {
            healed_part = hp_leg_l;
        } else if (ch == '6') {
            healed_part = hp_leg_r;
        } else if (ch == '7' || ch == KEY_ESCAPE) {
            healed_part = num_hp_parts;
            break;
        }
    } while (ch < '1' || ch > '7');
    werase(hp_window);
    wrefresh(hp_window);
    delwin(hp_window);
    refresh();

    return healed_part;
}