/*
* File: fight.cc
* Summary: Functions used during combat.
* Written by: Linley Henzell
*/
#include "AppHdr.h"
#include "fight.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <algorithm>
#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<const monsters*>(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<const monsters*>(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<monsters*>(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<int> 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<int>(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<chaos_type>(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<int> 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<int> 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<monsters*>(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;
}
}