/* * File: fight.cc * Summary: Functions used during combat. * Written by: Linley Henzell */ #include "AppHdr.h" #include "fight.h" #include #include #include #include #include "externs.h" #include "areas.h" #include "artefact.h" #include "attitude-change.h" #include "beam.h" #include "cloud.h" #include "coordit.h" #include "database.h" #include "debug.h" #include "delay.h" #include "directn.h" #include "effects.h" #include "env.h" #include "map_knowledge.h" #include "fprop.h" #include "food.h" #include "goditem.h" #include "invent.h" #include "items.h" #include "itemname.h" #include "itemprop.h" #include "item_use.h" #include "kills.h" #include "macro.h" #include "makeitem.h" #include "message.h" #include "misc.h" #include "mon-behv.h" #include "mon-cast.h" #include "mon-place.h" #include "terrain.h" #include "mgen_data.h" #include "coord.h" #include "mon-stuff.h" #include "mon-util.h" #include "mutation.h" #include "ouch.h" #include "player.h" #include "religion.h" #include "shopping.h" #include "skills.h" #include "spells1.h" #include "spells3.h" #include "spl-mis.h" #include "spl-util.h" #include "state.h" #include "stuff.h" #include "transform.h" #include "traps.h" #include "travel.h" #include "tutorial.h" #include "view.h" #include "shout.h" #include "xom.h" #ifdef NOTE_DEBUG_CHAOS_BRAND #define NOTE_DEBUG_CHAOS_EFFECTS #endif #ifdef NOTE_DEBUG_CHAOS_EFFECTS #include "notes.h" #endif const int HIT_WEAK = 7; const int HIT_MED = 18; const int HIT_STRONG = 36; static void stab_message(actor *defender, int stab_bonus); static inline int player_weapon_str_weight( void ); static inline int player_weapon_dex_weight( void ); static inline int calc_stat_to_hit_base( void ); static inline int calc_stat_to_dam_base( void ); static void mons_lose_attack_energy(monsters *attacker, int wpn_speed, int which_attack, int effective_attack); /* ************************************************** * * * BEGIN PUBLIC FUNCTIONS * * * ************************************************** */ // This function is only used when monsters are attacking. bool test_melee_hit(int to_hit, int ev, defer_rand& r) { int roll = -1; int margin = AUTOMATIC_HIT; ev *= 2; if (to_hit >= AUTOMATIC_HIT) return (true); else if (r[0].x_chance_in_y(MIN_HIT_MISS_PERCENTAGE, 100)) margin = (r[1].random2(2) ? 1 : -1) * AUTOMATIC_HIT; else { roll = r[2].random2( to_hit + 1 ); margin = (roll - r[3].random2avg(ev, 2)); } #if DEBUG_DIAGNOSTICS float miss; if (to_hit < ev) miss = 100.0 - MIN_HIT_MISS_PERCENTAGE / 2.0; else { miss = MIN_HIT_MISS_PERCENTAGE / 2.0 + ((100.0 - MIN_HIT_MISS_PERCENTAGE) * ev) / to_hit; } mprf( MSGCH_DIAGNOSTICS, "to hit: %d; ev: %d; miss: %0.2f%%; roll: %d; result: %s%s (%d)", to_hit, ev, miss, roll, (margin >= 0) ? "hit" : "miss", (roll == -1) ? "!!!" : "", margin ); #endif return (margin >= 0); } // This function returns the "extra" stats the player gets because of // choice of weapon... it's used only for giving warnings when a player // wields a less than ideal weapon. int effective_stat_bonus( int wepType ) { #ifdef USE_NEW_COMBAT_STATS int str_weight; if (wepType == -1) str_weight = player_weapon_str_weight(); else str_weight = weapon_str_weight( OBJ_WEAPONS, wepType ); return ((you.strength - you.dex) * (str_weight - 5) / 10); #else return (0); #endif } // Returns the to-hit for your extra unarmed attacks. // DOES NOT do the final roll (i.e., random2(your_to_hit)). static int calc_your_to_hit_unarmed(int uattack = UNAT_NO_ATTACK, bool vampiric = false) { int your_to_hit; your_to_hit = 13 + you.dex / 2 + you.skills[SK_UNARMED_COMBAT] / 2 + you.skills[SK_FIGHTING] / 5; if (wearing_amulet(AMU_INACCURACY)) your_to_hit -= 5; // Vampires know how to bite and aim better when thirsty. if (you.species == SP_VAMPIRE && uattack == UNAT_BITE) { your_to_hit += 1; if (vampiric) { if (you.hunger_state == HS_STARVING) your_to_hit += 2; else if (you.hunger_state < HS_SATIATED) your_to_hit += 1; } } else if (you.species != SP_VAMPIRE && you.hunger_state == HS_STARVING) your_to_hit -= 3; your_to_hit += slaying_bonus(PWPN_HIT); return your_to_hit; } // Calculates your to-hit roll. If random_factor is true, be stochastic; // if false, determinstic (e.g. for chardumps). int calc_your_to_hit( bool random_factor ) { melee_attack attk(&you, NULL); return attk.calc_to_hit(random_factor); } // Calculates your heavy armour penalty. If random_factor is true, // be stochastic; if false, deterministic (e.g. for chardumps.) int calc_heavy_armour_penalty( bool random_factor ) { const bool ur_armed = (you.weapon() != NULL); int heavy_armour = 0; // Heavy armour modifiers for shield borne. if (player_wearing_slot(EQ_SHIELD)) { switch (you.shield()->sub_type) { case ARM_SHIELD: if (you.skills[SK_SHIELDS] < maybe_random2(7, random_factor)) heavy_armour++; break; case ARM_LARGE_SHIELD: if (player_genus(GENPC_OGRE) || you.species == SP_TROLL || player_genus(GENPC_DRACONIAN)) { if (you.skills[SK_SHIELDS] < maybe_random2(13, random_factor)) heavy_armour++; // was potentially "+= 3" {dlb} } else { for (int i = 0; i < 3; i++) { if (you.skills[SK_SHIELDS] < maybe_random2(13, random_factor)) { heavy_armour += maybe_random2(3, random_factor); } } } break; default: break; } } // Heavy armour modifiers for PARM_EVASION. if (player_wearing_slot(EQ_BODY_ARMOUR)) { int ev_pen = property( you.inv[you.equip[EQ_BODY_ARMOUR]], PARM_EVASION ); // Wearing heavy armour in water is particularly cumbersome. if (you.species == SP_MERFOLK && grd(you.pos()) == DNGN_DEEP_WATER && you.swimming()) { ev_pen *= 2; } if (ev_pen < 0 && maybe_random2(you.skills[SK_ARMOUR], random_factor) < abs(ev_pen)) { heavy_armour += maybe_random2( abs(ev_pen), random_factor ); } } // ??? what is the reasoning behind this ??? {dlb} // My guess is that its supposed to encourage monk-style play -- bwr if (!ur_armed && heavy_armour) { if (random_factor) { heavy_armour *= (coinflip() ? 3 : 2); } else { // avg. value: (2+3)/2 heavy_armour *= 5; heavy_armour /= 2; } } return (heavy_armour); } static bool player_fights_well_unarmed(int heavy_armour_penalty) { return (you.burden_state == BS_UNENCUMBERED && x_chance_in_y(you.skills[SK_UNARMED_COMBAT], 20) && x_chance_in_y(2, 1 + heavy_armour_penalty)); } unchivalric_attack_type is_unchivalric_attack(const actor *attacker, const actor *defender) { const monsters *def = (defender->atype() == ACT_PLAYER ? NULL : dynamic_cast(defender)); unchivalric_attack_type unchivalric = UCAT_NO_ATTACK; // No unchivalric attacks on monsters that cannot fight (e.g. // plants) or monsters the attacker can't see (either due to // invisibility or being behind opaque clouds). if (defender->cannot_fight() || (attacker && !attacker->can_see(defender))) return (unchivalric); // Distracted (but not batty); this only applies to players. if (attacker && attacker->atype() == ACT_PLAYER && def && def->foe != MHITYOU && !mons_is_batty(def)) { unchivalric = UCAT_DISTRACTED; } // confused (but not perma-confused) if (def && def->has_ench(ENCH_CONFUSION) && !mons_class_flag(def->type, M_CONFUSED)) { unchivalric = UCAT_CONFUSED; } // fleeing if (def && mons_is_fleeing(def)) unchivalric = UCAT_FLEEING; // invisible if (attacker && !attacker->visible_to(defender)) unchivalric = UCAT_INVISIBLE; // held in a net if (def && def->caught()) unchivalric = UCAT_HELD_IN_NET; // petrifying if (def && def->petrifying()) unchivalric = UCAT_PETRIFYING; // petrified if (defender->petrified()) unchivalric = UCAT_PETRIFIED; // paralysed if (defender->paralysed()) unchivalric = UCAT_PARALYSED; // sleeping if (defender->asleep()) unchivalric = UCAT_SLEEPING; return (unchivalric); } ////////////////////////////////////////////////////////////////////////// // Melee attack melee_attack::melee_attack(actor *attk, actor *defn, bool allow_unarmed, int which_attack) : attacker(attk), defender(defn), cancel_attack(false), did_hit(false), perceived_attack(false), obvious_effect(false), needs_message(false), attacker_visible(false), defender_visible(false), attacker_invisible(false), defender_invisible(false), defender_starting_attitude(ATT_HOSTILE), unarmed_ok(allow_unarmed), attack_number(which_attack), to_hit(0), damage_done(0), special_damage(0), aux_damage(0), stab_attempt(false), stab_bonus(0), min_delay(0), final_attack_delay(0), noise_factor(0), extra_noise(0), weapon(NULL), damage_brand(SPWPN_NORMAL), wpn_skill(SK_UNARMED_COMBAT), hands(HANDS_ONE), hand_half_bonus(false), art_props(0), unrand_entry(NULL), attack_verb("bug"), verb_degree(), no_damage_message(), special_damage_message(), unarmed_attack(), special_damage_flavour(BEAM_NONE), shield(NULL), defender_shield(NULL), heavy_armour_penalty(0), can_do_unarmed(false), water_attack(false), miscast_level(-1), miscast_type(SPTYP_NONE), miscast_target(NULL), final_effects() { init_attack(); } void melee_attack::check_hand_half_bonus_eligible() { hand_half_bonus = (unarmed_ok && !can_do_unarmed && !shield && weapon && !weapon->cursed() && hands == HANDS_HALF); } void melee_attack::init_attack() { weapon = attacker->weapon(attack_number); damage_brand = attacker->damage_brand(attack_number); if (weapon && weapon->base_type == OBJ_WEAPONS && is_artefact(*weapon)) { artefact_wpn_properties(*weapon, art_props); if (is_unrandom_artefact(*weapon)) unrand_entry = get_unrand_entry(weapon->special); } wpn_skill = weapon ? weapon_skill(*weapon) : SK_UNARMED_COMBAT; if (weapon) { hands = hands_reqd(*weapon, attacker->body_size()); switch (single_damage_type(*weapon)) { case DAM_BLUDGEON: case DAM_WHIP: noise_factor = 125; break; case DAM_SLICE: noise_factor = 100; break; case DAM_PIERCE: noise_factor = 75; break; } } shield = attacker->shield(); if (defender) defender_shield = defender->shield(); water_attack = is_water_attack(attacker, defender); attacker_visible = attacker->observable(); attacker_invisible = (!attacker_visible && you.see_cell(attacker->pos())); defender_visible = defender && defender->observable(); defender_invisible = (!defender_visible && defender && you.see_cell(defender->pos())); needs_message = (attacker_visible || defender_visible); if (defender && defender->atype() == ACT_MONSTER) defender_starting_attitude = defender_as_monster()->temp_attitude(); else { // Not really, but this is used for god conducts, so hostile is // fine. defender_starting_attitude = ATT_HOSTILE; } if (defender && defender->submerged()) { // Unarmed attacks from tentacles are the only ones that can // reach submerged monsters. unarmed_ok = (attacker->damage_type() == DVORP_TENTACLE); } miscast_level = -1; miscast_type = SPTYP_NONE; miscast_target = NULL; } std::string melee_attack::actor_name(const actor *a, description_level_type desc, bool actor_visible, bool actor_invisible) { return (actor_visible ? a->name(desc) : anon_name(desc, actor_invisible)); } std::string melee_attack::pronoun(const actor *a, pronoun_type pron, bool actor_visible) { return (actor_visible ? a->pronoun(pron) : anon_pronoun(pron)); } std::string melee_attack::anon_pronoun(pronoun_type pron) { switch (pron) { default: case PRONOUN_CAP: return "It"; case PRONOUN_NOCAP: return "it"; case PRONOUN_CAP_POSSESSIVE: return "Its"; case PRONOUN_NOCAP_POSSESSIVE: return "its"; case PRONOUN_REFLEXIVE: return "itself"; } } std::string melee_attack::anon_name(description_level_type desc, bool actor_invisible) { switch (desc) { case DESC_CAP_THE: case DESC_CAP_A: return (actor_invisible ? "It" : "Something"); case DESC_CAP_YOUR: return ("Its"); case DESC_NOCAP_YOUR: case DESC_NOCAP_ITS: return ("its"); case DESC_NONE: return (""); case DESC_NOCAP_THE: case DESC_NOCAP_A: case DESC_PLAIN: default: return (actor_invisible? "it" : "something"); } } std::string melee_attack::atk_name(description_level_type desc) const { return actor_name(attacker, desc, attacker_visible, attacker_invisible); } std::string melee_attack::def_name(description_level_type desc) const { return actor_name(defender, desc, defender_visible, defender_invisible); } std::string melee_attack::wep_name(description_level_type desc, unsigned long ignore_flags) const { ASSERT(weapon != NULL); if (attacker->atype() == ACT_PLAYER) return weapon->name(desc, false, false, false, false, ignore_flags); std::string name; bool possessive = false; if (desc == DESC_CAP_YOUR) { desc = DESC_CAP_THE; possessive = true; } else if (desc == DESC_NOCAP_YOUR) { desc = DESC_NOCAP_THE; possessive = true; } if (possessive) name = apostrophise(atk_name(desc)) + " "; name += weapon->name(DESC_PLAIN, false, false, false, false, ignore_flags); return (name); } bool melee_attack::is_banished(const actor *a) const { if (!a || a->alive()) return (false); if (a->atype() == ACT_PLAYER) return (you.banished); else return (dynamic_cast(a)->flags & MF_BANISHED); } bool melee_attack::is_water_attack(const actor *attk, const actor *defn) const { return (defn && attk->swimming() && defn->floundering()); } void melee_attack::check_autoberserk() { if (weapon && art_props[ARTP_ANGRY] >= 1 && !one_chance_in(1 + art_props[ARTP_ANGRY])) { attacker->go_berserk(false); } } bool melee_attack::check_unrand_effects(bool mondied) { // If bashing the defender with a wielded unrandart launcher, don't use // unrand_entry->fight_func, since that's the function used for // launched missiles. if (unrand_entry && unrand_entry->fight_func.melee_effects && weapon && fires_ammo_type(*weapon) == MI_NONE) { unrand_entry->fight_func.melee_effects(weapon, attacker, defender, mondied); return (!defender->alive()); } return (false); } void melee_attack::identify_mimic(actor *act) { if (act && act->atype() == ACT_MONSTER && mons_is_mimic(act->id()) && you.can_see(act)) { monsters* mon = dynamic_cast(act); mon->flags |= MF_KNOWN_MIMIC; } } bool melee_attack::attack() { // If a mimic is attacking or defending, it is thereafter known. identify_mimic(attacker); identify_mimic(defender); coord_def defender_pos = defender->pos(); if (attacker->atype() == ACT_PLAYER && defender->atype() == ACT_MONSTER) { if (stop_attack_prompt(defender_as_monster(), false, attacker->pos())) { cancel_attack = true; return (false); } } if (attacker != defender) { // Allow setting of your allies' target, etc. attacker->attacking(defender); check_autoberserk(); } // The attacker loses nutrition. attacker->make_hungry(3, true); // Xom thinks fumbles are funny... if (attacker->fumbles_attack()) { // Make sure the monster uses up some energy, even though // it didn't actually attack. attacker->lose_energy(EUT_ATTACK); // ... and thinks fumbling when trying to hit yourself is just // hilarious. if (attacker == defender) xom_is_stimulated(255); else xom_is_stimulated(14); if (damage_brand == SPWPN_CHAOS) chaos_affects_attacker(); return (false); } // Non-fumbled self-attacks due to confusion are still pretty funny, // though. else if (attacker == defender && attacker->confused()) { // And is still hilarious if it's the player. if (attacker->atype() == ACT_PLAYER) xom_is_stimulated(255); else xom_is_stimulated(128); } // Defending monster protects itself from attacks using the wall // it's in. if (defender->atype() == ACT_MONSTER && cell_is_solid(defender->pos()) && mons_wall_shielded(defender_as_monster())) { std::string feat_name = raw_feature_description(grd(defender->pos())); if (attacker->atype() == ACT_PLAYER) { player_apply_attack_delay(); if (you.can_see(defender)) { mprf("The %s protects %s from harm.", feat_name.c_str(), defender->name(DESC_NOCAP_THE).c_str()); } else { mprf("You hit the %s.", feat_name.c_str()); } } else if (you.can_see(attacker)) { // Make sure the monster uses up some energy, even though it // didn't actually land a blow. attacker->lose_energy(EUT_ATTACK); if (!mons_near(defender_as_monster())) { simple_monster_message(attacker_as_monster(), " hits something"); } else if (!you.can_see(attacker)) { mprf("%s hits the %s.", defender->name(DESC_CAP_THE).c_str(), feat_name.c_str()); } else { mprf("%s tries to hit the %s, but is blocked by the %s.", attacker->name(DESC_CAP_THE).c_str(), defender->name(DESC_NOCAP_THE).c_str(), feat_name.c_str()); } } if (damage_brand == SPWPN_CHAOS) chaos_affects_attacker(); return (true); } to_hit = calc_to_hit(); god_conduct_trigger conducts[3]; disable_attack_conducts(conducts); if (attacker->atype() == ACT_PLAYER && attacker != defender) set_attack_conducts(conducts, defender_as_monster()); // Trying to stay general beyond this point is a recipe for insanity. // Maybe when Stone Soup hits 1.0... :-) bool retval = ((attacker->atype() == ACT_PLAYER) ? player_attack() : (defender->atype() == ACT_PLAYER) ? mons_attack_you() : mons_attack_mons()); if (env.sanctuary_time > 0 && retval && !cancel_attack && attacker != defender && !attacker->confused()) { if (is_sanctuary(attacker->pos()) || is_sanctuary(defender->pos())) { if (attacker->atype() == ACT_PLAYER || attacker_as_monster()->friendly()) { remove_sanctuary(true); } } } if (attacker->atype() == ACT_PLAYER) { handle_noise(defender_pos); if (damage_brand == SPWPN_CHAOS) chaos_affects_attacker(); do_miscast(); } enable_attack_conducts(conducts); return (retval); } static int _modify_blood_amount(const int damage, const int dam_type) { int factor = 0; // DVORP_NONE switch (dam_type) { case DVORP_CRUSHING: // flails, also unarmed case DVORP_TENTACLE: // unarmed, tentacles factor = 2; break; case DVORP_SLASHING: // whips factor = 4; break; case DVORP_PIERCING: // pole-arms factor = 5; break; case DVORP_STABBING: // knives, daggers factor = 8; break; case DVORP_SLICING: // other short/long blades, also blade hands factor = 10; break; case DVORP_CHOPPING: // axes factor = 17; break; case DVORP_CLAWING: // unarmed, claws factor = 24; break; } return (damage * factor / 10); } static bool _vamp_wants_blood_from_monster(const monsters *mon) { if (you.species != SP_VAMPIRE) return (false); if (you.hunger_state == HS_ENGORGED) return (false); if (mon->is_summoned()) return (false); if (!mons_has_blood(mon->type)) return (false); const int chunk_type = mons_corpse_effect( mon->type ); // Don't drink poisonous or mutagenic blood. return (chunk_type == CE_CLEAN || chunk_type == CE_CONTAMINATED || chunk_type == CE_POISONOUS && player_res_poison()); } // Should life protection protect from this? // Called when stabbing, for bite attacks, and vampires wielding vampiric weapons // Returns true if blood was drawn. static bool _player_vampire_draws_blood(const monsters* mon, const int damage, bool needs_bite_msg = false, int reduction = 1) { ASSERT(you.species == SP_VAMPIRE); if (!_vamp_wants_blood_from_monster(mon)) return (false); const int chunk_type = mons_corpse_effect(mon->type); // Now print message, need biting unless already done (never for bat form!) if (needs_bite_msg && !player_in_bat_form()) { mprf( "You bite %s, and draw %s blood!", mon->name(DESC_NOCAP_THE, true).c_str(), mon->pronoun(PRONOUN_NOCAP_POSSESSIVE).c_str()); } else mprf( "You draw %s's blood!", mon->name(DESC_NOCAP_THE, true).c_str() ); // Regain hp. if (you.hp < you.hp_max) { int heal = 1 + random2(damage); if (heal > you.experience_level) heal = you.experience_level; if (chunk_type == CE_CLEAN) heal += 1 + random2(damage); // Decrease healing when done in bat form. if (player_in_bat_form()) heal /= 2; if (heal > 0) { inc_hp(heal, false); mprf("You feel %sbetter.", (you.hp == you.hp_max) ? "much " : ""); } } // Gain nutrition. if (you.hunger_state != HS_ENGORGED) { int food_value = 0; if (chunk_type == CE_CLEAN) food_value = 30 + random2avg(59, 2); else if (chunk_type == CE_CONTAMINATED || chunk_type == CE_POISONOUS) food_value = 15 + random2avg(29, 2); // Bats get a rather less nutrition out of it. if (player_in_bat_form()) food_value /= 2; food_value /= reduction; lessen_hunger(food_value, false); } did_god_conduct(DID_DRINK_BLOOD, 5 + random2(4)); return (true); } bool melee_attack::player_attack() { if (cancel_attack) return (false); noise_factor = 100; player_apply_attack_delay(); player_stab_check(); coord_def where = defender->pos(); if (player_hits_monster()) { did_hit = true; if (Tutorial.tutorial_left) Tutorial.tut_melee_counter++; const bool shield_blocked = attack_shield_blocked(true); if (shield_blocked) damage_done = 0; else { // This actually does more than calculate damage - it also // sets up messages, etc. player_calc_hit_damage(); } if (you.duration[DUR_SLIMIFY] && mon_can_be_slimified(defender_as_monster())) { // Bail out after sliming so we don't get aux unarmed and // attack a fellow slime. damage_done = 0; slimify_monster(defender_as_monster()); you.duration[DUR_SLIMIFY] = 0; return (true); } bool hit_woke_orc = false; if (you.religion == GOD_BEOGH && defender->mons_species() == MONS_ORC && !defender->is_summoned() && !defender_as_monster()->is_shapeshifter() && !player_under_penance() && you.piety >= piety_breakpoint(2) && mons_near(defender_as_monster()) && defender->asleep()) { hit_woke_orc = true; } // Always upset monster regardless of damage. // However, successful stabs inhibit shouting. behaviour_event(defender_as_monster(), ME_WHACK, MHITYOU, coord_def(), !stab_attempt); if (damage_done > 0 && defender->can_bleed() && !defender->is_summoned() && !defender->submerged()) { int blood = _modify_blood_amount(damage_done, attacker->damage_type()); if (blood > defender->stat_hp()) blood = defender->stat_hp(); bleed_onto_floor(where, defender->id(), blood, true); } if (damage_done > 0 || !defender_visible && !shield_blocked) player_announce_hit(); else if (!shield_blocked && damage_done <= 0) { no_damage_message = make_stringf("You %s %s.", attack_verb.c_str(), defender->name(DESC_NOCAP_THE).c_str()); } defender->hurt(&you, damage_done, special_damage_flavour, false); if (damage_done) player_exercise_combat_skills(); if (player_check_monster_died()) return (true); player_sustain_passive_damage(); // Thirsty stabbing vampires get to draw blood. if (you.species == SP_VAMPIRE && you.hunger_state < HS_SATIATED && stab_attempt && stab_bonus > 0) { _player_vampire_draws_blood(defender_as_monster(), damage_done, true); } // At this point, pretend we didn't hit at all. if (shield_blocked) did_hit = false; if (hit_woke_orc) { // Call function of orcs first noticing you, but with // beaten-up conversion messages (if applicable). beogh_follower_convert(defender_as_monster(), true); return (true); } } else player_warn_miss(); if (did_hit && player_monattk_hit_effects(false)) return (true); const bool did_primary_hit = did_hit; if (unarmed_ok && where == defender->pos() && player_aux_unarmed()) return (true); if ((did_primary_hit || did_hit) && defender->alive() && where == defender->pos()) { print_wounds(defender_as_monster()); } return (did_primary_hit || did_hit); } // Returns true to end the attack round. bool melee_attack::player_aux_unarmed() { unwind_var save_brand(damage_brand); damage_brand = SPWPN_NORMAL; int uattack = UNAT_NO_ATTACK; bool simple_miss_message = false; std::string miss_verb; coord_def defender_pos = defender->pos(); if (can_do_unarmed) { if (you.species == SP_NAGA) uattack = UNAT_HEADBUTT; else uattack = (coinflip() ? UNAT_HEADBUTT : UNAT_KICK); if (player_mutation_level(MUT_FANGS) || you.attribute[ATTR_TRANSFORMATION] == TRAN_DRAGON) { uattack = UNAT_BITE; } if ((you.attribute[ATTR_TRANSFORMATION] == TRAN_DRAGON || player_genus(GENPC_DRACONIAN) || (you.species == SP_MERFOLK && you.swimming()) || player_mutation_level(MUT_STINGER)) && one_chance_in(3)) { uattack = UNAT_TAILSLAP; } if (coinflip()) uattack = UNAT_PUNCH; if (you.species == SP_VAMPIRE && !one_chance_in(3)) uattack = UNAT_BITE; } for (int scount = 0; scount < 5; scount++) { noise_factor = 100; unarmed_attack.clear(); miss_verb.clear(); simple_miss_message = false; damage_brand = SPWPN_NORMAL; aux_damage = 0; switch (scount) { case 0: { if (uattack != UNAT_KICK) //jmf: hooves mutation { if (!player_mutation_level(MUT_HOOVES) && !player_mutation_level(MUT_TALONS) || coinflip()) { continue; } } if (you.attribute[ATTR_TRANSFORMATION] == TRAN_ICE_BEAST || you.attribute[ATTR_TRANSFORMATION] == TRAN_DRAGON || you.attribute[ATTR_TRANSFORMATION] == TRAN_SPIDER || you.attribute[ATTR_TRANSFORMATION] == TRAN_BAT) { continue; } // Kenku have large taloned feet that do good damage. const bool clawed_kick = player_mutation_level(MUT_TALONS); if (clawed_kick) { unarmed_attack = "claw"; miss_verb = "kick"; } else unarmed_attack = "kick"; aux_damage = (player_mutation_level(MUT_HOOVES) ? 10 : clawed_kick ? 8 : 5); break; } case 1: if (uattack != UNAT_HEADBUTT) { if (!player_mutation_level(MUT_HORNS) && !player_mutation_level(MUT_BEAK) || !one_chance_in(3)) { continue; } } if (you.attribute[ATTR_TRANSFORMATION] == TRAN_SPIDER || you.attribute[ATTR_TRANSFORMATION] == TRAN_ICE_BEAST || you.attribute[ATTR_TRANSFORMATION] == TRAN_DRAGON || you.attribute[ATTR_TRANSFORMATION] == TRAN_BAT) { continue; } if (player_mutation_level(MUT_BEAK) && (!player_mutation_level(MUT_HORNS) || coinflip())) { unarmed_attack = "peck"; aux_damage = 6; noise_factor = 75; } else { // Minotaurs used to get +5 damage here, now they get // +6 because of the horns. unarmed_attack = "headbutt"; aux_damage = 5 + player_mutation_level(MUT_HORNS) * 3; if (you.equip[EQ_HELMET] != -1) { const item_def& helmet = you.inv[you.equip[EQ_HELMET]]; if (is_hard_helmet(helmet)) { aux_damage += 2; if (get_helmet_desc(helmet) == THELM_DESC_SPIKED || get_helmet_desc(helmet) == THELM_DESC_HORNED) { aux_damage += 3; } } } } break; case 2: // draconians if (uattack != UNAT_TAILSLAP) { // not draconian, and not wet merfolk if (!player_genus(GENPC_DRACONIAN) && !(you.species == SP_MERFOLK && you.swimming()) && !player_mutation_level(MUT_STINGER) || (!one_chance_in(4))) { continue; } } if (you.attribute[ATTR_TRANSFORMATION] == TRAN_SPIDER || you.attribute[ATTR_TRANSFORMATION] == TRAN_ICE_BEAST || you.attribute[ATTR_TRANSFORMATION] == TRAN_BAT) { continue; } // TSO worshippers don't use their stinger in order to // avoid poisoning. if (you.religion == GOD_SHINING_ONE && player_mutation_level(MUT_STINGER) > 0) { continue; } unarmed_attack = "tail-slap"; aux_damage = 6; noise_factor = 125; if (player_mutation_level(MUT_STINGER) > 0) { aux_damage += (player_mutation_level(MUT_STINGER) * 2 - 1); damage_brand = SPWPN_VENOM; } // Grey dracs have spiny tails, or something. // Maybe add this to player messaging. {dlb} // // STINGER mutation doesn't give extra damage here... that // would probably be a bit much, we'll still get the // poison bonus so it's still somewhat good. if (you.species == SP_GREY_DRACONIAN && you.experience_level >= 7) aux_damage = 12; break; case 3: if (uattack != UNAT_PUNCH) continue; if (you.attribute[ATTR_TRANSFORMATION] == TRAN_SPIDER || you.attribute[ATTR_TRANSFORMATION] == TRAN_ICE_BEAST || you.attribute[ATTR_TRANSFORMATION] == TRAN_DRAGON || you.attribute[ATTR_TRANSFORMATION] == TRAN_BAT) { continue; } // No punching with a shield or 2-handed wpn, except staves. if (shield || coinflip() || (weapon && hands == HANDS_TWO && weapon->base_type != OBJ_STAVES && weapon_skill(*weapon) != SK_STAVES)) { continue; } unarmed_attack = "punch"; // applied twice aux_damage = 5 + you.skills[SK_UNARMED_COMBAT] / 3; if (you.attribute[ATTR_TRANSFORMATION] == TRAN_BLADE_HANDS) { unarmed_attack = "slash"; aux_damage += 6; noise_factor = 75; simple_miss_message = true; } else if (you.has_usable_claws()) { unarmed_attack = "claw"; aux_damage += roll_dice(1, 3); simple_miss_message = true; } break; case 4: if (uattack != UNAT_BITE) continue; if (!player_mutation_level(MUT_FANGS)) continue; if (you.species != SP_VAMPIRE && one_chance_in(5) || one_chance_in(7)) { continue; } // no biting with visored helmet if (you.equip[EQ_HELMET] != -1 && (get_helmet_desc((you.inv[you.equip[EQ_HELMET]])) == THELM_DESC_VISORED)) { continue; } unarmed_attack = "bite"; simple_miss_message = true; aux_damage += player_mutation_level(MUT_FANGS) * 2 + you.skills[SK_UNARMED_COMBAT] / 5; noise_factor = 75; if (you.species == SP_VAMPIRE) { if (_vamp_wants_blood_from_monster(defender_as_monster())) { // prob of vampiric bite: // 1/4 when non-thirsty, 1/2 when thirsty, 100% when // bloodless if (you.hunger_state >= HS_SATIATED && coinflip()) break; if (you.hunger_state != HS_STARVING && coinflip()) break; damage_brand = SPWPN_VAMPIRICISM; } else if (!one_chance_in(3)) // monster not interesting bloodwise continue; } break; // To add more, add to while part of loop below as well default: continue; } // unified to-hit calculation to_hit = random2(calc_your_to_hit_unarmed(uattack, damage_brand == SPWPN_VAMPIRICISM)); make_hungry(2, true); handle_noise(defender_pos); alert_nearby_monsters(); // XXX We're clobbering did_hit did_hit = false; bool ely_block = false; const int evasion = defender->melee_evasion(attacker); const int helpful_evasion = defender->melee_evasion(attacker, EV_IGNORE_HELPLESS); // No monster Phase Shift yet if (you.religion != GOD_ELYVILON && you.penance[GOD_ELYVILON] && to_hit >= evasion && one_chance_in(20)) { simple_god_message(" blocks your attack.", GOD_ELYVILON); ely_block = true; } bool auto_hit = one_chance_in(30); if (!ely_block && !auto_hit && to_hit >= evasion && !(to_hit >= helpful_evasion) && defender_visible) { mprf("Helpless, %s fails to dodge your %s.", defender->name(DESC_NOCAP_THE).c_str(), miss_verb.empty() ? unarmed_attack.c_str() : miss_verb.c_str()); } if (!ely_block && (to_hit >= evasion || auto_hit)) { // Upset the monster. behaviour_event(defender_as_monster(), ME_WHACK, MHITYOU); if (attack_shield_blocked(true)) continue; if (player_apply_aux_unarmed()) return (true); } else { if (simple_miss_message) { mprf("You miss %s.", defender->name(DESC_NOCAP_THE).c_str()); } else { mprf("Your %s misses %s.", miss_verb.empty() ? unarmed_attack.c_str() : miss_verb.c_str(), defender->name(DESC_NOCAP_THE).c_str()); } if (ely_block) dec_penance(GOD_ELYVILON, 1 + random2(to_hit - evasion)); } } return (false); } bool melee_attack::player_apply_aux_unarmed() { did_hit = true; aux_damage = player_aux_stat_modify_damage(aux_damage); aux_damage += slaying_bonus(PWPN_DAMAGE); aux_damage = random2(aux_damage); // Clobber wpn_skill. wpn_skill = SK_UNARMED_COMBAT; aux_damage = player_apply_weapon_skill(aux_damage); aux_damage = player_apply_fighting_skill(aux_damage, true); aux_damage = player_apply_misc_modifiers(aux_damage); // Clear stab bonus which will be set for the primary weapon attack. stab_bonus = 0; aux_damage = player_apply_monster_ac(aux_damage); aux_damage = defender->hurt(&you, aux_damage, BEAM_MISSILE, false); damage_done = aux_damage; if (damage_done > 0) { player_exercise_combat_skills(); mprf("You %s %s%s%s", unarmed_attack.c_str(), defender->name(DESC_NOCAP_THE).c_str(), debug_damage_number().c_str(), attack_strength_punctuation().c_str()); if (damage_brand == SPWPN_VENOM && coinflip()) poison_monster(defender_as_monster(), KC_YOU); // Normal vampiric biting attack, not if already got stabbing special. if (damage_brand == SPWPN_VAMPIRICISM && you.species == SP_VAMPIRE && (!stab_attempt || stab_bonus <= 0)) { _player_vampire_draws_blood(defender_as_monster(), damage_done); } } else // no damage was done { mprf("You %s %s%s.", unarmed_attack.c_str(), defender->name(DESC_NOCAP_THE).c_str(), you.can_see(defender) ? ", but do no damage" : ""); } if (defender_as_monster()->hit_points < 1) { _monster_die(defender_as_monster(), KILL_YOU, NON_MONSTER); return (true); } return (false); } std::string melee_attack::debug_damage_number() { #ifdef DEBUG_DIAGNOSTICS return make_stringf(" for %d", damage_done); #else return (""); #endif } std::string melee_attack::special_attack_punctuation() { if (special_damage < 6) return "."; else return "!"; } std::string melee_attack::attack_strength_punctuation() { if (attacker->atype() == ACT_PLAYER) { if (damage_done < HIT_WEAK) return "."; else if (damage_done < HIT_MED) return "!"; else if (damage_done < HIT_STRONG) return "!!"; else return "!!!"; } else { return (damage_done < HIT_WEAK ? "." : "!"); } } void melee_attack::player_announce_hit() { if (!verb_degree.empty() && verb_degree[0] != ' ') verb_degree = " " + verb_degree; msg::stream << "You " << attack_verb << ' ' << defender->name(DESC_NOCAP_THE) << verb_degree << debug_damage_number() << attack_strength_punctuation() << std::endl; } std::string melee_attack::player_why_missed() { if (heavy_armour_penalty > 0 && (to_hit + heavy_armour_penalty/2 >= defender->melee_evasion(attacker))) { return "Your armour prevents you from hitting "; } else return "You miss "; } void melee_attack::player_warn_miss() { did_hit = false; // Upset only non-sleeping monsters if we missed. if (!defender->asleep()) behaviour_event(defender_as_monster(), ME_WHACK, MHITYOU); msg::stream << player_why_missed() << defender->name(DESC_NOCAP_THE) << "." << std::endl; } bool melee_attack::player_hits_monster() { const int evasion = defender->melee_evasion(attacker); const int evasion_helpful = defender->melee_evasion(attacker, EV_IGNORE_HELPLESS); dprf("your to-hit: %d; defender effective EV: %d", to_hit, evasion); if (to_hit >= evasion_helpful || one_chance_in(20)) { return (true); } if (to_hit >= evasion || ((defender->cannot_act() || defender->asleep()) && !one_chance_in(10 + you.skills[SK_STABBING])) || defender_as_monster()->petrifying() && !one_chance_in(2 + you.skills[SK_STABBING])) { if (defender_visible) msg::stream << "Helpless, " << defender->name(DESC_NOCAP_THE) << " fails to dodge your attack." << std::endl; return (true); } return (false); } int melee_attack::player_stat_modify_damage(int damage) { int dammod = 78; const int dam_stat_val = calc_stat_to_dam_base(); if (dam_stat_val > 11) dammod += (random2(dam_stat_val - 11) * 2); else if (dam_stat_val < 9) dammod -= (random2(9 - dam_stat_val) * 3); damage *= dammod; damage /= 78; return (damage); } int melee_attack::player_aux_stat_modify_damage(int damage) { int dammod = 10; const int dam_stat_val = calc_stat_to_dam_base(); if (dam_stat_val > 11) dammod += random2(dam_stat_val - 11) / 3; else if (dam_stat_val < 9) dammod -= random2(9 - dam_stat_val) / 2; damage *= dammod; damage /= 10; return (damage); } int melee_attack::player_apply_water_attack_bonus(int damage) { // Yes, this isn't the *2 damage that monsters get, but since // monster hps and player hps are different (as are the sizes // of the attacks) it just wouldn't be fair to give merfolk // that gross a potential... still they do get something for // making use of the water. -- bwr // return (damage + random2avg(10,2)); // [dshaligram] Experimenting with the full double damage bonus since // it takes real effort to set up a water attack and it's very situational. return (damage * 2); } int melee_attack::player_apply_weapon_skill(int damage) { if (weapon && (weapon->base_type == OBJ_WEAPONS || weapon->base_type == OBJ_STAVES)) { damage *= 25 + (random2( you.skills[ wpn_skill ] + 1 )); damage /= 25; } return (damage); } int melee_attack::player_apply_fighting_skill(int damage, bool aux) { const int base = aux? 40 : 30; damage *= base + (random2(you.skills[SK_FIGHTING] + 1)); damage /= base; return (damage); } int melee_attack::player_apply_misc_modifiers(int damage) { if (you.duration[DUR_MIGHT] > 1) damage += 1 + random2(10); if (you.species != SP_VAMPIRE && you.hunger_state == HS_STARVING) damage -= random2(5); return (damage); } int melee_attack::player_apply_weapon_bonuses(int damage) { if (weapon && (weapon->base_type == OBJ_WEAPONS || item_is_rod( *weapon ))) { int wpn_damage_plus = weapon->plus2; if (item_is_rod( *weapon )) wpn_damage_plus = (short)weapon->props["rod_enchantment"]; damage += (wpn_damage_plus > -1) ? (random2(1 + wpn_damage_plus)) : -(1 + random2(-wpn_damage_plus)); // removed 2-handed weapons from here... their "bonus" is // already included in the damage stat for the weapon -- bwr if (hand_half_bonus) damage += random2(3); if (get_equip_race(*weapon) == ISFLAG_DWARVEN && player_genus(GENPC_DWARVEN)) { damage += random2(3); } if (get_equip_race(*weapon) == ISFLAG_ORCISH && you.species == SP_HILL_ORC) { if (you.religion == GOD_BEOGH && !player_under_penance()) { #ifdef DEBUG_DIAGNOSTICS const int orig_damage = damage; #endif if (you.piety > 80 || coinflip()) damage++; damage += random2avg( div_rand_round( std::min(static_cast(you.piety), 180), 33), 2); #ifdef DEBUG_DIAGNOSTICS mprf(MSGCH_DIAGNOSTICS, "Damage: %d -> %d, Beogh bonus: %d", orig_damage, damage, damage - orig_damage); #endif } if (coinflip()) damage++; } // Demonspawn get a damage bonus for demonic weapons. if (you.species == SP_DEMONSPAWN && is_demonic(*weapon)) damage += random2(3); } return (damage); } void melee_attack::player_weapon_auto_id() { if (weapon && weapon->base_type == OBJ_WEAPONS && !is_range_weapon( *weapon ) && !item_ident( *weapon, ISFLAG_KNOW_PLUSES ) && x_chance_in_y(you.skills[ wpn_skill ], 100)) { set_ident_flags( *weapon, ISFLAG_KNOW_PLUSES ); mprf("You are wielding %s.", weapon->name(DESC_NOCAP_A).c_str()); more(); you.wield_change = true; } } int melee_attack::player_stab_weapon_bonus(int damage) { if (weapon && weapon->base_type == OBJ_WEAPONS && (weapon->sub_type == WPN_CLUB || weapon->sub_type == WPN_SPEAR || weapon->sub_type == WPN_TRIDENT || weapon->sub_type == WPN_DEMON_TRIDENT)) { goto ok_weaps; } switch (wpn_skill) { case SK_SHORT_BLADES: { int bonus = (you.dex * (you.skills[SK_STABBING] + 1)) / 5; if (weapon->sub_type != WPN_DAGGER) bonus /= 2; bonus = stepdown_value( bonus, 10, 10, 30, 30 ); damage += bonus; } // fall through ok_weaps: case SK_LONG_BLADES: damage *= 10 + you.skills[SK_STABBING] / (stab_bonus + (wpn_skill == SK_SHORT_BLADES ? 0 : 2)); damage /= 10; // fall through default: damage *= 12 + you.skills[SK_STABBING] / stab_bonus; damage /= 12; } return (damage); } int melee_attack::player_stab(int damage) { // The stabbing message looks better here: if (stab_attempt) { // Construct reasonable message. stab_message(defender, stab_bonus); exercise(SK_STABBING, 1 + random2avg(5, 4)); did_god_conduct(DID_STABBING, 4); } else { stab_bonus = 0; // Ok.. if you didn't backstab, you wake up the neighborhood. // I can live with that. alert_nearby_monsters(); } if (stab_bonus) { // Let's make sure we have some damage to work with... damage = std::max(1, damage); if (defender->asleep()) { // Sleeping moster wakes up when stabbed but may be groggy. if (x_chance_in_y(you.skills[SK_STABBING] + you.dex + 1, 200)) { int stun = random2(you.dex + 1); if (defender_as_monster()->speed_increment > stun) defender_as_monster()->speed_increment -= stun; else defender_as_monster()->speed_increment = 0; } } damage = player_stab_weapon_bonus(damage); } return (damage); } int melee_attack::player_apply_monster_ac(int damage) { if (stab_bonus) { // When stabbing we can get by some of the armour. if (defender->armour_class() > 0) { const int ac = defender->armour_class() - random2(you.skills[SK_STABBING] / stab_bonus); if (ac > 0) damage -= random2(1 + ac); } } else { // Apply AC normally. if (defender->armour_class() > 0) damage -= random2(1 + defender->armour_class()); } if (defender->petrified()) damage /= 3; return (damage); } int melee_attack::player_weapon_type_modify(int damage) { int weap_type = WPN_UNKNOWN; if (!weapon) weap_type = WPN_UNARMED; else if (item_is_staff(*weapon)) weap_type = WPN_QUARTERSTAFF; else if (item_is_rod(*weapon)) weap_type = WPN_CLUB; else if (weapon->base_type == OBJ_WEAPONS) weap_type = weapon->sub_type; // All weak hits look the same, except for when the player // has a non-weapon in hand. - bwr // Exception: vampire bats only bite to allow for drawing blood. if (damage < HIT_WEAK && (you.species != SP_VAMPIRE || !player_in_bat_form())) { if (weap_type != WPN_UNKNOWN) attack_verb = "hit"; else attack_verb = "clumsily bash"; return (damage); } // Take transformations into account, if no weapon is wielded. if (weap_type == WPN_UNARMED && you.attribute[ATTR_TRANSFORMATION] != TRAN_NONE) { switch (you.attribute[ATTR_TRANSFORMATION]) { case TRAN_SPIDER: case TRAN_BAT: case TRAN_PIG: if (damage < HIT_STRONG) attack_verb = "bite"; else attack_verb = "maul"; break; case TRAN_BLADE_HANDS: if (damage < HIT_MED) attack_verb = "slash"; else if (damage < HIT_STRONG) attack_verb = "slice"; else attack_verb = "shred"; break; case TRAN_STATUE: case TRAN_LICH: if (you.has_usable_claws()) { if (damage < HIT_MED) attack_verb = "claw"; else if (damage < HIT_STRONG) attack_verb = "mangle"; else attack_verb = "eviscerate"; break; } // or fall-through case TRAN_ICE_BEAST: if (damage < HIT_MED) attack_verb = "punch"; else attack_verb = "pummel"; break; case TRAN_DRAGON: if (damage < HIT_MED) attack_verb = "claw"; else if (damage < HIT_STRONG) attack_verb = "bite"; else { attack_verb = "maul"; if (defender->body_size() <= SIZE_MEDIUM && coinflip()) attack_verb = "trample on"; } break; } // transformations return (damage); } // Take normal hits into account. If the hit is from a weapon with // more than one damage type, randomly choose one damage type from // it. switch (weapon ? single_damage_type(*weapon) : -1) { case DAM_PIERCE: if (damage < HIT_MED) attack_verb = "puncture"; else if (damage < HIT_STRONG) attack_verb = "impale"; else { attack_verb = "spit"; if (defender->atype() == ACT_MONSTER && defender_visible && mons_genus(defender_as_monster()->type) == MONS_HOG) { verb_degree = " like the proverbial pig"; } else verb_degree = " like a pig"; } break; case DAM_SLICE: if (damage < HIT_MED) attack_verb = "slash"; else if (damage < HIT_STRONG) attack_verb = "slice"; else if (mons_genus(defender_as_monster()->type) == MONS_OGRE) { attack_verb = "dice"; verb_degree = " like an onion"; } else { attack_verb = "open"; verb_degree = " like a pillowcase"; } break; case DAM_BLUDGEON: if (damage < HIT_MED) attack_verb = one_chance_in(4) ? "thump" : "sock"; else if (damage < HIT_STRONG) attack_verb = "bludgeon"; else { attack_verb = "crush"; verb_degree = " like a grape"; } break; case DAM_WHIP: if (damage < HIT_MED) attack_verb = "whack"; else if (damage < HIT_STRONG) attack_verb = "thrash"; else { switch(defender->holiness()) { case MH_HOLY: case MH_NATURAL: case MH_DEMONIC: attack_verb = "punish"; verb_degree = " causing immense pain"; break; default: attack_verb = "devastate"; } } break; case -1: // unarmed if (you.damage_type() == DVORP_CLAWING) { if (damage < HIT_MED) attack_verb = "claw"; else if (damage < HIT_STRONG) attack_verb = "mangle"; else attack_verb = "eviscerate"; } else { if (damage < HIT_MED) attack_verb = "punch"; else attack_verb = "pummel"; } break; case WPN_UNKNOWN: default: attack_verb = "hit"; break; } return (damage); } void melee_attack::player_exercise_combat_skills() { const bool helpless = defender->cannot_fight(); if (!helpless || you.skills[wpn_skill] < 1) exercise(wpn_skill, 1); if ((!helpless || you.skills[SK_FIGHTING] < 1) && one_chance_in(3)) { exercise(SK_FIGHTING, 1); } } void melee_attack::player_check_weapon_effects() { if (weapon && weapon->base_type == OBJ_WEAPONS) { if (is_holy_item(*weapon)) did_god_conduct(DID_HOLY, 1); else if (is_demonic(*weapon)) did_god_conduct(DID_UNHOLY, 1); else if (get_weapon_brand(*weapon) == SPWPN_SPEED || weapon->sub_type == WPN_QUICK_BLADE) { did_god_conduct(DID_HASTY, 1); } } } // Effects that occur after all other effects, even if the monster is dead. // For example, explosions that would hit other creatures, but we want // to deal with only one creature at a time, so that's handled last. // You probably want to call player_monattk_hit_effects instead, as that // function calls this one. // Returns true if the combat round should end here. bool melee_attack::player_monattk_final_hit_effects(bool mondied) { for (unsigned int i = 0; i < final_effects.size(); ++i) { switch (final_effects[i].flavor) { case FINEFF_LIGHTNING_DISCHARGE: if (you.see_cell(final_effects[i].location)) mpr("Electricity arcs through the water!"); conduct_electricity(final_effects[i].location, attacker); break; } } return mondied; } // Returns true if the combat round should end here. bool melee_attack::player_monattk_hit_effects(bool mondied) { player_check_weapon_effects(); mondied = check_unrand_effects(mondied) || mondied; // Thirsty vampires will try to use a stabbing situation to draw blood. if (you.species == SP_VAMPIRE && you.hunger_state < HS_SATIATED && mondied && stab_attempt && stab_bonus > 0 && _player_vampire_draws_blood(defender_as_monster(), damage_done, true)) { // No further effects. } else if (you.species == SP_VAMPIRE && damage_brand == SPWPN_VAMPIRICISM && you.weapon() && _player_vampire_draws_blood(defender_as_monster(), damage_done, false, (mondied ? 1 : 10))) { // No further effects. } // Vampiric *weapon* effects for the killing blow. else if (mondied && damage_brand == SPWPN_VAMPIRICISM && you.weapon() && you.species != SP_VAMPIRE) // vampires get their bonus elsewhere { if (defender->holiness() == MH_NATURAL && !defender->is_summoned() && damage_done > 0 && you.hp < you.hp_max && !one_chance_in(5)) { mpr("You feel better."); // More than if not killed. const int heal = 1 + random2(damage_done); dprf("Vampiric healing: damage %d, healed %d", damage_done, heal); inc_hp(heal, false); if (you.hunger_state != HS_ENGORGED) lessen_hunger(30 + random2avg(59, 2), false); did_god_conduct(DID_NECROMANCY, 2); } } if (mondied) return player_monattk_final_hit_effects(true); // These effects apply only to monsters that are still alive: // Returns true if a head was cut off *and* the wound was cauterized, // in which case the cauterization was the ego effect, so don't burn // the hydra some more. // // Also returns true if the hydra's last head was cut off, in which // case nothing more should be done to the hydra. if (decapitate_hydra(damage_done)) return player_monattk_final_hit_effects(!defender->alive()); // These two (staff damage and damage brand) are mutually exclusive! player_apply_staff_damage(); // Returns true if the monster croaked. if (!special_damage && apply_damage_brand()) return player_monattk_final_hit_effects(true); if (!no_damage_message.empty()) { if (special_damage > 0) emit_nodmg_hit_message(); else { mprf("You %s %s, but do no damage.", attack_verb.c_str(), defender->name(DESC_NOCAP_THE).c_str()); } } if (needs_message && !special_damage_message.empty()) { mprf("%s", special_damage_message.c_str()); // Don't do a message-only miscast right after a special damage. if (miscast_level == 0) miscast_level = -1; } #ifdef DEBUG_DIAGNOSTICS mprf(MSGCH_DIAGNOSTICS, "Special damage to %s: %d, flavour: %d", defender->name(DESC_NOCAP_THE).c_str(), special_damage, special_damage_flavour); #endif special_damage = defender->hurt(&you, special_damage, special_damage_flavour, false); if (!defender->alive()) { _monster_die(defender_as_monster(), KILL_YOU, NON_MONSTER); return player_monattk_final_hit_effects(true); } if (stab_attempt && stab_bonus > 0 && weapon && weapon->base_type == OBJ_WEAPONS && weapon->sub_type == WPN_CLUB && damage_done + special_damage > random2(defender->get_experience_level()) && !defender_as_monster()->has_ench(ENCH_CONFUSION) && mons_class_is_confusable(defender->id())) { if (defender_as_monster()->add_ench(mon_enchant(ENCH_CONFUSION, 0, KC_YOU, 20+random2(30)))) // 1-3 turns { mprf("%s is stunned!", defender->name(DESC_CAP_THE).c_str()); } } return player_monattk_final_hit_effects(false); } void melee_attack::_monster_die(monsters* monster, killer_type killer, int killer_index) { const bool chaos = (damage_brand == SPWPN_CHAOS); const bool reaping = (damage_brand == SPWPN_REAPING); // Copy defender before it gets reset by monster_die(). monsters* def_copy = NULL; if (chaos || reaping) def_copy = new monsters(*monster); // The monster is about to die, so restore its original attitude // for the cleanup effects (god reactions). This could be a // problem if the "killing" is actually an Abyss banishment - we // don't want to create permafriendlies this way - so don't do it // then. if (monster == defender && killer != KILL_RESET) monster->attitude = defender_starting_attitude; int corpse = monster_die(monster, killer, killer_index); if (chaos) { chaos_killed_defender(def_copy); delete def_copy; } else if (reaping) { if (corpse != -1) mons_reaped(attacker, def_copy); delete def_copy; } } static bool is_boolean_resist(beam_type flavour) { switch (flavour) { case BEAM_ELECTRICITY: case BEAM_MIASMA: // rotting case BEAM_NAPALM: case BEAM_WATER: // water asphyxiation damage, // bypassed by being water inhabitant. return (true); default: return (false); } } // Gets the percentage of the total damage of this damage flavour that can // be resisted. static inline int get_resistible_fraction(beam_type flavour) { switch (flavour) { // Drowning damage from water is resistible by being a water thing, or // otherwise asphyx resistant. case BEAM_WATER: return (40); // Assume ice storm and throw icicle are mostly solid. case BEAM_ICE: return (25); case BEAM_LAVA: return (35); case BEAM_POISON_ARROW: return (40); default: return (100); } } // Adjusts damage for elemental resists, electricity and poison. // // FIXME: Does not (yet) handle life draining, player acid damage // (does handle monster acid damage), miasma, and other exotic // attacks. // // beam_type is just used to determine the damage flavour, it does not // necessarily imply that the attack is a beam attack. int resist_adjust_damage(actor *defender, beam_type flavour, int res, int rawdamage, bool ranged) { if (!res) return (rawdamage); const bool monster = (defender->atype() == ACT_MONSTER); const int resistible_fraction = get_resistible_fraction(flavour); int resistible = rawdamage * resistible_fraction / 100; const int irresistible = rawdamage - resistible; if (res > 0) { if (monster && res >= 3) resistible = 0; else { // Check if this is a resist that pretends to be boolean for // damage purposes. Only electricity, miasma and sticky // flame (napalm) do this at the moment; raw poison damage // uses the normal formula. const int bonus_res = (is_boolean_resist(flavour) ? 1 : 0); // Use a new formula for players, but keep the old, more // effective one for monsters. if (monster) resistible /= 1 + bonus_res + res * res; else resistible /= resist_fraction(res, bonus_res); } } else if (res < 0) resistible = resistible * (ranged? 15 : 20) / 10; return std::max(resistible + irresistible, 0); } void melee_attack::calc_elemental_brand_damage( beam_type flavour, int res, const char *verb) { special_damage = resist_adjust_damage(defender, flavour, res, random2(damage_done) / 2 + 1); if (special_damage > 0 && verb && needs_message) { special_damage_message = make_stringf( "%s %s %s%s", atk_name(DESC_CAP_THE).c_str(), attacker->conj_verb(verb).c_str(), mons_defender_name().c_str(), special_attack_punctuation().c_str()); } } int melee_attack::fire_res_apply_cerebov_downgrade(int res) { if (weapon && weapon->special == UNRAND_CEREBOV) --res; return (res); } void melee_attack::drain_defender() { if (defender->atype() == ACT_MONSTER && one_chance_in(3)) return; special_damage = 1 + random2(damage_done) / (2 + defender->res_negative_energy()); if (defender->drain_exp(attacker, true)) { if (defender->atype() == ACT_PLAYER) obvious_effect = true; else if (defender_visible) { special_damage_message = make_stringf( "%s %s %s!", atk_name(DESC_CAP_THE).c_str(), attacker->conj_verb("drain").c_str(), mons_defender_name().c_str()); } attacker->god_conduct(DID_NECROMANCY, 2); } } void melee_attack::rot_defender(int amount, int immediate) { if (defender->rot(attacker, amount, immediate, true)) { if (defender->atype() == ACT_MONSTER && defender_visible) { special_damage_message = make_stringf( "%s %s!", def_name(DESC_CAP_THE).c_str(), amount > 0 ? "rots" : "looks less resilient"); } } } bool melee_attack::distortion_affects_defender() { //jmf: blink frogs *like* distortion // I think could be amended to let blink frogs "grow" like // jellies do {dlb} if (defender->atype() == ACT_MONSTER && mons_genus(defender_as_monster()->type) == MONS_BLINK_FROG) { if (one_chance_in(5)) { emit_nodmg_hit_message(); if (defender_visible) { special_damage_message = make_stringf("%s %s in the distortional energy.", def_name(DESC_CAP_THE).c_str(), defender->conj_verb("bask").c_str()); } defender->heal(1 + random2avg(7, 2), true); // heh heh } return (false); } if (one_chance_in(3)) { if (defender_visible) { special_damage_message = make_stringf( "Space bends around %s.", def_name(DESC_NOCAP_THE).c_str()); } special_damage += 1 + random2avg(7, 2); return (false); } if (one_chance_in(3)) { if (defender_visible) { special_damage_message = make_stringf( "Space warps horribly around %s!", def_name(DESC_NOCAP_THE).c_str()); } special_damage += 3 + random2avg(24, 2); return (false); } if (one_chance_in(3)) { emit_nodmg_hit_message(); if (defender_visible) obvious_effect = true; defender->blink(); return (false); } // Used to be coinflip() || coinflip() for players, just coinflip() // for monsters; this is a compromise. Note that it makes banishment // a touch more likely for players, and a shade less likely for // monsters. if (!one_chance_in(3)) { emit_nodmg_hit_message(); if (defender_visible) obvious_effect = true; defender->teleport(coinflip(), one_chance_in(5)); return (false); } if (you.level_type != LEVEL_ABYSS && coinflip()) { emit_nodmg_hit_message(); if (defender->atype() == ACT_PLAYER && attacker_visible && weapon != NULL && !is_unrandom_artefact(*weapon) && !is_special_unrandom_artefact(*weapon)) { // If the player is being sent to the Abyss by being attacked // with a distortion weapon, then we have to ID it before // the player goes to Abyss, while the weapon object is // still in memory. if (is_artefact(*weapon)) artefact_wpn_learn_prop(*weapon, ARTP_BRAND); else set_ident_flags(*weapon, ISFLAG_KNOW_TYPE); } else if (defender_visible) obvious_effect = true; defender->banish(attacker->name(DESC_PLAIN, true)); return (true); } return (false); } enum chaos_type { CHAOS_CLONE, CHAOS_POLY, CHAOS_POLY_UP, CHAOS_MAKE_SHIFTER, CHAOS_MISCAST, CHAOS_RAGE, CHAOS_HEAL, CHAOS_HASTE, CHAOS_INVIS, CHAOS_SLOW, CHAOS_PARALYSIS, CHAOS_PETRIFY, NUM_CHAOS_TYPES }; // XXX: We might want to vary the probabilities for the various effects // based on whether the source is a weapon of chaos or a monster with // AF_CHAOS. void melee_attack::chaos_affects_defender() { const bool mon = defender->atype() == ACT_MONSTER; const bool immune = mon && mons_immune_magic(defender_as_monster()); const bool is_natural = mon && defender->holiness() == MH_NATURAL; const bool is_shifter = mon && defender_as_monster()->is_shapeshifter(); const bool can_clone = mon && defender->is_holy() && mons_clonable(defender_as_monster(), true); const bool can_poly = is_shifter || (defender->can_safely_mutate() && !immune); const bool can_rage = defender->can_go_berserk(); int clone_chance = can_clone ? 1 : 0; int poly_chance = can_poly ? 1 : 0; int poly_up_chance = can_poly && mon ? 1 : 0; int shifter_chance = can_poly && is_natural && mon ? 1 : 0; int rage_chance = can_rage ? 10 : 0; int miscast_chance = 10; // Already a shifter? if (is_shifter) shifter_chance = 0; // A chaos self-attack increases the chance of certain effects, // due to a short-circuit/feedback/resonance/whatever. if (attacker == defender) { clone_chance *= 2; poly_chance *= 2; poly_up_chance *= 2; shifter_chance *= 2; miscast_chance *= 2; // Inform player that something is up. if (you.see_cell(defender->pos())) { if (defender->atype() == ACT_PLAYER) mpr("You give off a flash of multicoloured light!"); else if (you.can_see(defender)) { simple_monster_message(defender_as_monster(), " gives off a flash of " "multicoloured light!"); } else mpr("There is a flash of multicoloured light!"); } } // NOTE: Must appear in exact same order as in chaos_type enumeration. int probs[NUM_CHAOS_TYPES] = { clone_chance, // CHAOS_CLONE poly_chance, // CHAOS_POLY poly_up_chance, // CHAOS_POLY_UP shifter_chance, // CHAOS_MAKE_SHIFTER miscast_chance, // CHAOS_MISCAST rage_chance, // CHAOS_RAGE 10, // CHAOS_HEAL 10, // CHAOS_HASTE 10, // CHAOS_INVIS 10, // CHAOS_SLOW 10, // CHAOS_PARALYSIS 10, // CHAOS_PETRIFY }; bolt beam; beam.flavour = BEAM_NONE; int choice = choose_random_weighted(probs, probs + NUM_CHAOS_TYPES); #ifdef NOTE_DEBUG_CHAOS_EFFECTS std::string chaos_effect = "CHAOS effect: "; switch (choice) { case CHAOS_CLONE: chaos_effect += "clone"; break; case CHAOS_POLY: chaos_effect += "polymorph"; break; case CHAOS_POLY_UP: chaos_effect += "polymorph PPT_MORE"; break; case CHAOS_MAKE_SHIFTER: chaos_effect += "shifter"; break; case CHAOS_MISCAST: chaos_effect += "miscast"; break; case CHAOS_RAGE: chaos_effect += "berserk"; break; case CHAOS_HEAL: chaos_effect += "healing"; break; case CHAOS_HASTE: chaos_effect += "hasting"; break; case CHAOS_INVIS: chaos_effect += "invisible"; break; case CHAOS_SLOW: chaos_effect += "slowing"; break; case CHAOS_PARALYSIS: chaos_effect += "paralysis"; break; case CHAOS_PETRIFY: chaos_effect += "petrify"; break; default: chaos_effect += "(other)"; break; } take_note(Note(NOTE_MESSAGE, 0, 0, chaos_effect.c_str()), true); #endif switch (static_cast(choice)) { case CHAOS_CLONE: { ASSERT(can_clone && clone_chance > 0); ASSERT(defender->atype() == ACT_MONSTER); int clone_idx = clone_mons(defender_as_monster(), true, &obvious_effect); if (clone_idx != NON_MONSTER) { if (obvious_effect) { special_damage_message = make_stringf("%s is duplicated!", def_name(DESC_NOCAP_THE).c_str()); } monsters &clone(menv[clone_idx]); // The player shouldn't get new permanent followers from cloning. if (clone.attitude == ATT_FRIENDLY && !clone.is_summoned()) clone.mark_summoned(6, true, MON_SUMM_CLONE); // Monsters being cloned is interesting. xom_is_stimulated(clone.friendly() ? 16 : 32); } break; } case CHAOS_POLY: ASSERT(can_poly && poly_chance > 0); beam.flavour = BEAM_POLYMORPH; break; case CHAOS_POLY_UP: ASSERT(can_poly && poly_up_chance > 0); ASSERT(defender->atype() == ACT_MONSTER); obvious_effect = you.can_see(defender); monster_polymorph(defender_as_monster(), RANDOM_MONSTER, PPT_MORE); break; case CHAOS_MAKE_SHIFTER: { ASSERT(can_poly && shifter_chance > 0); ASSERT(!is_shifter); ASSERT(defender->atype() == ACT_MONSTER); obvious_effect = you.can_see(defender); defender_as_monster()->add_ench(one_chance_in(3) ? ENCH_GLOWING_SHAPESHIFTER : ENCH_SHAPESHIFTER); // Immediately polymorph monster, just to make the effect obvious. monster_polymorph(defender_as_monster(), RANDOM_MONSTER); // Xom loves it if this happens! const int friend_factor = defender_as_monster()->friendly() ? 1 : 2; const int glow_factor = (defender_as_monster()->has_ench(ENCH_SHAPESHIFTER) ? 1 : 2); xom_is_stimulated( 64 * friend_factor * glow_factor ); break; } case CHAOS_MISCAST: { int level = defender->get_experience_level(); // At level == 27 there's a 13.9% chance of a level 3 miscast. int level0_chance = level; int level1_chance = std::max( 0, level - 7); int level2_chance = std::max( 0, level - 12); int level3_chance = std::max( 0, level - 17); level = random_choose_weighted( level0_chance, 0, level1_chance, 1, level2_chance, 2, level3_chance, 3, 0); miscast_level = level; miscast_type = SPTYP_RANDOM; miscast_target = one_chance_in(3) ? attacker : defender; break; } case CHAOS_RAGE: ASSERT(can_rage && rage_chance > 0); defender->go_berserk(false); obvious_effect = you.can_see(defender); break; case CHAOS_HEAL: beam.flavour = BEAM_HEALING; break; case CHAOS_HASTE: beam.flavour = BEAM_HASTE; break; case CHAOS_INVIS: beam.flavour = BEAM_INVISIBILITY; break; case CHAOS_SLOW: beam.flavour = BEAM_SLOW; break; case CHAOS_PARALYSIS: beam.flavour = BEAM_PARALYSIS; break; case CHAOS_PETRIFY: beam.flavour = BEAM_PETRIFY; break; default: ASSERT(!"Invalid chaos effect type"); break; } if (beam.flavour != BEAM_NONE) { beam.type = 0; beam.range = 0; beam.colour = BLACK; beam.effect_known = false; if (weapon && you.can_see(attacker)) { beam.name = wep_name(DESC_NOCAP_YOUR); beam.item = weapon; } else beam.name = atk_name(DESC_NOCAP_THE); beam.thrower = (attacker->atype() == ACT_PLAYER) ? KILL_YOU : attacker_as_monster()->confused_by_you() ? KILL_YOU_CONF : KILL_MON; if (beam.thrower == KILL_YOU || attacker_as_monster()->friendly()) beam.attitude = ATT_FRIENDLY; beam.beam_source = attacker->mindex(); beam.source = defender->pos(); beam.target = defender->pos(); beam.damage = dice_def(damage_done + special_damage + aux_damage, 1); beam.ench_power = beam.damage.num; beam.fire(); if (you.can_see(defender)) obvious_effect = beam.obvious_effect; } if (!you.can_see(attacker)) obvious_effect = false; } static bool _move_stairs(const actor* attacker, const actor* defender) { const coord_def orig_pos = attacker->pos(); const dungeon_feature_type stair_feat = grd(orig_pos); if (feat_stair_direction(stair_feat) == CMD_NO_CMD) return (false); // The player can't use shops to escape, so don't bother. if (stair_feat == DNGN_ENTER_SHOP) return (false); // Don't move around notable terrain the player is aware of if it's // out of sight. if (is_notable_terrain(stair_feat) && is_terrain_known(orig_pos.x, orig_pos.y) && !you.see_cell(orig_pos)) { return (false); } coord_def dest(-1, -1); // Prefer to send it under the defender. if (defender->alive() && defender->pos() != attacker->pos()) dest = defender->pos(); return slide_feature_over(attacker->pos(), dest); } #define DID_AFFECT() \ { \ if (miscast_level == 0) \ miscast_level = -1; \ return; \ } void melee_attack::chaos_affects_attacker() { if (miscast_level >= 1 || !attacker->alive()) return; // Move stairs out from under the attacker. if (one_chance_in(100) && _move_stairs(attacker, defender)) { #ifdef NOTE_DEBUG_CHAOS_EFFECTS take_note(Note(NOTE_MESSAGE, 0, 0, "CHAOS affects attacker: move stairs"), true); #endif DID_AFFECT(); } // Dump attacker or items under attacker to another level. if (is_valid_shaft_level() && (attacker->will_trigger_shaft() || igrd(attacker->pos()) != NON_ITEM) && one_chance_in(1000)) { (void) attacker->do_shaft(); #ifdef NOTE_DEBUG_CHAOS_EFFECTS take_note(Note(NOTE_MESSAGE, 0, 0, "CHAOS affects attacker: shaft effect"), true); #endif DID_AFFECT(); } // Create a colourful cloud. if (weapon && one_chance_in(1000)) { mprf("Smoke pours forth from %s!", wep_name(DESC_NOCAP_YOUR).c_str()); big_cloud(random_smoke_type(), KC_OTHER, attacker->pos(), 20, 4 + random2(8)); #ifdef NOTE_DEBUG_CHAOS_EFFECTS take_note(Note(NOTE_MESSAGE, 0, 0, "CHAOS affects attacker: smoke"), true); #endif DID_AFFECT(); } // Make a loud noise. if (weapon && player_can_hear(attacker->pos()) && one_chance_in(200)) { std::string msg = ""; if (!you.can_see(attacker)) { std::string noise = getSpeakString("weapon_noise"); if (!noise.empty()) msg = "You hear " + noise; } else { msg = getSpeakString("weapon_noises"); std::string wepname = wep_name(DESC_CAP_YOUR); if (!msg.empty()) { msg = replace_all(msg, "@Your_weapon@", wepname); msg = replace_all(msg, "@The_weapon@", wepname); } } if (!msg.empty()) { mpr(msg.c_str(), MSGCH_SOUND); noisy(15, attacker->pos(), attacker->mindex()); #ifdef NOTE_DEBUG_CHAOS_EFFECTS take_note(Note(NOTE_MESSAGE, 0, 0, "CHAOS affects attacker: noise"), true); #endif DID_AFFECT(); } } } static void _find_remains(monsters* mon, int &corpse_class, int &corpse_index, item_def &fake_corpse, int &last_item, std::vector items) { for (int i = 0; i < NUM_MONSTER_SLOTS; ++i) { const int idx = mon->inv[i]; if (idx == NON_ITEM) continue; item_def &item(mitm[idx]); if (!item.is_valid() || item.pos != mon->pos()) continue; items.push_back(idx); } corpse_index = NON_ITEM; last_item = NON_ITEM; corpse_class = fill_out_corpse(mon, fake_corpse, true); if (corpse_class == -1 || mons_weight(corpse_class) == 0) return; // Stop at first non-matching corpse, since the freshest corpse will // be at the top of the stack. for (stack_iterator si(mon->pos()); si; ++si) { if (si->base_type == OBJ_CORPSES && si->sub_type == CORPSE_BODY) { if (si->orig_monnum != fake_corpse.orig_monnum || si->plus != fake_corpse.plus || si->plus2 != fake_corpse.plus2 || si->special != fake_corpse.special || si->flags != fake_corpse.flags) { break; } // If it's a hydra the number of heads must match. if ((short) mon->number != si->props[MONSTER_NUMBER].get_short()) break; // Got it! corpse_index = si.link(); break; } else { // Last item which we're sure belonged to the monster. for (unsigned int i = 0; i < items.size(); i++) if (items[i] == si.link()) last_item = si.link(); } } } static bool _make_zombie(monsters* mon, int corpse_class, int corpse_index, item_def &fake_corpse, int last_item) { // If the monster dropped a corpse, then don't waste it by turning // it into a zombie. if (corpse_index != NON_ITEM || !mons_class_can_be_zombified(corpse_class)) return (false); // Good gods won't let their gifts/followers be raised as the // undead. if (is_good_god(mon->god)) return (false); // First attempt to raise zombie fitted out with all its old // equipment. int zombie_index = -1; int idx = get_item_slot(0); if (idx != NON_ITEM && last_item != NON_ITEM) { mitm[idx] = fake_corpse; mitm[idx].pos = mon->pos(); // Insert it in the item stack right after the monster's last // item, so it will be equipped with all the monster's items. mitm[idx].link = mitm[last_item].link; mitm[last_item].link = idx; animate_remains(mon->pos(), CORPSE_BODY, mon->behaviour, mon->foe, 0, "a chaos effect", mon->god, true, true, true, &zombie_index); } // No equipment to get, or couldn't get it for some reason. if (zombie_index == -1) { monster_type type = (mons_zombie_size(mon->type) == Z_SMALL) ? MONS_ZOMBIE_SMALL : MONS_ZOMBIE_LARGE; mgen_data mg(type, mon->behaviour, 0, 0, 0, mon->pos(), mon->foe, MG_FORCE_PLACE, mon->god, mon->type, mon->number); mg.non_actor_summoner = "a chaos effect"; zombie_index = create_monster(mg); } if (zombie_index == -1) return (false); monsters *zombie = &menv[zombie_index]; // Attempt to force zombie into exact same spot. if (zombie->pos() != mon->pos() && zombie->is_habitable(mon->pos())) zombie->move_to_pos(mon->pos()); if (you.can_see(mon)) { if (you.can_see(zombie)) simple_monster_message(mon, " instantly turns into a zombie!"); else if (last_item != NON_ITEM) { simple_monster_message(mon, "'s equipment vanishes!"); autotoggle_autopickup(true); } } else { simple_monster_message(zombie, " appears from thin air!"); autotoggle_autopickup(false); } player_angers_monster(zombie); return (true); } // mon is a copy of the monster from before monster_die() was called, // though its hitpoints may be non-positive. // // NOTE: Isn't called if monster dies from poisoning caused by chaos. void melee_attack::chaos_killed_defender(monsters* mon) { ASSERT(mon->type != -1 && mon->type != MONS_NO_MONSTER); ASSERT(in_bounds(mon->pos())); ASSERT(!defender->alive()); if (!attacker->alive()) return; if (attacker->atype() == ACT_PLAYER && you.banished) return; if (mon->is_summoned() || (mon->flags & (MF_BANISHED | MF_HARD_RESET))) return; int corpse_class, corpse_index, last_item; item_def fake_corpse; std::vector items; _find_remains(mon, corpse_class, corpse_index, fake_corpse, last_item, items); if (one_chance_in(100) && _make_zombie(mon, corpse_class, corpse_index, fake_corpse, last_item)) { #ifdef NOTE_DEBUG_CHAOS_EFFECTS take_note(Note(NOTE_MESSAGE, 0, 0, "CHAOS killed defender: zombified monster"), true); #endif DID_AFFECT(); } } void melee_attack::do_miscast() { if (miscast_level == -1) return; ASSERT(miscast_target != NULL); ASSERT(miscast_level >= 0 && miscast_level <= 3); ASSERT(count_bits(miscast_type) == 1); if (!miscast_target->alive()) return; if (miscast_target->atype() == ACT_PLAYER && you.banished) return; const bool chaos_brand = weapon && get_weapon_brand(*weapon) == SPWPN_CHAOS; // If the miscast is happening on the attacker's side and is due to // a chaos weapon then make smoke/sand/etc pour out of the weapon // instead of the attacker's hands. std::string hand_str; std::string cause = atk_name(DESC_NOCAP_THE); int source; const int ignore_mask = ISFLAG_KNOW_CURSE | ISFLAG_KNOW_PLUSES; if (attacker->atype() == ACT_PLAYER) { source = NON_MONSTER; if (chaos_brand) { cause = "a chaos effect from "; // Ignore a lot of item flags to make cause as short as possible, // so it will (hopefully) fit onto a single line in the death // cause screen. cause += wep_name(DESC_NOCAP_YOUR, ignore_mask | ISFLAG_COSMETIC_MASK | ISFLAG_RACIAL_MASK); if (miscast_target == attacker) hand_str = wep_name(DESC_PLAIN, ignore_mask); } } else { source = attacker->mindex(); if (chaos_brand && miscast_target == attacker && you.can_see(attacker)) { hand_str = wep_name(DESC_PLAIN, ignore_mask); } } MiscastEffect(miscast_target, source, (spschool_flag_type) miscast_type, miscast_level, cause, NH_NEVER, 0, hand_str, false); // Don't do miscast twice for one attack. miscast_level = -1; } // NOTE: random_chaos_brand() and random_chaos_attack_flavour() should // return a set of effects that are roughly the same, to make it easy // for chaos_affects_defender() not to do duplicate effects caused // by the non-chaos brands/flavours they return. int melee_attack::random_chaos_brand() { int brand = SPWPN_NORMAL; // Assuming the chaos to be mildly intelligent, try to avoid brands // that clash with the most basic resists of the defender, // i.e. its holiness. while (true) { brand = (random_choose_weighted( 5, SPWPN_VORPAL, 10, SPWPN_FLAMING, 10, SPWPN_FREEZING, 10, SPWPN_ELECTROCUTION, 10, SPWPN_VENOM, 10, SPWPN_CHAOS, 5, SPWPN_DRAINING, 5, SPWPN_VAMPIRICISM, 5, SPWPN_HOLY_WRATH, 2, SPWPN_CONFUSE, 2, SPWPN_DISTORTION, 0)); if (one_chance_in(3)) break; bool susceptible = true; switch (brand) { case SPWPN_FLAMING: if (defender->is_fiery()) susceptible = false; break; case SPWPN_FREEZING: if (defender->is_icy()) susceptible = false; break; case SPWPN_ELECTROCUTION: if (defender->airborne()) susceptible = false; break; case SPWPN_VENOM: if (defender->holiness() == MH_UNDEAD) susceptible = false; break; case SPWPN_VAMPIRICISM: if (defender->is_summoned()) { susceptible = false; break; } // intentional fall-through case SPWPN_DRAINING: if (defender->holiness() != MH_NATURAL) susceptible = false; break; case SPWPN_HOLY_WRATH: if (defender->holiness() != MH_UNDEAD && defender->holiness() != MH_DEMONIC) { susceptible = false; } break; case SPWPN_CONFUSE: if (defender->holiness() == MH_NONLIVING || defender->holiness() == MH_PLANT) { susceptible = false; } break; default: break; } if (susceptible) break; } #ifdef NOTE_DEBUG_CHAOS_BRAND std::string brand_name = "CHAOS brand: "; switch (brand) { case SPWPN_NORMAL: brand_name += "(plain)"; break; case SPWPN_FLAMING: brand_name += "flaming"; break; case SPWPN_FREEZING: brand_name += "freezing"; break; case SPWPN_HOLY_WRATH: brand_name += "holy wrath"; break; case SPWPN_ELECTROCUTION: brand_name += "electrocution"; break; case SPWPN_VENOM: brand_name += "venom"; break; case SPWPN_DRAINING: brand_name += "draining"; break; case SPWPN_DISTORTION: brand_name += "distortion"; break; case SPWPN_VAMPIRICISM: brand_name += "vampiricism"; break; case SPWPN_VORPAL: brand_name += "vorpal"; break; // ranged weapon brands case SPWPN_FLAME: brand_name += "flame"; break; case SPWPN_FROST: brand_name += "frost"; break; // both ranged and non-ranged case SPWPN_CHAOS: brand_name += "chaos"; break; case SPWPN_CONFUSE: brand_name += "confusion"; break; default: brand_name += "(other)"; break; } // Pretty much duplicated by the chaos effect note, // which will be much more informative. if (brand != SPWPN_CHAOS) take_note(Note(NOTE_MESSAGE, 0, 0, brand_name.c_str()), true); #endif return (brand); } mon_attack_flavour melee_attack::random_chaos_attack_flavour() { mon_attack_flavour flavours[] = {AF_FIRE, AF_COLD, AF_ELEC, AF_POISON_NASTY, AF_VAMPIRIC, AF_DISTORT, AF_CONFUSE, AF_CHAOS}; return (RANDOM_ELEMENT(flavours)); } bool melee_attack::apply_damage_brand() { bool brand_was_known = false; if (weapon) { if (is_artefact(*weapon)) brand_was_known = artefact_known_wpn_property(*weapon, ARTP_BRAND); else brand_was_known = item_type_known(*weapon); } bool ret = false; // Monster resistance to the brand. int res = 0; special_damage = 0; obvious_effect = false; int brand; if (damage_brand == SPWPN_CHAOS) brand = random_chaos_brand(); else brand = damage_brand; switch (brand) { case SPWPN_FLAMING: res = fire_res_apply_cerebov_downgrade(defender->res_fire()); calc_elemental_brand_damage(BEAM_FIRE, res, defender->is_icy() ? "melt" : "burn"); defender->expose_to_element(BEAM_FIRE); extra_noise += 1; break; case SPWPN_FREEZING: calc_elemental_brand_damage(BEAM_COLD, defender->res_cold(), "freeze"); defender->expose_to_element(BEAM_COLD); break; case SPWPN_HOLY_WRATH: if (defender->undead_or_demonic()) special_damage = 1 + (random2(damage_done * 15) / 10); if (special_damage && defender_visible) { special_damage_message = make_stringf( "%s %s%s", def_name(DESC_CAP_THE).c_str(), defender->conj_verb("convulse").c_str(), special_attack_punctuation().c_str()); } break; case SPWPN_ELECTROCUTION: extra_noise += 2; if (defender->airborne() || defender->res_elec() > 0) break; else if (one_chance_in(3)) { special_damage_message = defender->atype() == ACT_PLAYER? "You are electrocuted!" : "There is a sudden explosion of sparks!"; special_damage = 10 + random2(15); special_damage_flavour = BEAM_ELECTRICITY; // Check for arcing in water, and add the final effect. const coord_def& pos = defender->pos(); // We know the defender is neither airborne nor electricity // resistant, from above, but is it on water? if (feat_is_water(grd(pos))) { attack_final_effect effect; effect.flavor = FINEFF_LIGHTNING_DISCHARGE; effect.location = pos; final_effects.push_back(effect); } } break; case SPWPN_ORC_SLAYING: if (is_orckind(defender)) { special_damage = 1 + random2(3*damage_done/2); if (defender_visible) { special_damage_message = make_stringf( "%s %s%s", defender->name(DESC_CAP_THE).c_str(), defender->conj_verb("convulse").c_str(), special_attack_punctuation().c_str()); } } break; case SPWPN_DRAGON_SLAYING: if (is_dragonkind(defender)) { special_damage = 1 + random2(3*damage_done/2); if (defender_visible) { special_damage_message = make_stringf( "%s %s%s", defender->name(DESC_CAP_THE).c_str(), defender->conj_verb("convulse").c_str(), special_attack_punctuation().c_str()); } } break; case SPWPN_VENOM: if (!one_chance_in(4)) { int old_poison; if (defender->atype() == ACT_PLAYER) old_poison = you.duration[DUR_POISONING]; else { old_poison = (defender_as_monster()->get_ench(ENCH_POISON)).degree; } // Poison monster message needs to arrive after hit message. emit_nodmg_hit_message(); // Weapons of venom do two levels of poisoning to the player, // but only one level to monsters. defender->poison(attacker, 2); if (defender->atype() == ACT_PLAYER && old_poison < you.duration[DUR_POISONING] || defender->atype() != ACT_PLAYER && old_poison < (defender_as_monster()->get_ench(ENCH_POISON)).degree) { obvious_effect = true; } } break; case SPWPN_DRAINING: drain_defender(); break; case SPWPN_VORPAL: special_damage = 1 + random2(damage_done) / 4; // Note: Leaving special_damage_message empty because there isn't one. break; case SPWPN_VAMPIRICISM: { // Vampire bat form -- why the special handling? if (attacker->atype() == ACT_PLAYER && you.species == SP_VAMPIRE && player_in_bat_form()) { _player_vampire_draws_blood(defender_as_monster(), damage_done); break; } if (x_chance_in_y(defender->res_negative_energy(), 3)) break; if (!weapon || defender->holiness() != MH_NATURAL || damage_done < 1 || attacker->stat_hp() == attacker->stat_maxhp() || defender->atype() != ACT_PLAYER && defender_as_monster()->is_summoned() || one_chance_in(5)) { break; } obvious_effect = true; // Handle weapon effects. // We only get here if we've done base damage, so no // worries on that score. if (attacker->atype() == ACT_PLAYER) mpr("You feel better."); else if (attacker_visible) { if (defender->atype() == ACT_PLAYER) { mprf("%s draws strength from your injuries!", attacker->name(DESC_CAP_THE).c_str()); } else { mprf("%s is healed.", attacker->name(DESC_CAP_THE).c_str()); } } int hp_boost = 0; // Thus is probably more valuable on larger weapons? if (weapon && is_unrandom_artefact(*weapon) && weapon->special == UNRAND_VAMPIRES_TOOTH) { hp_boost = damage_done; } else hp_boost = 1 + random2(damage_done); attacker->heal(hp_boost); if (attacker->hunger_level() != HS_ENGORGED) attacker->make_hungry(-random2avg(59, 2)); attacker->god_conduct(DID_NECROMANCY, 2); break; } case SPWPN_PAIN: if (defender->res_negative_energy()) break; if (x_chance_in_y(attacker->skill(SK_NECROMANCY) + 1, 8)) { if (defender_visible) { special_damage_message = make_stringf("%s %s in agony.", defender->name(DESC_CAP_THE).c_str(), defender->conj_verb("writhe").c_str()); } special_damage += random2( 1 + attacker->skill(SK_NECROMANCY) ); } attacker->god_conduct(DID_NECROMANCY, 4); break; case SPWPN_DISTORTION: ret = distortion_affects_defender(); break; case SPWPN_CONFUSE: { // This was originally for confusing touch and it doesn't really // work on the player, but a monster with a chaos weapon will // occassionally come up with this brand. -cao if (defender->atype() == ACT_PLAYER) break; emit_nodmg_hit_message(); const int hdcheck = (defender->holiness() == MH_NATURAL ? random2(30) : random2(22)); if (mons_class_is_confusable(defender->id()) && hdcheck >= defender->get_experience_level()) { // Declaring these just to pass to the enchant function. bolt beam_temp; beam_temp.thrower = attacker->atype() == ACT_PLAYER? KILL_YOU : KILL_MON; beam_temp.flavour = BEAM_CONFUSION; beam_temp.beam_source = attacker->mindex(); beam_temp.apply_enchantment_to_monster(defender_as_monster()); obvious_effect = beam_temp.obvious_effect; } if (attacker->atype() == ACT_PLAYER && damage_brand == SPWPN_CONFUSE) { ASSERT(you.duration[DUR_CONFUSING_TOUCH]); you.duration[DUR_CONFUSING_TOUCH] -= roll_dice(3, 5) * BASELINE_DELAY; if (you.duration[DUR_CONFUSING_TOUCH] < 1) you.duration[DUR_CONFUSING_TOUCH] = 1; obvious_effect = false; } break; } case SPWPN_CHAOS: chaos_affects_defender(); break; } if (damage_brand == SPWPN_CHAOS && brand != SPWPN_CHAOS && !ret && miscast_level == -1 && one_chance_in(20)) { miscast_level = 0; miscast_type = SPTYP_RANDOM; miscast_target = coinflip() ? attacker : defender; } if (attacker->atype() == ACT_PLAYER && damage_brand == SPWPN_CHAOS) { // If your god objects to using chaos, then it makes the // brand obvious. if (did_god_conduct(DID_CHAOS, 2 + random2(3), brand_was_known)) obvious_effect = true; } if (!obvious_effect) obvious_effect = !special_damage_message.empty(); if (obvious_effect && attacker_visible && weapon != NULL) { if (is_artefact(*weapon)) artefact_wpn_learn_prop(*weapon, ARTP_BRAND); else set_ident_flags(*weapon, ISFLAG_KNOW_TYPE); } return (ret); } // XXX: // * Noise should probably scale non-linearly with damage_done, and // maybe even non-linearly with noise_factor. // // * Damage reduction via armour of the defender reduces noise, // but shouldn't. // // * Damage reduction because of negative damage modifiers on the // weapon reduce noise, but probably shouldn't. // // * Might want a different formula for noise generated by the // player. // // Ideas: // * Each weapon type has a noise rating, like it does an accuracy // rating and base delay. // // * For player, stealth skill and/or weapon skillr reducing noise. // // * Randart property to make randart weapons louder or softer when // they hit. void melee_attack::handle_noise(const coord_def & pos) { // Successful stabs make no noise. if (stab_attempt) { noise_factor = 0; extra_noise = 0; return; } int level = (noise_factor * damage_done / 100 / 4) + extra_noise; if (noise_factor > 0) level = std::max(1, level); if (level > 0) noisy(level, pos, attacker->mindex()); noise_factor = 0; extra_noise = 0; } // Returns true if the attack cut off a head *and* cauterized it. bool melee_attack::chop_hydra_head( int dam, int dam_type, int wpn_brand ) { // Monster attackers have only a 25% chance of making the // chop-check to prevent runaway head inflation. if (attacker->atype() == ACT_MONSTER && !one_chance_in(4)) return (false); if ((dam_type == DVORP_SLICING || dam_type == DVORP_CHOPPING || dam_type == DVORP_CLAWING) && dam > 0 && (dam >= 4 || wpn_brand == SPWPN_VORPAL || coinflip())) { const char *verb = NULL; if (dam_type == DVORP_CLAWING) { static const char *claw_verbs[] = { "rip", "tear", "claw" }; verb = RANDOM_ELEMENT(claw_verbs); } else { static const char *slice_verbs[] = { "slice", "lop", "chop", "hack" }; verb = RANDOM_ELEMENT(slice_verbs); } if (defender_as_monster()->number == 1) // will be zero afterwards { if (defender_visible) { mprf("%s %s %s's last head off!", atk_name(DESC_CAP_THE).c_str(), attacker->conj_verb(verb).c_str(), def_name(DESC_NOCAP_THE).c_str()); } defender_as_monster()->number--; if (!defender->is_summoned()) bleed_onto_floor(defender->pos(), defender->id(), defender_as_monster()->hit_points, true); defender->hurt(attacker, defender_as_monster()->hit_points); return (true); } else { if (defender_visible) { mprf("%s %s one of %s's heads off!", atk_name(DESC_CAP_THE).c_str(), attacker->conj_verb(verb).c_str(), def_name(DESC_NOCAP_THE).c_str()); } defender_as_monster()->number--; // Only living hydras get to regenerate heads. if (defender->holiness() == MH_NATURAL) { unsigned int limit = 20; if (defender->id() == MONS_LERNAEAN_HYDRA) limit = 27; if (wpn_brand == SPWPN_FLAMING) { if (defender_visible) mpr( "The flame cauterises the wound!" ); return (true); } else if (defender_as_monster()->number < limit - 1) { simple_monster_message(defender_as_monster(), " grows two more!" ); defender_as_monster()->number += 2; defender->heal(8 + random2(8), true); } } } } return (false); } bool melee_attack::decapitate_hydra(int dam, int damage_type) { if (defender->atype() == ACT_MONSTER && defender_as_monster()->has_hydra_multi_attack() && defender->id() != MONS_SPECTRAL_THING) { const int dam_type = (damage_type != -1) ? damage_type : attacker->damage_type(); const int wpn_brand = attacker->damage_brand(); return chop_hydra_head(dam, dam_type, wpn_brand); } return (false); } void melee_attack::player_sustain_passive_damage() { if (mons_class_flag(defender->id(), M_ACID_SPLASH)) weapon_acid(5); } int melee_attack::player_staff_damage(int skill) { return (random2(5*(you.skills[skill] + you.skills[SK_EVOCATIONS])/4)); } void melee_attack::emit_nodmg_hit_message() { if (!no_damage_message.empty()) { mprf("%s", no_damage_message.c_str()); no_damage_message.clear(); } } void melee_attack::player_apply_staff_damage() { special_damage = 0; if (!weapon || !item_is_staff(*weapon)) return; if (random2(15) > you.skills[SK_EVOCATIONS]) { return; } switch (weapon->sub_type) { case STAFF_AIR: if (damage_done + you.skills[SK_AIR_MAGIC] <= random2(20)) break; special_damage = resist_adjust_damage(defender, BEAM_ELECTRICITY, defender->res_elec(), player_staff_damage(SK_AIR_MAGIC)); if (special_damage) { special_damage_message = make_stringf("%s is jolted!", defender->name(DESC_CAP_THE).c_str()); special_damage_flavour = BEAM_ELECTRICITY; } break; case STAFF_COLD: special_damage = resist_adjust_damage(defender, BEAM_COLD, defender->res_cold(), player_staff_damage(SK_ICE_MAGIC)); if (special_damage) { special_damage_message = make_stringf( "You freeze %s!", defender->name(DESC_NOCAP_THE).c_str()); } break; case STAFF_EARTH: special_damage = player_staff_damage(SK_EARTH_MAGIC); special_damage = player_apply_monster_ac(special_damage); if (special_damage > 0) { special_damage_message = make_stringf( "You crush %s!", defender->name(DESC_NOCAP_THE).c_str()); } break; case STAFF_FIRE: special_damage = resist_adjust_damage(defender, BEAM_FIRE, defender->res_fire(), player_staff_damage(SK_FIRE_MAGIC)); if (special_damage) { special_damage_message = make_stringf( "You burn %s!", defender->name(DESC_NOCAP_THE).c_str()); } break; case STAFF_POISON: { // Cap chance at 30% -- let staff of Olgreb shine. int temp_rand = damage_done + you.skills[SK_POISON_MAGIC]; if (temp_rand > 30) temp_rand = 30; if (x_chance_in_y(temp_rand, 100)) { // Poison monster message needs to arrive after hit message. emit_nodmg_hit_message(); poison_monster(defender_as_monster(), KC_YOU); } break; } case STAFF_DEATH: if (defender->res_negative_energy()) break; if (x_chance_in_y(you.skills[SK_NECROMANCY] + 1, 8)) { special_damage = player_staff_damage(SK_NECROMANCY); if (special_damage) { special_damage_message = make_stringf( "%s convulses in agony!", defender->name(DESC_CAP_THE).c_str()); did_god_conduct(DID_NECROMANCY, 4); } } break; case STAFF_POWER: case STAFF_SUMMONING: case STAFF_CHANNELING: case STAFF_CONJURATION: case STAFF_ENCHANTMENT: case STAFF_ENERGY: case STAFF_WIZARDRY: break; default: mpr("You're wielding some staff I've never heard of! (fight.cc)", MSGCH_ERROR); break; } if (special_damage > 0) { if (!item_type_known(*weapon)) { set_ident_flags( *weapon, ISFLAG_KNOW_TYPE ); set_ident_type( *weapon, ID_KNOWN_TYPE ); mprf("You are wielding %s.", weapon->name(DESC_NOCAP_A).c_str()); more(); you.wield_change = true; } } } bool melee_attack::player_check_monster_died() { if (!defender->alive()) { // note: doesn't take account of special weapons, etc. dprf("Hit for %d.", damage_done); player_monattk_hit_effects(true); _monster_die(defender_as_monster(), KILL_YOU, NON_MONSTER); return (true); } return (false); } void melee_attack::player_calc_hit_damage() { int potential_damage; potential_damage = !weapon ? player_calc_base_unarmed_damage() : player_calc_base_weapon_damage(); potential_damage = player_stat_modify_damage(potential_damage); if (water_attack) potential_damage = player_apply_water_attack_bonus(potential_damage); // apply damage bonus from ring of slaying // (before randomization -- some of these rings // are stupidly powerful) -- GDL potential_damage += slaying_bonus(PWPN_DAMAGE); damage_done = potential_damage > 0 ? one_chance_in(3) + random2(potential_damage) : 0; damage_done = player_apply_weapon_skill(damage_done); damage_done = player_apply_fighting_skill(damage_done, false); damage_done = player_apply_misc_modifiers(damage_done); damage_done = player_apply_weapon_bonuses(damage_done); player_weapon_auto_id(); damage_done = player_stab(damage_done); damage_done = player_apply_monster_ac(damage_done); // This doesn't actually modify damage. -- bwr // It only chooses the appropriate verb. damage_done = std::max(0, player_weapon_type_modify(damage_done)); } int melee_attack::calc_to_hit(bool random) { return (attacker->atype() == ACT_PLAYER ? player_to_hit(random) : mons_to_hit()); } int melee_attack::player_to_hit(bool random_factor) { heavy_armour_penalty = calc_heavy_armour_penalty(random_factor); can_do_unarmed = player_fights_well_unarmed(heavy_armour_penalty); hand_half_bonus = unarmed_ok && !can_do_unarmed && !shield && weapon && !weapon ->cursed() && hands == HANDS_HALF; int your_to_hit = 15 + (calc_stat_to_hit_base() / 2); #ifdef DEBUG_DIAGNOSTICS const int base_to_hit = your_to_hit; #endif if (water_attack) your_to_hit += 5; if (wearing_amulet(AMU_INACCURACY)) your_to_hit -= 5; // If you can't see yourself, you're a little less accurate. if (!you.visible_to(&you)) your_to_hit -= 5; // fighting contribution your_to_hit += maybe_random2(1 + you.skills[SK_FIGHTING], random_factor); // weapon skill contribution if (weapon) { if (wpn_skill != SK_FIGHTING) { if (you.skills[wpn_skill] < 1 && player_in_a_dangerous_place()) xom_is_stimulated(14); // Xom thinks that is mildly amusing. your_to_hit += maybe_random2(you.skills[wpn_skill] + 1, random_factor); } } else { // ...you must be unarmed your_to_hit += (you.species == SP_TROLL || you.species == SP_GHOUL) ? 4 : 2; your_to_hit += maybe_random2(1 + you.skills[SK_UNARMED_COMBAT], random_factor); } // weapon bonus contribution if (weapon) { if (weapon->base_type == OBJ_WEAPONS) { your_to_hit += weapon->plus; your_to_hit += property( *weapon, PWPN_HIT ); if (get_equip_race(*weapon) == ISFLAG_ELVEN && player_genus(GENPC_ELVEN)) { your_to_hit += (random_factor && coinflip() ? 2 : 1); } else if (get_equip_race(*weapon) == ISFLAG_ORCISH && you.religion == GOD_BEOGH && !player_under_penance()) { your_to_hit++; } } else if (weapon->base_type == OBJ_STAVES) { // magical staff your_to_hit += property( *weapon, PWPN_HIT ); if (item_is_rod( *weapon )) your_to_hit += (short)weapon->props["rod_enchantment"]; } } // slaying bonus your_to_hit += slaying_bonus(PWPN_HIT); // hunger penalty if (you.hunger_state == HS_STARVING) your_to_hit -= 3; // armour penalty your_to_hit -= heavy_armour_penalty; #if DEBUG_DIAGNOSTICS int roll_hit = your_to_hit; #endif // hit roll your_to_hit = maybe_random2(your_to_hit, random_factor); #if DEBUG_DIAGNOSTICS dprf( "to hit die: %d; rolled value: %d; base: %d", roll_hit, your_to_hit, base_to_hit ); #endif if (hand_half_bonus) your_to_hit += maybe_random2(3, random_factor); if (weapon && wpn_skill == SK_SHORT_BLADES && you.duration[DUR_SURE_BLADE]) { int turn_duration = you.duration[DUR_SURE_BLADE] / BASELINE_DELAY; your_to_hit += 5 + (random_factor ? random2limit( turn_duration, 10 ) : turn_duration / 2); } // other stuff if (!weapon) { if (you.duration[DUR_CONFUSING_TOUCH]) { // Just trying to touch is easier that trying to damage. your_to_hit += maybe_random2(you.dex, random_factor); } switch (you.attribute[ATTR_TRANSFORMATION]) { case TRAN_SPIDER: your_to_hit += maybe_random2(10, random_factor); break; case TRAN_BAT: your_to_hit += maybe_random2(12, random_factor); break; case TRAN_ICE_BEAST: your_to_hit += maybe_random2(10, random_factor); break; case TRAN_BLADE_HANDS: your_to_hit += maybe_random2(12, random_factor); break; case TRAN_STATUE: your_to_hit += maybe_random2(9, random_factor); break; case TRAN_DRAGON: your_to_hit += maybe_random2(10, random_factor); break; case TRAN_LICH: your_to_hit += maybe_random2(10, random_factor); break; case TRAN_NONE: default: break; } } // Check for backlight (Corona). if (defender && defender->atype() == ACT_MONSTER) { if (defender->backlit() && !defender->halo_radius()) your_to_hit += 2 + random2(8); // Invisible monsters are hard to hit. else if (!defender->visible_to(&you)) your_to_hit -= 6; } return (your_to_hit); } void melee_attack::player_stab_check() { // Unknown mimics cannot be stabbed. if (mons_is_unknown_mimic(defender_as_monster())) { stab_attempt = false; stab_bonus = 0; return; } const unchivalric_attack_type uat = is_unchivalric_attack(&you, defender); stab_attempt = (uat != UCAT_NO_ATTACK); const bool roll_needed = (uat != UCAT_SLEEPING && uat != UCAT_PARALYSED); int roll = 100; if (uat == UCAT_INVISIBLE && !mons_sense_invis(defender_as_monster())) roll -= 10; switch (uat) { case UCAT_NO_ATTACK: stab_bonus = 0; break; case UCAT_SLEEPING: case UCAT_PARALYSED: stab_bonus = 1; break; case UCAT_HELD_IN_NET: case UCAT_PETRIFYING: case UCAT_PETRIFIED: stab_bonus = 2; break; case UCAT_INVISIBLE: case UCAT_CONFUSED: case UCAT_FLEEING: stab_bonus = 4; break; case UCAT_DISTRACTED: stab_bonus = 6; break; } // See if we need to roll against dexterity / stabbing. if (stab_attempt && roll_needed) { stab_attempt = x_chance_in_y(you.skills[SK_STABBING] + you.dex + 1, roll); } } void melee_attack::player_apply_attack_delay() { int attack_delay = weapon ? player_weapon_speed() : player_unarmed_speed(); attack_delay = player_apply_shield_delay(attack_delay); if (attack_delay < 3) attack_delay = 3; final_attack_delay = attack_delay; you.time_taken = std::max(2, div_rand_round(you.time_taken * final_attack_delay, 10)); dprf( "Weapon speed: %d; min: %d; attack time: %d", final_attack_delay, min_delay, you.time_taken ); } int melee_attack::player_weapon_speed() { int attack_delay = 0; if (weapon && (weapon->base_type == OBJ_WEAPONS || weapon->base_type == OBJ_STAVES)) { attack_delay = property( *weapon, PWPN_SPEED ); attack_delay -= you.skills[ wpn_skill ] / 2; min_delay = property( *weapon, PWPN_SPEED ) / 2; // Short blades can get up to at least unarmed speed. if (wpn_skill == SK_SHORT_BLADES && min_delay > 5) min_delay = 5; // Using both hands can get a weapon up to speed 7 if ((hands == HANDS_TWO || hand_half_bonus) && min_delay > 7) { min_delay = 7; } // never go faster than speed 3 (ie 3 attacks per round) if (min_delay < 3) min_delay = 3; // Hand and a half bonus only helps speed up to a point, any more // than speed 10 must come from skill and the weapon if (hand_half_bonus && attack_delay > 10) attack_delay--; // apply minimum to weapon skill modification if (attack_delay < min_delay) attack_delay = min_delay; if (weapon->base_type == OBJ_WEAPONS && damage_brand == SPWPN_SPEED) { attack_delay = (attack_delay + 1) / 2; } } return (attack_delay); } int melee_attack::player_unarmed_speed() { int unarmed_delay = 10; // Not even bats can attack faster than this. min_delay = 5; // Unarmed speed. if (you.burden_state == BS_UNENCUMBERED && one_chance_in(heavy_armour_penalty + 1)) { unarmed_delay = std::max(10 - you.skills[SK_UNARMED_COMBAT] / (player_in_bat_form() ? 3 : 5), min_delay); } return (unarmed_delay); } int melee_attack::player_apply_shield_delay(int attack_delay) { if (shield) { switch (shield->sub_type) { case ARM_LARGE_SHIELD: if (you.skills[SK_SHIELDS] <= 10 + random2(17)) attack_delay++; // [dshaligram] Fall-through case ARM_SHIELD: if (you.skills[SK_SHIELDS] <= 3 + random2(17)) attack_delay++; break; } } return (attack_delay); } int melee_attack::player_calc_base_unarmed_damage() { int damage = 3; if (you.duration[DUR_CONFUSING_TOUCH]) { // No base hand damage while using this spell. damage = 0; } if (you.attribute[ATTR_TRANSFORMATION] != TRAN_NONE) { switch (you.attribute[ATTR_TRANSFORMATION]) { case TRAN_SPIDER: damage = 5; break; case TRAN_BAT: damage = (you.species == SP_VAMPIRE ? 2 : 1); break; case TRAN_ICE_BEAST: damage = 12; break; case TRAN_BLADE_HANDS: damage = 12 + (you.strength / 4) + (you.dex / 4); break; case TRAN_STATUE: damage = 12 + you.strength; break; case TRAN_DRAGON: damage = 20 + you.strength; break; case TRAN_LICH: damage = 5; break; } } else if (you.equip[ EQ_GLOVES ] == -1) { // Claw damage only applies for bare hands. if (you.species == SP_TROLL) damage += 5; else if (you.species == SP_GHOUL) damage += 2; damage += player_mutation_level(MUT_CLAWS) * 2; } if (player_in_bat_form()) { // Bats really don't do a lot of damage. damage += you.skills[SK_UNARMED_COMBAT] / 5; } else damage += you.skills[SK_UNARMED_COMBAT]; return (damage); } int melee_attack::player_calc_base_weapon_damage() { int damage = 0; if (weapon->base_type == OBJ_WEAPONS || weapon->base_type == OBJ_STAVES) damage = property( *weapon, PWPN_DAMAGE ); // Staves can be wielded with a worn shield, but are much less // effective. if (shield && weapon->base_type == OBJ_WEAPONS && weapon_skill(*weapon) == SK_STAVES && hands_reqd(*weapon, you.body_size()) == HANDS_HALF) { damage /= 2; } return (damage); } /////////////////////////////////////////////////////////////////////////// bool melee_attack::mons_attack_mons() { const coord_def atk_pos = attacker->pos(); const coord_def def_pos = defender->pos(); // Self-attacks never violate sanctuary. if ((is_sanctuary(atk_pos) || is_sanctuary(def_pos)) && attacker != defender) { // Friendly monsters should only violate sanctuary if explicitly // ordered to do so by the player. if (attacker_as_monster()->friendly()) { if (you.pet_target == MHITYOU || you.pet_target == MHITNOT) { if (attacker->confused() && you.can_see(attacker)) { mpr("Zin prevents your ally from violating sanctuary " "in its confusion.", MSGCH_GOD); } else if (attacker->berserk() && you.can_see(attacker)) { mpr("Zin prevents your ally from violating sanctuary " "in its berserker rage.", MSGCH_GOD); } cancel_attack = true; return (false); } } // Non-friendly monsters should never violate sanctuary. else { dprf("Preventing hostile violation of sanctuary."); cancel_attack = true; return (false); } } mons_perform_attack(); // If a hydra was attacking it may have switched targets and started // hitting the player. -cao if (defender->atype() == ACT_PLAYER) return (did_hit); if (perceived_attack && (defender_as_monster()->foe == MHITNOT || one_chance_in(3)) && attacker->alive() && defender->alive()) { behaviour_event(defender_as_monster(), ME_WHACK, attacker->mindex()); } // If an enemy attacked a friend, set the pet target if it isn't set // already, but not if sanctuary is in effect (pet target must be // set explicitly by the player during sanctuary). if (perceived_attack && attacker->alive() && defender_as_monster()->friendly() && !crawl_state.arena && !attacker_as_monster()->wont_attack() && you.pet_target == MHITNOT && env.sanctuary_time <= 0) { you.pet_target = attacker->mindex(); } return (did_hit); } bool melee_attack::mons_self_destructs() { if (attacker->id() == MONS_GIANT_SPORE || attacker->id() == MONS_BALL_LIGHTNING || attacker->id() == MONS_ORB_OF_DESTRUCTION) { attacker_as_monster()->hit_points = -1; // Do the explosion right now. monster_die(attacker_as_monster(), KILL_MON, attacker->mindex()); return (true); } return (false); } bool melee_attack::mons_attack_warded_off() { // [dshaligram] Note: warding is no longer a simple 50% chance. const int warding = defender->warding(); if (warding && attacker->is_summoned() && !attacker_as_monster()->check_res_magic(warding)) { if (needs_message) { mprf("%s tries to attack %s, but flinches away.", atk_name(DESC_CAP_THE).c_str(), mons_defender_name().c_str()); } return (true); } return (false); } int melee_attack::mons_attk_delay() { return (weapon ? property(*weapon, PWPN_SPEED) : 0); } bool melee_attack::attack_shield_blocked(bool verbose) { if (!defender_shield && defender->atype() != ACT_PLAYER) return (false); if (defender->incapacitated()) return (false); const int con_block = random2(attacker->shield_bypass_ability(to_hit) + defender->shield_block_penalty()); int pro_block = defender->shield_bonus(); if (!attacker->visible_to(defender)) pro_block /= 3; #ifdef DEBUG_DIAGNOSTICS mprf(MSGCH_DIAGNOSTICS, "Defender: %s, Pro-block: %d, Con-block: %d", def_name(DESC_PLAIN).c_str(), pro_block, con_block); #endif if (pro_block >= con_block) { perceived_attack = true; if (needs_message && verbose) { mprf("%s %s %s attack.", def_name(DESC_CAP_THE).c_str(), defender->conj_verb("block").c_str(), atk_name(DESC_NOCAP_ITS).c_str()); } defender->shield_block_succeeded(attacker); return (true); } return (false); } int melee_attack::mons_calc_damage(const mon_attack_def &attk) { int damage = 0; int damage_max = 0; if (weapon && weapon->base_type == OBJ_WEAPONS && !is_range_weapon(*weapon)) { damage_max = property( *weapon, PWPN_DAMAGE ); damage += random2( damage_max ); if (get_equip_race(*weapon) == ISFLAG_ORCISH && attacker->mons_species() == MONS_ORC && coinflip()) { damage++; } if (weapon->plus2 >= 0) damage += random2( weapon->plus2 ); else damage -= random2( 1 - weapon->plus2 ); damage -= 1 + random2(3); } damage_max += attk.damage; damage += 1 + random2(attk.damage); // Berserk/mighted/frenzied monsters get bonus damage. if (attacker_as_monster()->has_ench(ENCH_MIGHT)) damage = damage * 3 / 2; else if (attacker_as_monster()->has_ench(ENCH_BATTLE_FRENZY)) { const mon_enchant ench = attacker_as_monster()->get_ench(ENCH_BATTLE_FRENZY); #ifdef DEBUG_DIAGNOSTICS const int orig_damage = damage; #endif damage = damage * (115 + ench.degree * 15) / 100; #ifdef DEBUG_DIAGNOSTICS mprf(MSGCH_DIAGNOSTICS, "%s frenzy damage: %d->%d", attacker->name(DESC_PLAIN).c_str(), orig_damage, damage); #endif } if (water_attack) damage *= 2; // If the defender is asleep, the attacker gets a stab. if (defender && defender->asleep()) { damage = damage * 5 / 2; #ifdef DEBUG_DIAGNOSTICS mprf(MSGCH_DIAGNOSTICS, "Stab damage vs %s: %d", defender->name(DESC_PLAIN).c_str(), damage); #endif } return (mons_apply_defender_ac(damage, damage_max)); } int melee_attack::mons_apply_defender_ac(int damage, int damage_max) { int ac = defender->armour_class(); if (ac > 0) { int damage_reduction = random2(ac + 1); if (!defender->wearing_light_armour()) { if (const item_def *arm = defender->slot_item(EQ_BODY_ARMOUR)) { const int armac = property(*arm, PARM_AC); int perc = 2 * (defender->skill(SK_ARMOUR) + armac); if (perc > 50) perc = 50; int min = 1 + (damage_max * perc) / 100; if (min > ac / 2) min = ac / 2; if (damage_reduction < min) damage_reduction = min; } } damage -= damage_reduction; } if (damage < 1) damage = 0; return damage; } static const char *klown_attack[] = { "hit", "poke", "prod", "flog", "pound", "slap", "tickle", "defenestrate", "sucker-punch", "elbow", "pinch", "strangle-hug", "squeeze", "tease", "eye-gouge", "karate-kick", "headlock", "wrestle", "trip-wire", "kneecap" }; std::string melee_attack::mons_attack_verb(const mon_attack_def &attk) { if (attacker->id() == MONS_KILLER_KLOWN && attk.type == AT_HIT) return (RANDOM_ELEMENT(klown_attack)); if (attacker->id() == MONS_KRAKEN_TENTACLE && attk.type == AT_TENTACLE_SLAP) return ("slap"); static const char *attack_types[] = { "", "hit", // including weapon attacks "bite", "sting", // spore "hit", "touch", "engulf", "claw", "peck", "headbutt", "punch", "kick", "tentacle-slap", "tail-slap", "gore", "constrict" }; return (attack_types[attk.type]); } std::string melee_attack::mons_attack_desc(const mon_attack_def &attk) { if (!you.can_see(attacker)) return (""); if (weapon) { std::string result = ""; const item_def wpn = *weapon; if (get_weapon_brand(wpn) == SPWPN_REACHING) { if (grid_distance(attacker->pos(), defender->pos()) == 2) result += " from afar"; } if (attacker->id() != MONS_DANCING_WEAPON) { result += " with "; result += weapon->name(DESC_NOCAP_A); } return (result); } else if (attk.flavour == AF_REACH && grid_distance(attacker->pos(), defender->pos()) == 2) { return " from afar"; } return (""); } std::string melee_attack::mons_defender_name() { if (attacker == defender) return pronoun(attacker, PRONOUN_REFLEXIVE, attacker_visible); else return def_name(DESC_NOCAP_THE); } void melee_attack::mons_announce_hit(const mon_attack_def &attk) { if (water_attack && attacker_visible && attacker != defender) { mprf("%s uses the watery terrain to its advantage.", attacker->name(DESC_CAP_THE).c_str()); } if (needs_message) { mprf("%s %s %s%s%s%s", atk_name(DESC_CAP_THE).c_str(), attacker->conj_verb( mons_attack_verb(attk) ).c_str(), mons_defender_name().c_str(), debug_damage_number().c_str(), mons_attack_desc(attk).c_str(), attack_strength_punctuation().c_str()); } } void melee_attack::mons_announce_dud_hit(const mon_attack_def &attk) { if (needs_message) { mprf("%s %s %s but doesn't do any damage.", atk_name(DESC_CAP_THE).c_str(), attacker->conj_verb( mons_attack_verb(attk) ).c_str(), mons_defender_name().c_str()); } } void melee_attack::check_defender_train_dodging() { // It's possible to train both dodging and armour under the new scheme. if (defender->wearing_light_armour(true) && attacker_visible && one_chance_in(3)) { perceived_attack = true; defender->exercise(SK_DODGING, 1); } } void melee_attack::check_defender_train_armour() { if (defender->wearing_light_armour()) return; const item_def *arm = defender->slot_item(EQ_BODY_ARMOUR); if (arm && coinflip() && x_chance_in_y(item_mass(*arm) + 1, 1000)) defender->exercise(SK_ARMOUR, coinflip()? 2 : 1); } void melee_attack::mons_set_weapon(const mon_attack_def &attk) { weapon = (attk.type == AT_HIT) ? attacker->weapon(attack_number) : NULL; damage_brand = attacker->damage_brand(attack_number); } void melee_attack::mons_do_poison(const mon_attack_def &attk) { if (defender->res_poison() > 0) return; if (attk.flavour == AF_POISON_NASTY || one_chance_in(15 + 5 * (attk.flavour == AF_POISON ? 1 : 0)) || (damage_done > 1 && one_chance_in(attk.flavour == AF_POISON ? 4 : 3))) { if (needs_message) { if (defender->atype() == ACT_PLAYER && (attk.type == AT_BITE || attk.type == AT_STING)) { if (attacker_visible) { mprf("%s %s was poisonous!", apostrophise(attacker->name(DESC_CAP_THE)).c_str(), mons_attack_verb(attk).c_str()); } } else { mprf("%s poisons %s!", atk_name(DESC_CAP_THE).c_str(), mons_defender_name().c_str()); } } int amount = 1; if (attk.flavour == AF_POISON_NASTY) amount++; else if (attk.flavour == AF_POISON_MEDIUM) amount += random2(3); else if (attk.flavour == AF_POISON_STRONG) amount += roll_dice(2, 5); defender->poison(attacker, amount); } } void melee_attack::mons_do_napalm() { if (defender->res_sticky_flame()) return; if (one_chance_in(20) || (damage_done > 2 && one_chance_in(3))) { if (needs_message) { mprf("%s %s covered in liquid flames%s", def_name(DESC_CAP_THE).c_str(), defender->conj_verb("are").c_str(), special_attack_punctuation().c_str()); } if (defender->atype() == ACT_PLAYER) napalm_player(random2avg(7, 3) + 1); else { napalm_monster(defender_as_monster(), attacker_as_monster()->friendly() ? KC_FRIENDLY : KC_OTHER, std::min(4, 1 + random2(attacker->get_experience_level())/2)); } } } void melee_attack::wasp_paralyse_defender() { // [dshaligram] Adopted 4.1.2's wasp mechanics, in slightly modified // form. if (attacker->id() == MONS_RED_WASP || one_chance_in(3)) defender->poison(attacker, coinflip() ? 2 : 1); int paralyse_roll = (damage_done > 4 ? 3 : 20); if (attacker->id() == MONS_YELLOW_WASP) paralyse_roll += 3; if (defender->res_poison() <= 0) { if (one_chance_in(paralyse_roll)) defender->paralyse(attacker, roll_dice(1, 3)); else defender->slow_down(attacker, roll_dice(1, 3)); } } void melee_attack::splash_monster_with_acid(int strength) { special_damage += roll_dice(2, 4); if (defender_visible) mprf("%s is splashed with acid.", defender->name(DESC_CAP_THE).c_str()); } void melee_attack::splash_defender_with_acid(int strength) { if (defender->atype() == ACT_PLAYER) { mpr("You are splashed with acid!"); splash_with_acid(strength); } else splash_monster_with_acid(strength); } static void _steal_item_from_player(monsters *mon) { if (mon->confused()) { std::string msg = getSpeakString("Maurice confused nonstealing"); if (!msg.empty() && msg != "__NONE") { msg = replace_all(msg, "@The_monster@", mon->name(DESC_CAP_THE)); mpr(msg.c_str(), MSGCH_TALK); } return; } mon_inv_type mslot = NUM_MONSTER_SLOTS; int steal_what = -1; int total_value = 0; for (int m = 0; m < ENDOFPACK; ++m) { if (!you.inv[m].is_valid()) continue; // Cannot unequip player. // TODO: Allow stealing of the wielded weapon? // Needs to be unwielded properly and should never lead to // fatal stat loss. // 1KB: I'd say no, weapon is being held, it's different from pulling // a wand from your pocket. if (item_is_equipped(you.inv[m])) continue; mon_inv_type monslot = item_to_mslot(you.inv[m]); if (monslot == NUM_MONSTER_SLOTS) { // Try a related slot instead to allow for stealing of other // valuable items. if (you.inv[m].base_type == OBJ_BOOKS) monslot = MSLOT_SCROLL; else if (you.inv[m].base_type == OBJ_JEWELLERY) monslot = MSLOT_MISCELLANY; else continue; } // Only try to steal stuff we can still store somewhere. if (mon->inv[monslot] != NON_ITEM) { if (monslot == MSLOT_WEAPON && mon->inv[MSLOT_ALT_WEAPON] == NON_ITEM) { monslot = MSLOT_ALT_WEAPON; } else continue; } // Candidate for stealing. const int value = item_value(you.inv[m], true); total_value += value; if (x_chance_in_y(value, total_value)) { steal_what = m; mslot = monslot; } } if (steal_what == -1 || you.gold > 0 && one_chance_in(10)) { // Found no item worth stealing, try gold. if (you.gold == 0) { if (silenced(mon->pos())) return; std::string complaint = getSpeakString("Maurice nonstealing"); if (!complaint.empty()) { complaint = replace_all(complaint, "@The_monster@", mon->name(DESC_CAP_THE)); mpr(complaint.c_str(), MSGCH_TALK); } bolt beem; beem.source = mon->pos(); beem.target = mon->pos(); beem.beam_source = mon->mindex(); // Try to teleport away. if (mon->has_ench(ENCH_TP)) { mons_cast_noise(mon, beem, SPELL_BLINK); monster_blink(mon); } else mons_cast(mon, beem, SPELL_TELEPORT_SELF); return; } const int stolen_amount = std::min(20 + random2(800), you.gold); if (mon->inv[MSLOT_GOLD] != NON_ITEM) { // If Maurice already's got some gold, simply increase the amount. mitm[mon->inv[MSLOT_GOLD]].quantity += stolen_amount; } else { // Else create a new item for this pile of gold. const int idx = items(0, OBJ_GOLD, OBJ_RANDOM, true, 0, 0); if (idx == NON_ITEM) return; item_def &new_item = mitm[idx]; new_item.base_type = OBJ_GOLD; new_item.sub_type = 0; new_item.plus = 0; new_item.plus2 = 0; new_item.special = 0; new_item.flags = 0; new_item.link = NON_ITEM; new_item.quantity = stolen_amount; new_item.pos.reset(); item_colour(new_item); unlink_item(idx); mon->inv[MSLOT_GOLD] = idx; new_item.set_holding_monster(mon->mindex()); } mprf("%s steals %s your gold!", mon->name(DESC_CAP_THE).c_str(), stolen_amount == you.gold ? "all" : "some of"); you.attribute[ATTR_GOLD_FOUND] -= stolen_amount; you.del_gold(stolen_amount); return; } ASSERT(steal_what != -1); ASSERT(mslot != NUM_MONSTER_SLOTS); ASSERT(mon->inv[mslot] == NON_ITEM); // Create new item. int index = get_item_slot(10); if (index == NON_ITEM) return; item_def &new_item = mitm[index]; // Copy item. new_item = you.inv[steal_what]; // Set quantity, and set the item as unlinked. new_item.quantity -= random2(new_item.quantity); new_item.pos.reset(); new_item.link = NON_ITEM; mprf("%s steals %s!", mon->name(DESC_CAP_THE).c_str(), new_item.name(DESC_NOCAP_YOUR).c_str()); unlink_item(index); mon->inv[mslot] = index; new_item.set_holding_monster(mon->mindex()); // You'll want to autopickup it after killing Maurice. new_item.flags |= ISFLAG_THROWN; mon->equip(new_item, mslot, true); // Item is gone from player's inventory. dec_inv_item_quantity(steal_what, new_item.quantity); } void melee_attack::mons_apply_attack_flavour(const mon_attack_def &attk) { // Most of this is from BWR 4.1.2. mon_attack_flavour flavour = attk.flavour; if (flavour == AF_CHAOS) flavour = random_chaos_attack_flavour(); switch (flavour) { default: break; case AF_MUTATE: if (one_chance_in(4)) defender->mutate(); break; case AF_POISON: case AF_POISON_NASTY: case AF_POISON_MEDIUM: case AF_POISON_STRONG: mons_do_poison(attk); break; case AF_POISON_STR: if (defender->res_poison() <= 0) { defender->poison(attacker, roll_dice(1, 3)); if (one_chance_in(4)) defender->drain_stat(STAT_STRENGTH, 1, attacker); } break; case AF_ROT: if (one_chance_in(20) || (damage_done > 2 && one_chance_in(3))) rot_defender(2 + random2(3), damage_done > 5 ? 1 : 0); break; case AF_DISEASE: defender->sicken(50 + random2(100)); break; case AF_FIRE: if (attacker->id() == MONS_FIRE_VORTEX) attacker_as_monster()->hit_points = -10; special_damage = resist_adjust_damage(defender, BEAM_FIRE, defender->res_fire(), attacker->get_experience_level() + random2(attacker->get_experience_level())); if (needs_message && special_damage) { mprf("%s %s engulfed in flames%s", def_name(DESC_CAP_THE).c_str(), defender->conj_verb("are").c_str(), special_attack_punctuation().c_str()); } defender->expose_to_element(BEAM_FIRE, 2); break; case AF_COLD: special_damage = resist_adjust_damage(defender, BEAM_COLD, defender->res_cold(), attacker->get_experience_level() + random2(2 * attacker->get_experience_level())); if (needs_message && special_damage) { mprf("%s %s %s%s", atk_name(DESC_CAP_THE).c_str(), attacker->conj_verb("freeze").c_str(), mons_defender_name().c_str(), special_attack_punctuation().c_str()); } defender->expose_to_element(BEAM_COLD, 2); break; case AF_ELEC: special_damage = resist_adjust_damage( defender, BEAM_ELECTRICITY, defender->res_elec(), attacker->get_experience_level() + random2(attacker->get_experience_level() / 2)); special_damage_flavour = BEAM_ELECTRICITY; if (defender->airborne()) special_damage = special_damage * 2 / 3; if (needs_message && special_damage) { mprf("%s %s %s%s", atk_name(DESC_CAP_THE).c_str(), attacker->conj_verb("shock").c_str(), mons_defender_name().c_str(), special_attack_punctuation().c_str()); } dprf("Shock damage: %d", special_damage); break; case AF_VAMPIRIC: // Only may bite non-vampiric monsters (or player) capable of bleeding. if (!defender->can_bleed()) break; // Disallow draining of summoned monsters since they can't bleed. // XXX: Is this too harsh? if (defender->is_summoned()) break; if (x_chance_in_y(defender->res_negative_energy(), 3)) break; if (defender->stat_hp() < defender->stat_maxhp()) { attacker->heal(1 + random2(damage_done), coinflip()); if (needs_message) { mprf("%s %s strength from %s injuries!", atk_name(DESC_CAP_THE).c_str(), attacker->conj_verb("draw").c_str(), def_name(DESC_NOCAP_ITS).c_str()); } // 4.1.2 actually drains max hp; we're being nicer and just doing // a rot effect. if ((damage_done > 6 && one_chance_in(3)) || one_chance_in(20)) { if (defender->atype() == ACT_PLAYER) mprf("You feel less resilient."); rot_defender(0, coinflip() ? 2 : 1); } } break; case AF_DRAIN_STR: if ((one_chance_in(20) || damage_done > 0 && one_chance_in(3)) && defender->res_negative_energy() < random2(4)) { defender->drain_stat(STAT_STRENGTH, 1, attacker); } break; case AF_DRAIN_DEX: if ((one_chance_in(20) || (damage_done > 0 && one_chance_in(3))) && defender->res_negative_energy() < random2(4)) { defender->drain_stat(STAT_DEXTERITY, 1, attacker); } break; case AF_HUNGER: if (defender->holiness() == MH_UNDEAD) break; if (one_chance_in(20) || (damage_done > 0 && coinflip())) defender->make_hungry(400, false); break; case AF_BLINK: if (one_chance_in(3)) { if (attacker_visible) { mprf("%s %s!", attacker->name(DESC_CAP_THE).c_str(), attacker->conj_verb("blink").c_str()); } attacker->blink(); } break; case AF_CONFUSE: if (attk.type == AT_SPORE) { if (defender->res_poison() > 0) break; if (--(attacker_as_monster()->hit_dice) <= 0) attacker_as_monster()->hit_points = -1; if (defender_visible) { mprf("%s %s engulfed in a cloud of spores!", defender->name(DESC_CAP_THE).c_str(), defender->conj_verb("are").c_str()); } } if (one_chance_in(10) || (damage_done > 2 && one_chance_in(3))) { defender->confuse(attacker, 1 + random2(3+attacker->get_experience_level())); } break; case AF_DRAIN_XP: if (one_chance_in(30) || (damage_done > 5 && coinflip()) || (attk.damage == 0 && !one_chance_in(3))) { drain_defender(); } break; case AF_PARALYSE: // Only wasps at the moment. wasp_paralyse_defender(); break; case AF_ACID: if (attacker->id() == MONS_SPINY_WORM && defender->res_poison() <= 0) defender->poison(attacker, 2 + random2(4)); splash_defender_with_acid(3); break; case AF_DISTORT: distortion_affects_defender(); break; case AF_RAGE: if (!one_chance_in(3) || !defender->can_go_berserk()) break; if (needs_message) { mprf("%s %s %s!", atk_name(DESC_CAP_THE).c_str(), attacker->conj_verb("infuriate").c_str(), mons_defender_name().c_str()); } defender->go_berserk(false); break; case AF_NAPALM: mons_do_napalm(); break; case AF_CHAOS: chaos_affects_defender(); break; case AF_STEAL: // Ignore monsters, for now. if (defender->atype() != ACT_PLAYER) break; _steal_item_from_player(attacker_as_monster()); break; case AF_STEAL_FOOD: { // Monsters don't carry food. if (defender->atype() != ACT_PLAYER) break; const bool stolen = expose_player_to_element(BEAM_STEAL_FOOD, 10); const bool ground = expose_items_to_element(BEAM_STEAL_FOOD, you.pos(), 10); if (needs_message) { if (stolen) { mprf("%s steals some of your food!", atk_name(DESC_CAP_THE).c_str()); } else if (ground) { mprf("%s steals some of the food from beneath you!", atk_name(DESC_CAP_THE).c_str()); } } break; } case AF_CRUSH: mprf("%s %s being crushed%s", def_name(DESC_CAP_THE).c_str(), defender->conj_verb("are").c_str(), special_attack_punctuation().c_str()); break; } } void melee_attack::mons_perform_attack_rounds() { const int nrounds = attacker_as_monster()->has_hydra_multi_attack() ? attacker_as_monster()->number : 4; coord_def pos = defender->pos(); const bool was_delayed = you_are_delayed(); // Melee combat, tell attacker to wield its melee weapon. attacker_as_monster()->wield_melee_weapon(); monsters* def_copy = NULL; int effective_attack_number = 0; for (attack_number = 0; attack_number < nrounds; ++attack_number, ++effective_attack_number) { // Handle noise from previous round. if (effective_attack_number > 0) handle_noise(pos); // Monster went away? if (!defender->alive() || defender->pos() != pos) { if (attacker == defender || !attacker_as_monster()->has_multitargeting()) { break; } // Hydras can try and pick up a new monster to attack to // finish out their round. -cao bool end = true; for (adjacent_iterator i(attacker->pos()); i; ++i) { if (*i == you.pos() && !attacker_as_monster()->friendly()) { attacker_as_monster()->foe = MHITYOU; attacker_as_monster()->target = you.pos(); defender = &you; end = false; break; } monsters *mons = monster_at(*i); if (mons && !mons_aligned(attacker_as_monster()->mindex(), mons->mindex())) { defender = mons; end = false; pos = mons->pos(); break; } } // No adjacent hostiles. if (end) break; } // Monsters hitting themselves get just one round. if (attack_number > 0 && attacker == defender) break; init_attack(); mon_attack_def attk = mons_attack_spec(attacker_as_monster(), attack_number); if (attk.type == AT_WEAP_ONLY) { int weap = attacker_as_monster()->inv[MSLOT_WEAPON]; if (weap == NON_ITEM) attk.type = AT_NONE; else if (is_range_weapon(mitm[weap])) attk.type = AT_SHOOT; } if (attk.type == AT_NONE) { // Make sure the monster uses up some energy, even // though it didn't actually attack. if (effective_attack_number == 0) attacker_as_monster()->lose_energy(EUT_ATTACK); break; } // Skip dummy attacks. if ((!unarmed_ok && attk.type != AT_HIT && attk.flavour != AF_REACH) || attk.type == AT_SHOOT) { --effective_attack_number; continue; } if (weapon == NULL) { switch(attk.type) { case AT_HEADBUTT: case AT_TENTACLE_SLAP: case AT_TAIL_SLAP: noise_factor = 150; break; case AT_HIT: case AT_PUNCH: case AT_KICK: case AT_CLAW: case AT_GORE: noise_factor = 125; break; case AT_BITE: case AT_PECK: case AT_CONSTRICT: noise_factor = 100; break; case AT_STING: case AT_SPORE: case AT_ENGULF: noise_factor = 75; break; case AT_TOUCH: noise_factor = 0; break; // To prevent compiler warnings. case AT_NONE: case AT_RANDOM: case AT_SHOOT: DEBUGSTR("Invalid attack flavour for noise_factor"); break; default: DEBUGSTR("Unhandled attack flavour for noise_factor"); break; } switch(attk.flavour) { case AF_FIRE: noise_factor += 50; break; case AF_ELEC: noise_factor += 100; break; default: break; } } damage_done = 0; mons_set_weapon(attk); to_hit = mons_to_hit(); const bool chaos_attack = damage_brand == SPWPN_CHAOS || (attk.flavour == AF_CHAOS && attacker != defender); // Make copy of monster before monster_die() resets it. if (chaos_attack && defender->atype() == ACT_MONSTER && !def_copy) def_copy = new monsters(*defender_as_monster()); final_attack_delay = mons_attk_delay(); if (damage_brand == SPWPN_SPEED) final_attack_delay = final_attack_delay / 2 + 1; mons_lose_attack_energy(attacker_as_monster(), final_attack_delay, attack_number, effective_attack_number); bool shield_blocked = false; bool this_round_hit = false; if (attacker != defender) { if (attack_shield_blocked(true)) { shield_blocked = true; perceived_attack = true; this_round_hit = did_hit = true; } else check_defender_train_dodging(); } if (!shield_blocked) { const int defender_evasion = defender->melee_evasion(attacker); int defender_evasion_help = defender->melee_evasion(attacker, EV_IGNORE_HELPLESS); int defender_evasion_nophase = defender->melee_evasion(attacker, EV_IGNORE_PHASESHIFT); defer_rand r; if (defender_invisible) { // No evasion feedback if we don't know what we're fighting defender_evasion_help = defender_evasion; defender_evasion_nophase = defender_evasion; } if (attacker == defender || test_melee_hit(to_hit, defender_evasion_help, r)) { // would have hit no matter what this_round_hit = true; } else if (test_melee_hit(to_hit, defender_evasion, r)) { if (needs_message) { mprf("Helpless, %s %s to dodge %s attack.", mons_defender_name().c_str(), defender->conj_verb("fail").c_str(), atk_name(DESC_NOCAP_ITS).c_str()); } this_round_hit = true; } else if (test_melee_hit(to_hit, defender_evasion_nophase, r)) { if (needs_message) { mprf("%s momentarily %s out as %s attack passes through %s.", defender->name(DESC_CAP_THE).c_str(), defender->conj_verb("phase").c_str(), atk_name(DESC_NOCAP_ITS).c_str(), defender->pronoun(PRONOUN_NOCAP).c_str()); } this_round_hit = false; } else { // Misses no matter what if (needs_message) { mprf("%s misses %s.", atk_name(DESC_CAP_THE).c_str(), mons_defender_name().c_str()); } } if (this_round_hit) { did_hit = true; perceived_attack = true; damage_done = mons_calc_damage(attk); } else { perceived_attack = perceived_attack || attacker_visible; } } if (check_unrand_effects()) break; if (damage_done < 1 && this_round_hit && !shield_blocked) mons_announce_dud_hit(attk); if (damage_done > 0) { if (shield_blocked) dprf("ERROR: Non-zero damage after shield block!"); mons_announce_hit(attk); check_defender_train_armour(); if (defender->can_bleed() && !defender->is_summoned() && !defender->submerged()) { int blood = _modify_blood_amount(damage_done, attacker->damage_type()); if (blood > defender->stat_hp()) blood = defender->stat_hp(); bleed_onto_floor(pos, defender->id(), blood, true); } if (decapitate_hydra(damage_done, attacker->damage_type(attack_number))) { continue; } special_damage = 0; special_damage_message.clear(); special_damage_flavour = BEAM_NONE; // Monsters attacking themselves don't get attack flavour. // The message sequences look too weird. // Also, stealing attacks aren't handled until after the damage msg. if (attacker != defender && attk.flavour != AF_STEAL) mons_apply_attack_flavour(attk); if (!special_damage_message.empty()) mprf("%s", special_damage_message.c_str()); // Defender banished. Bail before chaos_killed_defender() // is called, since the defender is still alive in the // Abyss. if (is_banished(defender)) { if (chaos_attack && attacker->alive()) chaos_affects_attacker(); do_miscast(); break; } defender->hurt(attacker, damage_done + special_damage, special_damage_flavour); if (!defender->alive()) { if (chaos_attack && defender->atype() == ACT_MONSTER) chaos_killed_defender(def_copy); if (chaos_attack && attacker->alive()) chaos_affects_attacker(); do_miscast(); continue; } // Yredelemnul's injury mirroring can kill the attacker. // Also, bail if the monster is attacking itself without a // weapon, since intrinsic monster attack flavours aren't // applied for self-attacks. if (!attacker->alive() || (attacker == defender && !weapon)) { if (miscast_target == defender) do_miscast(); break; } special_damage = 0; special_damage_message.clear(); special_damage_flavour = BEAM_NONE; apply_damage_brand(); if (!special_damage_message.empty()) { mprf("%s", special_damage_message.c_str()); // Don't do message-only miscasts along with a special // damage message. if (miscast_level == 0) miscast_level = -1; } if (special_damage > 0) defender->hurt(attacker, special_damage, special_damage_flavour); if (!defender->alive()) { if (chaos_attack && defender->atype() == ACT_MONSTER) chaos_killed_defender(def_copy); if (chaos_attack && attacker->alive()) chaos_affects_attacker(); do_miscast(); continue; } if (chaos_attack && attacker->alive()) chaos_affects_attacker(); if (miscast_target == defender) do_miscast(); // Yredelemnul's injury mirroring can kill the attacker. if (!attacker->alive()) break; if (miscast_target == attacker) do_miscast(); // Miscast might have killed the attacker. if (!attacker->alive()) break; if (attk.flavour == AF_STEAL) mons_apply_attack_flavour(attk); } item_def *weap = attacker_as_monster()->mslot_item(MSLOT_WEAPON); if (weap && you.can_see(attacker) && weap->cursed() && is_range_weapon(*weap)) { set_ident_flags(*weap, ISFLAG_KNOW_CURSE); } } // Handle noise from last round. handle_noise(pos); if (def_copy) delete def_copy; // Invisible monster might have interrupted butchering. if (was_delayed && defender->atype() == ACT_PLAYER && perceived_attack && !attacker_visible) { handle_interrupted_swap(false, true); } } bool melee_attack::mons_perform_attack() { if (attacker != defender && mons_self_destructs()) return (did_hit = perceived_attack = true); if (attacker != defender && mons_attack_warded_off()) { // A warded-off attack takes half the normal energy. attacker->lose_energy(EUT_ATTACK, 2); perceived_attack = true; return (false); } mons_perform_attack_rounds(); return (did_hit); } void melee_attack::mons_check_attack_perceived() { if (!perceived_attack) return; if (defender->atype() == ACT_PLAYER) { interrupt_activity(AI_MONSTER_ATTACKS, attacker_as_monster()); // If a friend wants to help, they can attack the attacking // monster, unless sanctuary is in effect since pet_target can // only be changed explicitly by the player during sanctuary. if (you.pet_target == MHITNOT && env.sanctuary_time <= 0) you.pet_target = attacker->mindex(); } } bool melee_attack::mons_attack_you() { mons_perform_attack(); mons_check_attack_perceived(); return (did_hit); } int melee_attack::mons_to_hit() { const int hd_mult = mons_class_flag(attacker->id(), M_FIGHTER)? 25 : 15; int mhit = 18 + attacker->get_experience_level() * hd_mult / 10; #ifdef DEBUG_DIAGNOSTICS const int base_hit = mhit; #endif if (water_attack) mhit += 5; if (weapon && weapon->base_type == OBJ_WEAPONS) mhit += weapon->plus + property(*weapon, PWPN_HIT); if (attacker->confused()) mhit -= 5; if (defender->backlit() && !defender->halo_radius()) mhit += 2 + random2(8); // Invisible defender is hard to hit if you can't see invis. Note // that this applies only to monsters vs monster and monster vs // player. Does not apply to a player fighting an invisible // monster. if (!defender->visible_to(attacker)) mhit = mhit * 65 / 100; #ifdef DEBUG_DIAGNOSTICS mprf(MSGCH_DIAGNOSTICS, "%s: Base to-hit: %d, Final to-hit: %d", attacker->name(DESC_PLAIN).c_str(), base_hit, mhit); #endif return (mhit); } /////////////////////////////////////////////////////////////////////////// bool wielded_weapon_check(item_def *weapon, bool no_message) { bool weapon_warning = false; bool unarmed_warning = false; if (weapon) { if (has_warning_inscription(*weapon, OPER_ATTACK) || weapon->base_type != OBJ_STAVES && (weapon->base_type != OBJ_WEAPONS || is_range_weapon(*weapon))) { weapon_warning = true; } } else if (you.attribute[ATTR_WEAPON_SWAP_INTERRUPTED] && you_tran_can_wear(EQ_WEAPON)) { unarmed_warning = true; } if (!you.received_weapon_warning && !you.confused() && (weapon_warning || unarmed_warning)) { if (no_message) return (false); std::string prompt = "Really attack while "; if (unarmed_warning) prompt += "being unarmed?"; else prompt += "wielding " + weapon->name(DESC_NOCAP_YOUR) + "? "; const bool result = yesno(prompt.c_str(), true, 'n'); learned_something_new(TUT_WIELD_WEAPON); // for tutorial Rangers // Don't warn again if you decide to continue your attack. if (result) you.received_weapon_warning = true; return (result); } return (true); } // Returns true if you hit the monster. bool you_attack(int monster_attacked, bool unarmed_attacks) { ASSERT(!crawl_state.arena); monsters *defender = &menv[monster_attacked]; melee_attack attk(&you, defender, unarmed_attacks); // We're trying to hit a monster, break out of travel/explore now. if (!travel_kill_monster(defender)) interrupt_activity(AI_HIT_MONSTER, defender); // Check if the player is fighting with something unsuitable. if (you.can_see(defender) && !wielded_weapon_check(attk.weapon)) { you.turn_is_over = false; return (false); } bool attack = attk.attack(); if (!attack) { // Attack was cancelled or unsuccessful... if (attk.cancel_attack) you.turn_is_over = false; return (false); } return (true); } // Lose attack energy for attacking with a weapon. which_attack is the actual // attack number, effective_attack is the attack number excluding synthetic // attacks (i.e. excluding M_ARCHER monsters' AT_SHOOT attacks). static void mons_lose_attack_energy(monsters *attacker, int wpn_speed, int which_attack, int effective_attack) { // Initial attack causes energy to be used for all attacks. No // additional energy is used for unarmed attacks. if (effective_attack == 0) attacker->lose_energy(EUT_ATTACK); // Monsters lose additional energy only for the first two weapon // attacks; subsequent hits are free. if (effective_attack > 1) return; // speed adjustment for weapon using monsters if (wpn_speed > 0) { const int atk_speed = attacker->action_energy(EUT_ATTACK); // only get one third penalty/bonus for second weapons. if (effective_attack > 0) wpn_speed = div_rand_round( (2 * atk_speed + wpn_speed), 3 ); int delta = div_rand_round( (wpn_speed - 10 + (atk_speed - 10)), 2 ); if (delta > 0) attacker->speed_increment -= delta; } } bool monster_attack_actor(monsters *attacker, actor *defender, bool allow_unarmed) { ASSERT(defender == &you || defender->atype() == ACT_MONSTER); return (defender->atype() == ACT_PLAYER ? monster_attack(attacker, allow_unarmed) : monsters_fight(attacker, dynamic_cast(defender), allow_unarmed)); } // A monster attacking the player. bool monster_attack(monsters* attacker, bool allow_unarmed) { ASSERT(!crawl_state.arena); // Friendly and good neutral monsters won't attack unless confused. if (attacker->wont_attack() && !mons_is_confused(attacker)) return (false); // In case the monster hasn't noticed you, bumping into it will // change that. behaviour_event(attacker, ME_ALERT, MHITYOU); melee_attack attk(attacker, &you, allow_unarmed); attk.attack(); return (true); } // Two monsters fighting each other. bool monsters_fight(monsters* attacker, monsters* defender, bool allow_unarmed) { melee_attack attk(attacker, defender, allow_unarmed); return (attk.attack()); } /* ************************************************** * * * END PUBLIC FUNCTIONS * * * ************************************************** */ // Returns a value between 0 and 10 representing the weight given to str int weapon_str_weight( object_class_type wpn_class, int wpn_type ) { int ret; const int wpn_skill = weapon_skill( wpn_class, wpn_type ); const int hands = hands_reqd( wpn_class, wpn_type, you.body_size() ); // These are low values, because we'll be adding some bonus to the // larger weapons later. Remember also that 1-1/2-hand weapons get // a bonus in player_weapon_str_weight() as well (can't be done // here because this function is used for cases where the weapon // isn't being used by the player). // Reasonings: // - Short Blades are the best for the dexterous... although they // are very limited in damage potential // - Long Swords are better for the dexterous, the two-handed // swords are a 50/50 split; bastard swords are in between. // - Staves: didn't want to punish the mages who want to use // these... made it a 50/50 split after the 2-hnd bonus // - Polearms: Spears and tridents are the only ones that can // be used one handed and are poking weapons, which requires // more agility than strength. The other ones also require a // fair amount of agility so they end up at 50/50 (most can poke // as well as slash... although slashing is not their strong // point). // - Axes are weighted and edged and so require mostly strength, // but not as much as Maces and Flails, which are typically // blunt and spiked weapons. switch (wpn_skill) { case SK_SHORT_BLADES: ret = 2; break; case SK_LONG_BLADES: ret = 3; break; case SK_STAVES: ret = 3; break; // == 5 after 2-hand bonus case SK_POLEARMS: ret = 3; break; // most are +2 for 2-hands case SK_AXES: ret = 6; break; case SK_MACES_FLAILS: ret = 7; break; default: ret = 5; break; } // whips are special cased (because they are not much like maces) if (wpn_type == WPN_WHIP || wpn_type == WPN_DEMON_WHIP) ret = 2; else if (wpn_type == WPN_QUICK_BLADE) // high dex is very good for these ret = 1; if (hands == HANDS_TWO) ret += 2; // most weapons are capped at 8 if (ret > 8) { // these weapons are huge, so strength plays a larger role if (wpn_type == WPN_GIANT_CLUB || wpn_type == WPN_GIANT_SPIKED_CLUB) ret = 9; else ret = 8; } return (ret); } // Returns a value from 0 to 10 representing the weight of strength to // dexterity for the players currently wielded weapon. static inline int player_weapon_str_weight() { const item_def* weapon = you.weapon(); // Unarmed, weighted slightly towards dex -- would have been more, // but then we'd be punishing Trolls and Ghouls who are strong and // get special unarmed bonuses. if (!weapon) return (4); int ret = weapon_str_weight(weapon->base_type, weapon->sub_type); if (hands_reqd(*weapon, you.body_size()) == HANDS_HALF && !you.shield()) ret += 1; return (ret); } // weapon_dex_weight() + weapon_str_weight == 10, so we only need to // define one of these. static inline int player_weapon_dex_weight( void ) { return (10 - player_weapon_str_weight()); } // weighted average of strength and dex, between (str+dex)/2 and dex static inline int calc_stat_to_hit_base( void ) { #ifdef USE_NEW_COMBAT_STATS // towards_str_avg is a variable, whose sign points towards strength, // and the magnitude is half the difference (thus, when added directly // to you.dex it gives the average of the two. const signed int towards_str_avg = (you.strength - you.dex) / 2; // dex is modified by strength towards the average, by the // weighted amount weapon_str_weight() / 10. return (you.dex + towards_str_avg * player_weapon_str_weight() / 10); #else return (you.dex); #endif } // weighted average of strength and dex, between str and (str+dex)/2 static inline int calc_stat_to_dam_base( void ) { #ifdef USE_NEW_COMBAT_STATS const signed int towards_dex_avg = (you.dex - you.strength) / 2; return (you.strength + towards_dex_avg * player_weapon_dex_weight() / 10); #else return (you.strength); #endif } static void stab_message(actor *defender, int stab_bonus) { switch (stab_bonus) { case 6: // big melee, monster surrounded/not paying attention if (coinflip()) { mprf( "You strike %s from a blind spot!", defender->name(DESC_NOCAP_THE).c_str() ); } else { mprf( "You catch %s momentarily off-guard.", defender->name(DESC_NOCAP_THE).c_str() ); } break; case 4: // confused/fleeing if (!one_chance_in(3)) { mprf( "You catch %s completely off-guard!", defender->name(DESC_NOCAP_THE).c_str() ); } else { mprf( "You strike %s from behind!", defender->name(DESC_NOCAP_THE).c_str() ); } break; case 2: case 1: mprf( "%s fails to defend %s.", defender->name(DESC_CAP_THE).c_str(), defender->pronoun(PRONOUN_REFLEXIVE).c_str() ); break; } }