/*
* File: mon-act.cc
* Summary: Monsters doing stuff (monsters acting).
* Written by: Linley Henzell
*/
#include "AppHdr.h"
#include "mon-act.h"
#include "areas.h"
#include "arena.h"
#include "attitude-change.h"
#include "beam.h"
#include "cloud.h"
#include "coordit.h"
#include "dbg-scan.h"
#include "delay.h"
#include "directn.h"
#include "env.h"
#include "map_knowledge.h"
#include "food.h"
#include "fprop.h"
#include "fight.h"
#include "itemname.h"
#include "itemprop.h"
#include "items.h"
#include "item_use.h"
#include "mapmark.h"
#include "message.h"
#include "misc.h"
#include "mon-abil.h"
#include "mon-behv.h"
#include "mon-cast.h"
#include "mon-iter.h"
#include "mon-place.h"
#include "mon-project.h"
#include "mgen_data.h"
#include "coord.h"
#include "mon-stuff.h"
#include "mutation.h"
#include "notes.h"
#include "player.h"
#include "random.h"
#include "religion.h"
#include "shopping.h" // for item values
#include "spells1.h"
#include "state.h"
#include "stuff.h"
#include "terrain.h"
#include "traps.h"
#include "tutorial.h"
#include "view.h"
#include "shout.h"
#include "viewchar.h"
static bool _handle_pickup(monsters *monster);
static void _mons_in_cloud(monsters *monster);
static bool _mon_can_move_to_pos(const monsters *monster,
const coord_def& delta,
bool just_check = false);
static bool _is_trap_safe(const monsters *monster, const coord_def& where,
bool just_check = false);
static bool _monster_move(monsters *monster);
static spell_type _map_wand_to_mspell(int wand_type);
// [dshaligram] Doesn't need to be extern.
static coord_def mmov;
static const coord_def mon_compass[8] = {
coord_def(-1,-1), coord_def(0,-1), coord_def(1,-1), coord_def(1,0),
coord_def( 1, 1), coord_def(0, 1), coord_def(-1,1), coord_def(-1,0)
};
static bool immobile_monster[MAX_MONSTERS];
// A probably needless optimization: convert the C string "just seen" to
// a C++ string just once, instead of twice every time a monster moves.
static const std::string _just_seen("just seen");
static inline bool _mons_natural_regen_roll(monsters *monster)
{
const int regen_rate = mons_natural_regen_rate(monster);
return (x_chance_in_y(regen_rate, 25));
}
// Do natural regeneration for monster.
static void _monster_regenerate(monsters *monster)
{
if (monster->has_ench(ENCH_SICK) || !mons_can_regenerate(monster))
return;
// Non-land creatures out of their element cannot regenerate.
if (mons_primary_habitat(monster) != HT_LAND
&& !monster_habitable_grid(monster, grd(monster->pos())))
{
return;
}
if (monster_descriptor(monster->type, MDSC_REGENERATES)
|| (monster->type == MONS_FIRE_ELEMENTAL
&& (grd(monster->pos()) == DNGN_LAVA
|| cloud_type_at(monster->pos()) == CLOUD_FIRE))
|| (monster->type == MONS_WATER_ELEMENTAL
&& feat_is_watery(grd(monster->pos())))
|| (monster->type == MONS_AIR_ELEMENTAL
&& env.cgrid(monster->pos()) == EMPTY_CLOUD
&& one_chance_in(3))
|| _mons_natural_regen_roll(monster))
{
monster->heal(1);
}
}
static bool _swap_monsters(monsters* mover, monsters* moved)
{
// Can't swap with a stationary monster.
if (mons_is_stationary(moved))
return (false);
// Swapping is a purposeful action.
if (mover->confused())
return (false);
// Right now just happens in sanctuary.
if (!is_sanctuary(mover->pos()) || !is_sanctuary(moved->pos()))
return (false);
// A friendly or good-neutral monster moving past a fleeing hostile
// or neutral monster, or vice versa.
if (mover->wont_attack() == moved->wont_attack()
|| mons_is_fleeing(mover) == mons_is_fleeing(moved))
{
return (false);
}
// Don't swap places if the player explicitly ordered their pet to
// attack monsters.
if ((mover->friendly() || moved->friendly())
&& you.pet_target != MHITYOU && you.pet_target != MHITNOT)
{
return (false);
}
if (!mover->can_pass_through(moved->pos())
|| !moved->can_pass_through(mover->pos()))
{
return (false);
}
if (!monster_habitable_grid(mover, grd(moved->pos()))
|| !monster_habitable_grid(moved, grd(mover->pos())))
{
return (false);
}
// Okay, we can do the swap.
const coord_def mover_pos = mover->pos();
const coord_def moved_pos = moved->pos();
mover->set_position(moved_pos);
moved->set_position(mover_pos);
mgrd(mover->pos()) = mover->mindex();
mgrd(moved->pos()) = moved->mindex();
if (you.can_see(mover) && you.can_see(moved))
{
mprf("%s and %s swap places.", mover->name(DESC_CAP_THE).c_str(),
moved->name(DESC_NOCAP_THE).c_str());
}
return (true);
}
static bool _do_mon_spell(monsters *monster, bolt &beem)
{
// Shapeshifters don't get spells.
if (!monster->is_shapeshifter() || !monster->is_actual_spellcaster())
{
if (handle_mon_spell(monster, beem))
{
mmov.reset();
return (true);
}
}
return (false);
}
static void _swim_or_move_energy(monsters *mon)
{
const dungeon_feature_type feat = grd(mon->pos());
// FIXME: Replace check with mons_is_swimming()?
mon->lose_energy( (feat >= DNGN_LAVA && feat <= DNGN_SHALLOW_WATER
&& !mon->airborne()) ? EUT_SWIM
: EUT_MOVE );
}
// Check up to eight grids in the given direction for whether there's a
// monster of the same alignment as the given monster that happens to
// have a ranged attack. If this is true for the first monster encountered,
// returns true. Otherwise returns false.
static bool _ranged_allied_monster_in_dir(monsters *mon, coord_def p)
{
coord_def pos = mon->pos();
for (int i = 1; i <= LOS_RADIUS; i++)
{
pos += p;
if (!in_bounds(pos))
break;
const monsters* ally = monster_at(pos);
if (ally == NULL)
continue;
if (mons_aligned(mon->mindex(), ally->mindex()))
{
// Hostile monsters of normal intelligence only move aside for
// monsters of the same type.
if (mons_intel(mon) <= I_NORMAL && !mon->wont_attack()
&& mons_genus(mon->type) != mons_genus(ally->type))
{
return (false);
}
if (mons_has_ranged_attack(ally)
|| mons_has_ranged_spell(ally, true))
{
return (true);
}
}
break;
}
return (false);
}
// Check whether there's a monster of the same type and alignment adjacent
// to the given monster in at least one of three given directions (relative to
// the monster position).
static bool _allied_monster_at(monsters *mon, coord_def a, coord_def b,
coord_def c)
{
std::vector<coord_def> pos;
pos.push_back(mon->pos() + a);
pos.push_back(mon->pos() + b);
pos.push_back(mon->pos() + c);
for (unsigned int i = 0; i < pos.size(); i++)
{
if (!in_bounds(pos[i]))
continue;
const monsters *ally = monster_at(pos[i]);
if (ally == NULL)
continue;
if (mons_is_stationary(ally))
continue;
// Hostile monsters of normal intelligence only move aside for
// monsters of the same genus.
if (mons_intel(mon) <= I_NORMAL && !mon->wont_attack()
&& mons_genus(mon->type) != mons_genus(ally->type))
{
continue;
}
if (mons_aligned(mon->mindex(), ally->mindex()))
return (true);
}
return (false);
}
// Altars as well as branch entrances are considered interesting for
// some monster types.
static bool _mon_on_interesting_grid(monsters *mon)
{
// Patrolling shouldn't happen all the time.
if (one_chance_in(4))
return (false);
const dungeon_feature_type feat = grd(mon->pos());
switch (feat)
{
// Holy beings will tend to patrol around altars to the good gods.
case DNGN_ALTAR_ELYVILON:
if (!one_chance_in(3))
return (false);
// else fall through
case DNGN_ALTAR_ZIN:
case DNGN_ALTAR_SHINING_ONE:
return (mon->is_holy());
// Orcs will tend to patrol around altars to Beogh, and guard the
// stairway from and to the Orcish Mines.
case DNGN_ALTAR_BEOGH:
case DNGN_ENTER_ORCISH_MINES:
case DNGN_RETURN_FROM_ORCISH_MINES:
return (mons_is_native_in_branch(mon, BRANCH_ORCISH_MINES));
// Same for elves and the Elven Halls.
case DNGN_ENTER_ELVEN_HALLS:
case DNGN_RETURN_FROM_ELVEN_HALLS:
return (mons_is_native_in_branch(mon, BRANCH_ELVEN_HALLS));
// Killer bees always return to their hive.
case DNGN_ENTER_HIVE:
return (mons_is_native_in_branch(mon, BRANCH_HIVE));
default:
return (false);
}
}
// If a hostile monster finds itself on a grid of an "interesting" feature,
// while unoccupied, it will remain in that area, and try to return to it
// if it left it for fighting, seeking etc.
static void _maybe_set_patrol_route(monsters *monster)
{
if (mons_is_wandering(monster)
&& !monster->friendly()
&& !monster->is_patrolling()
&& _mon_on_interesting_grid(monster))
{
monster->patrol_point = monster->pos();
}
}
// Keep kraken tentacles from wandering too far away from the boss monster.
static void _kraken_tentacle_movement_clamp(monsters *tentacle)
{
if (tentacle->type != MONS_KRAKEN_TENTACLE)
return;
const int kraken_idx = tentacle->number;
ASSERT(!invalid_monster_index(kraken_idx));
monsters *kraken = &menv[kraken_idx];
const int distance_to_head =
grid_distance(tentacle->pos(), kraken->pos());
// Beyond max distance, the only move the tentacle can make is
// back towards the head.
if (distance_to_head >= KRAKEN_TENTACLE_RANGE)
mmov = (kraken->pos() - tentacle->pos()).sgn();
}
//---------------------------------------------------------------
//
// handle_movement
//
// Move the monster closer to its target square.
//
//---------------------------------------------------------------
static void _handle_movement(monsters *monster)
{
coord_def delta;
_maybe_set_patrol_route(monster);
// Monsters will try to flee out of a sanctuary.
if (is_sanctuary(monster->pos())
&& mons_is_influenced_by_sanctuary(monster)
&& !mons_is_fleeing_sanctuary(monster))
{
mons_start_fleeing_from_sanctuary(monster);
}
else if (mons_is_fleeing_sanctuary(monster)
&& !is_sanctuary(monster->pos()))
{
// Once outside there's a chance they'll regain their courage.
// Nonliving and berserking monsters always stop immediately,
// since they're only being forced out rather than actually
// scared.
if (monster->holiness() == MH_NONLIVING
|| monster->berserk()
|| x_chance_in_y(2, 5))
{
mons_stop_fleeing_from_sanctuary(monster);
}
}
// Some calculations.
if (mons_class_flag(monster->type, M_BURROWS) && monster->foe == MHITYOU)
{
// Boring beetles always move in a straight line in your
// direction.
delta = you.pos() - monster->pos();
}
else
delta = monster->target - monster->pos();
// Move the monster.
mmov = delta.sgn();
if (mons_is_fleeing(monster) && monster->travel_target != MTRAV_WALL
&& (!monster->friendly()
|| monster->target != you.pos()))
{
mmov *= -1;
}
// Don't allow monsters to enter a sanctuary or attack you inside a
// sanctuary, even if you're right next to them.
if (is_sanctuary(monster->pos() + mmov)
&& (!is_sanctuary(monster->pos())
|| monster->pos() + mmov == you.pos()))
{
mmov.reset();
}
// Bounds check: don't let fleeing monsters try to run off the grid.
const coord_def s = monster->pos() + mmov;
if (!in_bounds_x(s.x))
mmov.x = 0;
if (!in_bounds_y(s.y))
mmov.y = 0;
// Now quit if we can't move.
if (mmov.origin())
return;
if (delta.rdist() > 3)
{
// Reproduced here is some semi-legacy code that makes monsters
// move somewhat randomly along oblique paths. It is an
// exceedingly good idea, given crawl's unique line of sight
// properties.
//
// Added a check so that oblique movement paths aren't used when
// close to the target square. -- bwr
// Sometimes we'll just move parallel the x axis.
if (abs(delta.x) > abs(delta.y) && coinflip())
mmov.y = 0;
// Sometimes we'll just move parallel the y axis.
if (abs(delta.y) > abs(delta.x) && coinflip())
mmov.x = 0;
}
const coord_def newpos(monster->pos() + mmov);
FixedArray < bool, 3, 3 > good_move;
for (int count_x = 0; count_x < 3; count_x++)
for (int count_y = 0; count_y < 3; count_y++)
{
const int targ_x = monster->pos().x + count_x - 1;
const int targ_y = monster->pos().y + count_y - 1;
// Bounds check: don't consider moving out of grid!
if (!in_bounds(targ_x, targ_y))
{
good_move[count_x][count_y] = false;
continue;
}
good_move[count_x][count_y] =
_mon_can_move_to_pos(monster, coord_def(count_x-1, count_y-1));
}
if (mons_wall_shielded(monster))
{
// The rock worm will try to move along through rock for as long as
// possible. If the player is walking through a corridor, for example,
// moving along in the wall beside him is much preferable to actually
// leaving the wall.
// This might cause the rock worm to take detours but it still
// comes off as smarter than otherwise.
if (mmov.x != 0 && mmov.y != 0) // diagonal movement
{
bool updown = false;
bool leftright = false;
coord_def t = monster->pos() + coord_def(mmov.x, 0);
if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t)))
updown = true;
t = monster->pos() + coord_def(0, mmov.y);
if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t)))
leftright = true;
if (updown && (!leftright || coinflip()))
mmov.y = 0;
else if (leftright)
mmov.x = 0;
}
else if (mmov.x == 0 && monster->target.x == monster->pos().x)
{
bool left = false;
bool right = false;
coord_def t = monster->pos() + coord_def(-1, mmov.y);
if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t)))
left = true;
t = monster->pos() + coord_def(1, mmov.y);
if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t)))
right = true;
if (left && (!right || coinflip()))
mmov.x = -1;
else if (right)
mmov.x = 1;
}
else if (mmov.y == 0 && monster->target.y == monster->pos().y)
{
bool up = false;
bool down = false;
coord_def t = monster->pos() + coord_def(mmov.x, -1);
if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t)))
up = true;
t = monster->pos() + coord_def(mmov.x, 1);
if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t)))
down = true;
if (up && (!down || coinflip()))
mmov.y = -1;
else if (down)
mmov.y = 1;
}
}
// If the monster is moving in your direction, whether to attack or
// protect you, or towards a monster it intends to attack, check
// whether we first need to take a step to the side to make sure the
// reinforcement can follow through. Only do this with 50% chance,
// though, so it's not completely predictable.
// First, check whether the monster is smart enough to even consider
// this.
if ((newpos == you.pos()
|| monster_at(newpos) && monster->foe == mgrd(newpos))
&& mons_intel(monster) >= I_ANIMAL
&& coinflip()
&& !mons_is_confused(monster) && !monster->caught()
&& !monster->berserk())
{
// If the monster is moving parallel to the x or y axis, check
// whether
//
// a) the neighbouring grids are blocked
// b) there are other unblocked grids adjacent to the target
// c) there's at least one allied monster waiting behind us.
//
// (For really smart monsters, also check whether there's a
// monster farther back in the corridor that has some kind of
// ranged attack.)
if (mmov.y == 0)
{
if (!good_move[1][0] && !good_move[1][2]
&& (good_move[mmov.x+1][0] || good_move[mmov.x+1][2])
&& (_allied_monster_at(monster, coord_def(-mmov.x, -1),
coord_def(-mmov.x, 0),
coord_def(-mmov.x, 1))
|| mons_intel(monster) >= I_NORMAL
&& !monster->wont_attack()
&& _ranged_allied_monster_in_dir(monster,
coord_def(-mmov.x, 0))))
{
if (good_move[mmov.x+1][0])
mmov.y = -1;
if (good_move[mmov.x+1][2] && (mmov.y == 0 || coinflip()))
mmov.y = 1;
}
}
else if (mmov.x == 0)
{
if (!good_move[0][1] && !good_move[2][1]
&& (good_move[0][mmov.y+1] || good_move[2][mmov.y+1])
&& (_allied_monster_at(monster, coord_def(-1, -mmov.y),
coord_def(0, -mmov.y),
coord_def(1, -mmov.y))
|| mons_intel(monster) >= I_NORMAL
&& !monster->wont_attack()
&& _ranged_allied_monster_in_dir(monster,
coord_def(0, -mmov.y))))
{
if (good_move[0][mmov.y+1])
mmov.x = -1;
if (good_move[2][mmov.y+1] && (mmov.x == 0 || coinflip()))
mmov.x = 1;
}
}
else // We're moving diagonally.
{
if (good_move[mmov.x+1][1])
{
if (!good_move[1][mmov.y+1]
&& (_allied_monster_at(monster, coord_def(-mmov.x, -1),
coord_def(-mmov.x, 0),
coord_def(-mmov.x, 1))
|| mons_intel(monster) >= I_NORMAL
&& !monster->wont_attack()
&& _ranged_allied_monster_in_dir(monster,
coord_def(-mmov.x, -mmov.y))))
{
mmov.y = 0;
}
}
else if (good_move[1][mmov.y+1]
&& (_allied_monster_at(monster, coord_def(-1, -mmov.y),
coord_def(0, -mmov.y),
coord_def(1, -mmov.y))
|| mons_intel(monster) >= I_NORMAL
&& !monster->wont_attack()
&& _ranged_allied_monster_in_dir(monster,
coord_def(-mmov.x, -mmov.y))))
{
mmov.x = 0;
}
}
}
// Now quit if we can't move.
if (mmov.origin())
return;
// Try to stay in sight of the player if we're moving towards
// him/her, in order to avoid the monster coming into view,
// shouting, and then taking a step in a path to the player which
// temporarily takes it out of view, which can lead to the player
// getting "comes into view" and shout messages with no monster in
// view.
// Doesn't matter for arena mode.
if (crawl_state.arena)
return;
// Did we just come into view?
if (monster->seen_context != _just_seen)
return;
monster->seen_context.clear();
// If the player can't see us, it doesn't matter.
if (!(monster->flags & MF_WAS_IN_VIEW))
return;
const coord_def old_pos = monster->pos();
const int old_dist = grid_distance(you.pos(), old_pos);
// We're not moving towards the player.
if (grid_distance(you.pos(), old_pos + mmov) >= old_dist)
{
// Give a message if we move back out of view.
monster->seen_context = _just_seen;
return;
}
// We're already staying in the player's LOS.
if (you.see_cell(old_pos + mmov))
return;
// Try to find a move that brings us closer to the player while
// keeping us in view.
int matches = 0;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
{
if (i == 0 && j == 0)
continue;
if (!good_move[i][j])
continue;
delta.set(i - 1, j - 1);
coord_def tmp = old_pos + delta;
if (grid_distance(you.pos(), tmp) < old_dist && you.see_cell(tmp))
{
if (one_chance_in(++matches))
mmov = delta;
break;
}
}
// The only way to get closer to the player is to step out of view;
// give a message so they player isn't confused about its being
// announced as coming into view but not being seen.
monster->seen_context = _just_seen;
}
//---------------------------------------------------------------
//
// _handle_potion
//
// Give the monster a chance to quaff a potion. Returns true if
// the monster imbibed.
//
//---------------------------------------------------------------
static bool _handle_potion(monsters *monster, bolt & beem)
{
if (monster->asleep()
|| monster->inv[MSLOT_POTION] == NON_ITEM
|| !one_chance_in(3))
{
return (false);
}
bool rc = false;
const int potion_idx = monster->inv[MSLOT_POTION];
item_def& potion = mitm[potion_idx];
const potion_type ptype = static_cast<potion_type>(potion.sub_type);
if (monster->can_drink_potion(ptype) && monster->should_drink_potion(ptype))
{
const bool was_visible = you.can_see(monster);
// Drink the potion.
const item_type_id_state_type id = monster->drink_potion_effect(ptype);
// Give ID if necessary.
if (was_visible && id != ID_UNKNOWN_TYPE)
set_ident_type(OBJ_POTIONS, ptype, id);
// Remove it from inventory.
if (dec_mitm_item_quantity(potion_idx, 1))
monster->inv[MSLOT_POTION] = NON_ITEM;
else if (is_blood_potion(potion))
remove_oldest_blood_potion(potion);
monster->lose_energy(EUT_ITEM);
rc = true;
}
return (rc);
}
static bool _handle_reaching(monsters *monster)
{
bool ret = false;
item_def *wpn = monster->weapon(0);
const mon_attack_def attk(mons_attack_spec(monster, 0));
actor *foe = monster->get_foe();
if (!foe)
return (false);
if (monster->submerged())
return (false);
if (mons_aligned(monster->mindex(), monster->foe))
return (false);
const coord_def foepos(foe->pos());
const coord_def delta(foepos - monster->pos());
const int grid_distance(delta.rdist());
const coord_def middle(monster->pos() + delta / 2);
if (grid_distance == 2
// The monster has to be attacking the correct position.
&& monster->target == foepos
// With a reaching weapon OR ...
&& ((wpn && get_weapon_brand(*wpn) == SPWPN_REACHING)
// ... with a native reaching attack, provided the attack
// is not on a full diagonal.
|| (attk.flavour == AF_REACH && attk.damage
&& delta.abs() <= 5))
// And with no dungeon furniture in the way of the reaching
// attack; if the middle square is empty, skip the LOS check.
&& (grd(middle) > DNGN_MAX_NONREACH
|| (monster->foe == MHITYOU?
you.see_cell_no_trans(monster->pos())
: monster->mon_see_cell(foepos, true))))
{
ret = true;
monster_attack_actor(monster, foe, false);
// Player saw the item reach.
if (wpn && !is_artefact(*wpn) && you.can_see(monster))
set_ident_flags(*wpn, ISFLAG_KNOW_TYPE);
}
return (ret);
}
//---------------------------------------------------------------
//
// handle_scroll
//
// Give the monster a chance to read a scroll. Returns true if
// the monster read something.
//
//---------------------------------------------------------------
static bool _handle_scroll(monsters *monster)
{
// Yes, there is a logic to this ordering {dlb}:
if (monster->asleep()
|| mons_is_confused(monster)
|| monster->submerged()
|| monster->inv[MSLOT_SCROLL] == NON_ITEM
|| !one_chance_in(3))
{
return (false);
}
// Make sure the item actually is a scroll.
if (mitm[monster->inv[MSLOT_SCROLL]].base_type != OBJ_SCROLLS)
return (false);
bool read = false;
item_type_id_state_type ident = ID_UNKNOWN_TYPE;
bool was_visible = you.can_see(monster);
// Notice how few cases are actually accounted for here {dlb}:
const int scroll_type = mitm[monster->inv[MSLOT_SCROLL]].sub_type;
switch (scroll_type)
{
case SCR_TELEPORTATION:
if (!monster->has_ench(ENCH_TP))
{
if (monster->caught() || mons_is_fleeing(monster)
|| monster->pacified())
{
simple_monster_message(monster, " reads a scroll.");
monster_teleport(monster, false);
read = true;
ident = ID_KNOWN_TYPE;
}
}
break;
case SCR_BLINKING:
if (monster->caught() || mons_is_fleeing(monster)
|| monster->pacified())
{
if (mons_near(monster))
{
simple_monster_message(monster, " reads a scroll.");
monster_blink(monster);
read = true;
ident = ID_KNOWN_TYPE;
}
}
break;
case SCR_SUMMONING:
if (mons_near(monster))
{
simple_monster_message(monster, " reads a scroll.");
const int mon = create_monster(
mgen_data(MONS_ABOMINATION_SMALL, SAME_ATTITUDE(monster),
monster, 0, 0, monster->pos(), monster->foe,
MG_FORCE_BEH));
read = true;
if (mon != -1)
{
if (you.can_see(&menv[mon]))
{
mprf("%s appears!", menv[mon].name(DESC_CAP_A).c_str());
ident = ID_KNOWN_TYPE;
}
player_angers_monster(&menv[mon]);
}
else if (you.can_see(monster))
canned_msg(MSG_NOTHING_HAPPENS);
}
break;
}
if (read)
{
if (dec_mitm_item_quantity(monster->inv[MSLOT_SCROLL], 1))
monster->inv[MSLOT_SCROLL] = NON_ITEM;
if (ident != ID_UNKNOWN_TYPE && was_visible)
set_ident_type(OBJ_SCROLLS, scroll_type, ident);
monster->lose_energy(EUT_ITEM);
}
return read;
}
//---------------------------------------------------------------
//
// handle_wand
//
// Give the monster a chance to zap a wand. Returns true if the
// monster zapped.
//
//---------------------------------------------------------------
static bool _handle_wand(monsters *monster, bolt &beem)
{
// Yes, there is a logic to this ordering {dlb}:
if (!mons_near(monster)
|| monster->asleep()
|| monster->has_ench(ENCH_SUBMERGED)
|| monster->inv[MSLOT_WAND] == NON_ITEM
|| mitm[monster->inv[MSLOT_WAND]].plus <= 0
|| coinflip())
{
return (false);
}
bool niceWand = false;
bool zap = false;
bool was_visible = you.can_see(monster);
item_def &wand(mitm[monster->inv[MSLOT_WAND]]);
// map wand type to monster spell type
const spell_type mzap = _map_wand_to_mspell(wand.sub_type);
if (mzap == SPELL_NO_SPELL)
return (false);
// set up the beam
int power = 30 + monster->hit_dice;
bolt theBeam = mons_spells(monster, mzap, power);
beem.name = theBeam.name;
beem.beam_source = monster->mindex();
beem.source = monster->pos();
beem.colour = theBeam.colour;
beem.range = theBeam.range;
beem.damage = theBeam.damage;
beem.ench_power = theBeam.ench_power;
beem.hit = theBeam.hit;
beem.type = theBeam.type;
beem.flavour = theBeam.flavour;
beem.thrower = theBeam.thrower;
beem.is_beam = theBeam.is_beam;
beem.is_explosion = theBeam.is_explosion;
#if HISCORE_WEAPON_DETAIL
beem.aux_source =
wand.name(DESC_QUALNAME, false, true, false, false);
#else
beem.aux_source =
wand.name(DESC_QUALNAME, false, true, false, false,
ISFLAG_KNOW_CURSE | ISFLAG_KNOW_PLUSES);
#endif
const int wand_type = wand.sub_type;
switch (wand_type)
{
case WAND_DISINTEGRATION:
// Dial down damage from wands of disintegration, since
// disintegration beams can do large amounts of damage.
beem.damage.size = beem.damage.size * 2 / 3;
break;
case WAND_ENSLAVEMENT:
case WAND_DIGGING:
case WAND_RANDOM_EFFECTS:
// These have been deemed "too tricky" at this time {dlb}:
return (false);
case WAND_POLYMORPH_OTHER:
// Monsters can be very trigger happy with wands, reduce this
// for polymorph.
if (!one_chance_in(5))
return (false);
break;
// These are wands that monsters will aim at themselves {dlb}:
case WAND_HASTING:
if (!monster->has_ench(ENCH_HASTE))
{
beem.target = monster->pos();
niceWand = true;
break;
}
return (false);
case WAND_HEALING:
if (monster->hit_points <= monster->max_hit_points / 2)
{
beem.target = monster->pos();
niceWand = true;
break;
}
return (false);
case WAND_INVISIBILITY:
if (!monster->has_ench(ENCH_INVIS)
&& !monster->has_ench(ENCH_SUBMERGED)
&& (!monster->friendly() || you.can_see_invisible(false)))
{
beem.target = monster->pos();
niceWand = true;
break;
}
return (false);
case WAND_TELEPORTATION:
if (monster->hit_points <= monster->max_hit_points / 2
|| monster->caught())
{
if (!monster->has_ench(ENCH_TP)
&& !one_chance_in(20))
{
beem.target = monster->pos();
niceWand = true;
break;
}
// This break causes the wand to be tried on the player.
break;
}
return (false);
}
// Fire tracer, if necessary.
if (!niceWand)
{
fire_tracer( monster, beem );
// Good idea?
zap = mons_should_fire(beem);
}
if (niceWand || zap)
{
if (!niceWand)
make_mons_stop_fleeing(monster);
if (!simple_monster_message(monster, " zaps a wand."))
{
if (!silenced(you.pos()))
mpr("You hear a zap.", MSGCH_SOUND);
}
// charge expenditure {dlb}
wand.plus--;
beem.is_tracer = false;
beem.fire();
if (was_visible)
{
if (niceWand || !beem.is_enchantment() || beem.obvious_effect)
set_ident_type(OBJ_WANDS, wand_type, ID_KNOWN_TYPE);
else
set_ident_type(OBJ_WANDS, wand_type, ID_MON_TRIED_TYPE);
// Increment zap count.
if (wand.plus2 >= 0)
wand.plus2++;
}
monster->lose_energy(EUT_ITEM);
return (true);
}
return (false);
}
static void _setup_generic_throw(struct monsters *monster, struct bolt &pbolt)
{
// FIXME we should use a sensible range here
pbolt.range = LOS_RADIUS;
pbolt.beam_source = monster->mindex();
pbolt.type = dchar_glyph(DCHAR_FIRED_MISSILE);
pbolt.flavour = BEAM_MISSILE;
pbolt.thrower = KILL_MON_MISSILE;
pbolt.aux_source.clear();
pbolt.is_beam = false;
}
static bool _mons_throw(struct monsters *monster, struct bolt &pbolt,
int hand_used)
{
std::string ammo_name;
bool returning = false;
int baseHit = 0, baseDam = 0; // from thrown or ammo
int ammoHitBonus = 0, ammoDamBonus = 0; // from thrown or ammo
int lnchHitBonus = 0, lnchDamBonus = 0; // special add from launcher
int exHitBonus = 0, exDamBonus = 0; // 'extra' bonus from skill/dex/str
int lnchBaseDam = 0;
int hitMult = 0;
int damMult = 0;
int diceMult = 100;
// Some initial convenience & initializations.
int wepClass = mitm[hand_used].base_type;
int wepType = mitm[hand_used].sub_type;
int weapon = monster->inv[MSLOT_WEAPON];
int lnchType = (weapon != NON_ITEM) ? mitm[weapon].sub_type : 0;
mon_inv_type slot = get_mon_equip_slot(monster, mitm[hand_used]);
ASSERT(slot != NUM_MONSTER_SLOTS);
const bool skilled = mons_class_flag(monster->type, M_FIGHTER);
monster->lose_energy(EUT_MISSILE);
const int throw_energy = monster->action_energy(EUT_MISSILE);
// Dropping item copy, since the launched item might be different.
item_def item = mitm[hand_used];
item.quantity = 1;
if (monster->friendly())
item.flags |= ISFLAG_DROPPED_BY_ALLY;
// FIXME we should actually determine a sensible range here
pbolt.range = LOS_RADIUS;
if (setup_missile_beam(monster, pbolt, item, ammo_name, returning))
return (false);
pbolt.aimed_at_spot = returning;
const launch_retval projected =
is_launched(monster, monster->mslot_item(MSLOT_WEAPON),
mitm[hand_used]);
// extract launcher bonuses due to magic
if (projected == LRET_LAUNCHED)
{
lnchHitBonus = mitm[weapon].plus;
lnchDamBonus = mitm[weapon].plus2;
lnchBaseDam = property(mitm[weapon], PWPN_DAMAGE);
}
// extract weapon/ammo bonuses due to magic
ammoHitBonus = item.plus;
ammoDamBonus = item.plus2;
// Archers get a boost from their melee attack.
if (mons_class_flag(monster->type, M_ARCHER))
{
const mon_attack_def attk = mons_attack_spec(monster, 0);
if (attk.type == AT_SHOOT)
{
if (projected == LRET_THROWN && wepClass == OBJ_MISSILES)
ammoHitBonus += random2avg(attk.damage, 2);
else
ammoDamBonus += random2avg(attk.damage, 2);
}
}
if (projected == LRET_THROWN)
{
// Darts are easy.
if (wepClass == OBJ_MISSILES && wepType == MI_DART)
{
baseHit = 11;
hitMult = 40;
damMult = 25;
}
else
{
baseHit = 6;
hitMult = 30;
damMult = 25;
}
baseDam = property(item, PWPN_DAMAGE);
if (wepClass == OBJ_MISSILES) // throw missile
{
// ammo damage needs adjusting here - OBJ_MISSILES
// don't get separate tohit/damage bonuses!
ammoDamBonus = ammoHitBonus;
// [dshaligram] Thrown stones/darts do only half the damage of
// launched stones/darts. This matches 4.0 behaviour.
if (wepType == MI_DART || wepType == MI_STONE
|| wepType == MI_SLING_BULLET)
{
baseDam = div_rand_round(baseDam, 2);
}
}
// give monster "skill" bonuses based on HD
exHitBonus = (hitMult * monster->hit_dice) / 10 + 1;
exDamBonus = (damMult * monster->hit_dice) / 10 + 1;
}
// Monsters no longer gain unfair advantages with weapons of
// fire/ice and incorrect ammo. They now have the same restrictions
// as players.
int bow_brand = SPWPN_NORMAL;
const int ammo_brand = get_ammo_brand(item);
if (projected == LRET_LAUNCHED)
{
bow_brand = get_weapon_brand(mitm[monster->inv[MSLOT_WEAPON]]);
switch (lnchType)
{
case WPN_BLOWGUN:
baseHit = 12;
hitMult = 60;
damMult = 0;
lnchDamBonus = 0;
break;
case WPN_BOW:
case WPN_LONGBOW:
baseHit = 0;
hitMult = 60;
damMult = 35;
// monsters get half the launcher damage bonus,
// which is about as fair as I can figure it.
lnchDamBonus = (lnchDamBonus + 1) / 2;
break;
case WPN_CROSSBOW:
baseHit = 4;
hitMult = 70;
damMult = 30;
break;
case WPN_SLING:
baseHit = 10;
hitMult = 40;
damMult = 20;
// monsters get half the launcher damage bonus,
// which is about as fair as I can figure it.
lnchDamBonus /= 2;
break;
}
// Launcher is now more important than ammo for base damage.
baseDam = property(item, PWPN_DAMAGE);
if (lnchBaseDam)
baseDam = lnchBaseDam + random2(1 + baseDam);
// missiles don't have pluses2; use hit bonus
ammoDamBonus = ammoHitBonus;
exHitBonus = (hitMult * monster->hit_dice) / 10 + 1;
exDamBonus = (damMult * monster->hit_dice) / 10 + 1;
if (!baseDam && elemental_missile_beam(bow_brand, ammo_brand))
baseDam = 4;
// [dshaligram] This is a horrible hack - we force beam.cc to
// consider this beam "needle-like".
if (wepClass == OBJ_MISSILES && wepType == MI_NEEDLE)
pbolt.ench_power = AUTOMATIC_HIT;
// elven bow w/ elven arrow, also orcish
if (get_equip_race(mitm[monster->inv[MSLOT_WEAPON]])
== get_equip_race(mitm[monster->inv[MSLOT_MISSILE]]))
{
baseHit++;
baseDam++;
if (get_equip_race(mitm[monster->inv[MSLOT_WEAPON]]) == ISFLAG_ELVEN)
pbolt.hit++;
}
// POISON brand launchers poison ammo
if (bow_brand == SPWPN_VENOM && ammo_brand == SPMSL_NORMAL)
set_item_ego_type(item, OBJ_MISSILES, SPMSL_POISONED);
// Vorpal brand increases damage dice size.
if (bow_brand == SPWPN_VORPAL)
diceMult = diceMult * 130 / 100;
// As do steel ammo.
if (ammo_brand == SPMSL_STEEL)
diceMult = diceMult * 150 / 100;
// Note: we already have throw_energy taken off. -- bwr
int speed_delta = 0;
if (lnchType == WPN_CROSSBOW)
{
if (bow_brand == SPWPN_SPEED)
{
// Speed crossbows take 50% less time to use than
// ordinary crossbows.
speed_delta = div_rand_round(throw_energy * 2, 5);
}
else
{
// Ordinary crossbows take 20% more time to use
// than ordinary bows.
speed_delta = -div_rand_round(throw_energy, 5);
}
}
else if (bow_brand == SPWPN_SPEED)
{
// Speed bows take 50% less time to use than
// ordinary bows.
speed_delta = div_rand_round(throw_energy, 2);
}
monster->speed_increment += speed_delta;
}
// Chaos overides flame and frost
if (pbolt.flavour != BEAM_MISSILE)
{
baseHit += 2;
exDamBonus += 6;
}
// monster intelligence bonus
if (mons_intel(monster) == I_HIGH)
exHitBonus += 10;
// Now, if a monster is, for some reason, throwing something really
// stupid, it will have baseHit of 0 and damage of 0. Ah well.
std::string msg = monster->name(DESC_CAP_THE);
msg += ((projected == LRET_LAUNCHED) ? " shoots " : " throws ");
if (!pbolt.name.empty() && projected == LRET_LAUNCHED)
msg += article_a(pbolt.name);
else
{
// build shoot message
msg += item.name(DESC_NOCAP_A);
// build beam name
pbolt.name = item.name(DESC_PLAIN, false, false, false);
}
msg += ".";
if (monster->observable())
{
mpr(msg.c_str());
if (projected == LRET_LAUNCHED
&& item_type_known(mitm[monster->inv[MSLOT_WEAPON]])
|| projected == LRET_THROWN
&& mitm[hand_used].base_type == OBJ_MISSILES)
{
set_ident_flags(mitm[hand_used], ISFLAG_KNOW_TYPE);
}
}
throw_noise(monster, pbolt, item);
// [dshaligram] When changing bolt names here, you must edit
// hiscores.cc (scorefile_entry::terse_missile_cause()) to match.
if (projected == LRET_LAUNCHED)
{
pbolt.aux_source = make_stringf("Shot with a%s %s by %s",
(is_vowel(pbolt.name[0]) ? "n" : ""), pbolt.name.c_str(),
monster->name(DESC_NOCAP_A).c_str());
}
else
{
pbolt.aux_source = make_stringf("Hit by a%s %s thrown by %s",
(is_vowel(pbolt.name[0]) ? "n" : ""), pbolt.name.c_str(),
monster->name(DESC_NOCAP_A).c_str());
}
// Add everything up.
pbolt.hit = baseHit + random2avg(exHitBonus, 2) + ammoHitBonus;
pbolt.damage =
dice_def(1, baseDam + random2avg(exDamBonus, 2) + ammoDamBonus);
if (projected == LRET_LAUNCHED)
{
pbolt.damage.size += lnchDamBonus;
pbolt.hit += lnchHitBonus;
}
pbolt.damage.size = diceMult * pbolt.damage.size / 100;
if (monster->has_ench(ENCH_BATTLE_FRENZY))
{
const mon_enchant ench = monster->get_ench(ENCH_BATTLE_FRENZY);
#ifdef DEBUG_DIAGNOSTICS
const dice_def orig_damage = pbolt.damage;
#endif
pbolt.damage.size = pbolt.damage.size * (115 + ench.degree * 15) / 100;
#ifdef DEBUG_DIAGNOSTICS
mprf(MSGCH_DIAGNOSTICS, "%s frenzy damage: %dd%d -> %dd%d",
monster->name(DESC_PLAIN).c_str(),
orig_damage.num, orig_damage.size,
pbolt.damage.num, pbolt.damage.size);
#endif
}
// Skilled archers get better to-hit and damage.
if (skilled)
{
pbolt.hit = pbolt.hit * 120 / 100;
pbolt.damage.size = pbolt.damage.size * 120 / 100;
}
scale_dice(pbolt.damage);
// decrease inventory
bool really_returns;
if (returning && !one_chance_in(mons_power(monster->type) + 3))
really_returns = true;
else
really_returns = false;
pbolt.drop_item = !really_returns;
// Redraw the screen before firing, in case the monster just
// came into view and the screen hasn't been updated yet.
viewwindow(false);
pbolt.fire();
// The item can be destroyed before returning.
if (really_returns && thrown_object_destroyed(&item, pbolt.target))
{
really_returns = false;
}
if (really_returns)
{
// Fire beam in reverse.
pbolt.setup_retrace();
viewwindow(false);
pbolt.fire();
msg::stream << "The weapon returns "
<< (you.can_see(monster)?
("to " + monster->name(DESC_NOCAP_THE))
: "whence it came from")
<< "!" << std::endl;
// Player saw the item return.
if (!is_artefact(item))
{
// Since this only happens for non-artefacts, also mark properties
// as known.
set_ident_flags(mitm[hand_used],
ISFLAG_KNOW_TYPE | ISFLAG_KNOW_PROPERTIES);
}
}
else if (dec_mitm_item_quantity(hand_used, 1))
monster->inv[returning ? slot : MSLOT_MISSILE] = NON_ITEM;
if (pbolt.special_explosion != NULL)
delete pbolt.special_explosion;
return (true);
}
//---------------------------------------------------------------
//
// handle_throw
//
// Give the monster a chance to throw something. Returns true if
// the monster hurled.
//
//---------------------------------------------------------------
static bool _handle_throw(monsters *monster, bolt & beem)
{
// Yes, there is a logic to this ordering {dlb}:
if (monster->incapacitated()
|| monster->asleep()
|| monster->submerged())
{
return (false);
}
if (mons_itemuse(monster) < MONUSE_STARTING_EQUIPMENT)
return (false);
const bool archer = mons_class_flag(monster->type, M_ARCHER);
// Highly-specialised archers are more likely to shoot than talk. (?)
if (one_chance_in(archer? 9 : 5))
return (false);
// Don't allow offscreen throwing for now.
if (monster->foe == MHITYOU && !mons_near(monster))
return (false);
// Monsters won't shoot in melee range, largely for balance reasons.
// Specialist archers are an exception to this rule.
if (!archer && adjacent(beem.target, monster->pos()))
return (false);
// If the monster is a spellcaster, don't bother throwing stuff.
if (mons_has_ranged_spell(monster, true, false))
return (false);
// Greatly lowered chances if the monster is fleeing or pacified and
// leaving the level.
if ((mons_is_fleeing(monster) || monster->pacified())
&& !one_chance_in(8))
{
return (false);
}
item_def *launcher = NULL;
const item_def *weapon = NULL;
const int mon_item = mons_pick_best_missile(monster, &launcher);
if (mon_item == NON_ITEM || !mitm[mon_item].is_valid())
return (false);
if (player_or_mon_in_sanct(monster))
return (false);
item_def *missile = &mitm[mon_item];
// Throwing a net at a target that is already caught would be
// completely useless, so bail out.
const actor *act = actor_at(beem.target);
if (missile->base_type == OBJ_MISSILES
&& missile->sub_type == MI_THROWING_NET
&& act && act->caught())
{
return (false);
}
// If the attack needs a launcher that we can't wield, bail out.
if (launcher)
{
weapon = monster->mslot_item(MSLOT_WEAPON);
if (weapon && weapon != launcher && weapon->cursed())
return (false);
}
// Ok, we'll try it.
_setup_generic_throw( monster, beem );
// Set fake damage for the tracer.
beem.damage = dice_def(10, 10);
// Set item for tracer, even though it probably won't be used
beem.item = missile;
// Fire tracer.
fire_tracer( monster, beem );
// Clear fake damage (will be set correctly in mons_throw).
beem.damage = 0;
// Good idea?
if (mons_should_fire( beem ))
{
// Monsters shouldn't shoot if fleeing, so let them "turn to attack".
make_mons_stop_fleeing(monster);
if (launcher && launcher != weapon)
monster->swap_weapons();
beem.name.clear();
return (_mons_throw( monster, beem, mon_item ));
}
return (false);
}
// Give the monster its action energy (aka speed_increment).
static void _monster_add_energy(monsters *monster)
{
if (monster->speed > 0)
{
// Randomise to make counting off monster moves harder:
const int energy_gained =
std::max(1, div_rand_round(monster->speed * you.time_taken, 10));
monster->speed_increment += energy_gained;
}
}
static void _khufu_drop_tomb(monsters *monster)
{
int count = 0;
monster->behaviour = BEH_SEEK; // don't wander on duty!
for (adjacent_iterator ai(monster->pos()); ai; ++ai)
{
if (grd(*ai) == DNGN_ROCK_WALL)
{
grd(*ai) = DNGN_FLOOR;
count++;
}
}
if (count)
{
you.update_los();
if (monster->observable())
mpr("The walls disappear!");
else
mpr("You hear a deep rumble.");
}
monster->number = 0;
monster->lose_energy(EUT_SPELL);
}
#ifdef DEBUG
# define DEBUG_ENERGY_USE(problem) \
if (monster->speed_increment == old_energy && monster->alive()) \
mprf(MSGCH_DIAGNOSTICS, \
problem " for monster '%s' consumed no energy", \
monster->name(DESC_PLAIN).c_str(), true);
#else
# define DEBUG_ENERGY_USE(problem) ((void) 0)
#endif
static void _handle_monster_move(monsters *monster)
{
monster->hit_points = std::min(monster->max_hit_points,
monster->hit_points);
fedhas_neutralise(monster);
// Monster just summoned (or just took stairs), skip this action.
if (testbits( monster->flags, MF_JUST_SUMMONED ))
{
monster->flags &= ~MF_JUST_SUMMONED;
return;
}
mon_acting mact(monster);
_monster_add_energy(monster);
// Handle clouds on nonmoving monsters.
if (monster->speed == 0
&& env.cgrid(monster->pos()) != EMPTY_CLOUD
&& !monster->submerged())
{
_mons_in_cloud( monster );
}
// Apply monster enchantments once for every normal-speed
// player turn.
monster->ench_countdown -= you.time_taken;
while (monster->ench_countdown < 0)
{
monster->ench_countdown += 10;
monster->apply_enchantments();
// If the monster *merely* died just break from the loop
// rather than quit altogether, since we have to deal with
// giant spores and ball lightning exploding at the end of the
// function, but do return if the monster's data has been
// reset, since then the monster type is invalid.
if (monster->type == MONS_NO_MONSTER)
return;
else if (monster->hit_points < 1)
break;
}
// Memory is decremented here for a reason -- we only want it
// decrementing once per monster "move".
if (monster->foe_memory > 0)
monster->foe_memory--;
// Otherwise there are potential problems with summonings.
if (monster->type == MONS_GLOWING_SHAPESHIFTER)
monster->add_ench(ENCH_GLOWING_SHAPESHIFTER);
if (monster->type == MONS_SHAPESHIFTER)
monster->add_ench(ENCH_SHAPESHIFTER);
// We reset batty monsters from wander to seek here, instead
// of in handle_behaviour() since that will be called with
// every single movement, and we want these monsters to
// hit and run. -- bwr
if (monster->foe != MHITNOT && mons_is_wandering(monster)
&& mons_is_batty(monster))
{
monster->behaviour = BEH_SEEK;
}
monster->check_speed();
monsterentry* entry = get_monster_data(monster->type);
if (!entry)
return;
int old_energy = INT_MAX;
int non_move_energy = std::min(entry->energy_usage.move,
entry->energy_usage.swim);
#if DEBUG_MONS_SCAN
bool monster_was_floating = mgrd(monster->pos()) != monster->mindex();
#endif
while (monster->has_action_energy())
{
// The continues & breaks are WRT this.
if (!monster->alive())
break;
const coord_def old_pos = monster->pos();
#if DEBUG_MONS_SCAN
if (!monster_was_floating
&& mgrd(monster->pos()) != monster->mindex())
{
mprf(MSGCH_ERROR, "Monster %s became detached from mgrd "
"in _handle_monster_move() loop",
monster->name(DESC_PLAIN, true).c_str());
mpr("[[[[[[[[[[[[[[[[[[", MSGCH_WARN);
debug_mons_scan();
mpr("]]]]]]]]]]]]]]]]]]", MSGCH_WARN);
monster_was_floating = true;
}
else if (monster_was_floating
&& mgrd(monster->pos()) == monster->mindex())
{
mprf(MSGCH_DIAGNOSTICS, "Monster %s re-attached itself to mgrd "
"in _handle_monster_move() loop",
monster->name(DESC_PLAIN, true).c_str());
monster_was_floating = false;
}
#endif
if (monster->speed_increment >= old_energy)
{
#ifdef DEBUG
if (monster->speed_increment == old_energy)
{
mprf(MSGCH_DIAGNOSTICS, "'%s' has same energy as last loop",
monster->name(DESC_PLAIN, true).c_str());
}
else
{
mprf(MSGCH_DIAGNOSTICS, "'%s' has MORE energy than last loop",
monster->name(DESC_PLAIN, true).c_str());
}
#endif
monster->speed_increment = old_energy - 10;
old_energy = monster->speed_increment;
continue;
}
old_energy = monster->speed_increment;
if (mons_is_projectile(monster->type))
{
if (iood_act(*monster))
return;
monster->lose_energy(EUT_MOVE);
continue;
}
monster->shield_blocks = 0;
cloud_type cl_type;
const int cloud_num = env.cgrid(monster->pos());
const bool avoid_cloud = mons_avoids_cloud(monster, cloud_num,
&cl_type);
if (cl_type != CLOUD_NONE)
{
if (avoid_cloud)
{
if (monster->submerged())
{
monster->speed_increment -= entry->energy_usage.swim;
break;
}
if (monster->type == MONS_NO_MONSTER)
{
monster->speed_increment -= entry->energy_usage.move;
break; // problem with vortices
}
}
_mons_in_cloud(monster);
if (monster->type == MONS_NO_MONSTER)
{
monster->speed_increment = 1;
break;
}
}
if (monster->type == MONS_TIAMAT && one_chance_in(3))
{
const int cols[] = { RED, WHITE, DARKGREY, GREEN, MAGENTA };
monster->colour = RANDOM_ELEMENT(cols);
}
_monster_regenerate(monster);
if (monster->cannot_act()
|| monster->type == MONS_SIXFIRHY // these move only 4 of 12 turns
&& ++monster->number / 4 % 3 != 2) // but are not helpless
{
monster->speed_increment -= non_move_energy;
continue;
}
handle_behaviour(monster);
// handle_behaviour() could make the monster leave the level.
if (!monster->alive())
break;
ASSERT(!crawl_state.arena || monster->foe != MHITYOU);
ASSERT(in_bounds(monster->target) || monster->target.origin());
// Submerging monsters will hide from clouds.
if (avoid_cloud
&& monster_can_submerge(monster, grd(monster->pos()))
&& !monster->caught()
&& !monster->submerged())
{
monster->add_ench(ENCH_SUBMERGED);
monster->speed_increment -= ENERGY_SUBMERGE(entry);
continue;
}
if (monster->speed >= 100)
{
monster->speed_increment -= non_move_energy;
continue;
}
if (igrd(monster->pos()) != NON_ITEM
&& (mons_itemuse(monster) >= MONUSE_WEAPONS_ARMOUR
|| mons_itemeat(monster) != MONEAT_NOTHING))
{
// Keep neutral and charmed monsters from picking up stuff.
// Same for friendlies if friendly_pickup is set to "none".
if (!monster->neutral() && !monster->has_ench(ENCH_CHARM)
|| (you.religion == GOD_JIYVA && mons_is_slime(monster))
&& (!monster->friendly()
|| you.friendly_pickup != FRIENDLY_PICKUP_NONE))
{
if (_handle_pickup(monster))
{
DEBUG_ENERGY_USE("handle_pickup()");
continue;
}
}
}
// Lurking monsters only stop lurking if their target is right
// next to them, otherwise they just sit there.
// However, if the monster is involuntarily submerged but
// still alive (e.g., nonbreathing which had water poured
// on top of it), this doesn't apply.
if (mons_is_lurking(monster) || monster->has_ench(ENCH_SUBMERGED))
{
if (monster->foe != MHITNOT
&& grid_distance(monster->target, monster->pos()) <= 1)
{
if (monster->submerged())
{
// Don't unsubmerge if the monster is too damaged or
// if the monster is afraid, or if it's avoiding the
// cloud on top of the water.
if (monster->hit_points <= monster->max_hit_points / 2
|| monster->has_ench(ENCH_FEAR)
|| avoid_cloud)
{
monster->speed_increment -= non_move_energy;
continue;
}
if (!monster->del_ench(ENCH_SUBMERGED))
{
// Couldn't unsubmerge.
monster->speed_increment -= non_move_energy;
continue;
}
}
monster->behaviour = BEH_SEEK;
}
else
{
monster->speed_increment -= non_move_energy;
continue;
}
}
if (monster->caught())
{
// Struggling against the net takes time.
_swim_or_move_energy(monster);
}
else if (!monster->petrified())
{
// Calculates mmov based on monster target.
_handle_movement(monster);
_kraken_tentacle_movement_clamp(monster);
if (mons_is_confused(monster)
|| monster->type == MONS_AIR_ELEMENTAL
&& monster->submerged())
{
mmov.reset();
int pfound = 0;
for (adjacent_iterator ai(monster->pos(), false); ai; ++ai)
if (monster->can_pass_through(*ai))
if (one_chance_in(++pfound))
mmov = *ai - monster->pos();
// OK, mmov determined.
const coord_def newcell = mmov + monster->pos();
monsters* enemy = monster_at(newcell);
if (enemy
&& newcell != monster->pos()
&& !is_sanctuary(monster->pos()))
{
if (monsters_fight(monster, enemy))
{
mmov.reset();
DEBUG_ENERGY_USE("monsters_fight()");
continue;
}
else
{
// FIXME: None of these work!
// Instead run away!
if (monster->add_ench(mon_enchant(ENCH_FEAR)))
{
behaviour_event(monster, ME_SCARE,
MHITNOT, newcell);
}
break;
}
}
}
}
mon_nearby_ability(monster);
if (monster->type == MONS_KHUFU && monster->number
&& monster->hit_points == monster->max_hit_points)
{
_khufu_drop_tomb(monster);
}
if (!monster->asleep() && !mons_is_wandering(monster)
// Berserking monsters are limited to running up and
// hitting their foes.
&& !monster->berserk()
// Slime creatures can split while wandering or resting.
|| monster->type == MONS_SLIME_CREATURE)
{
bolt beem;
beem.source = monster->pos();
beem.target = monster->target;
beem.beam_source = monster->mindex();
// Prevents unfriendlies from nuking you from offscreen.
// How nice!
const bool friendly_or_near =
monster->friendly() || monster->near_foe();
if (friendly_or_near
|| monster->type == MONS_TEST_SPAWNER
// Slime creatures can split when offscreen.
|| monster->type == MONS_SLIME_CREATURE)
{
// [ds] Special abilities shouldn't overwhelm
// spellcasting in monsters that have both. This aims
// to give them both roughly the same weight.
if (coinflip() ? mon_special_ability(monster, beem)
|| _do_mon_spell(monster, beem)
: _do_mon_spell(monster, beem)
|| mon_special_ability(monster, beem))
{
DEBUG_ENERGY_USE("spell or special");
mmov.reset();
continue;
}
}
if (friendly_or_near)
{
if (_handle_potion(monster, beem))
{
DEBUG_ENERGY_USE("_handle_potion()");
continue;
}
if (_handle_scroll(monster))
{
DEBUG_ENERGY_USE("_handle_scroll()");
continue;
}
if (_handle_wand(monster, beem))
{
DEBUG_ENERGY_USE("_handle_wand()");
continue;
}
if (_handle_reaching(monster))
{
DEBUG_ENERGY_USE("_handle_reaching()");
continue;
}
}
if (_handle_throw(monster, beem))
{
DEBUG_ENERGY_USE("_handle_throw()");
continue;
}
}
if (!monster->caught())
{
if (monster->pos() + mmov == you.pos())
{
ASSERT(!crawl_state.arena);
if (!monster->friendly())
{
// If it steps into you, cancel other targets.
monster->foe = MHITYOU;
monster->target = you.pos();
monster_attack(monster);
if (mons_is_batty(monster))
{
monster->behaviour = BEH_WANDER;
set_random_target(monster);
}
DEBUG_ENERGY_USE("monster_attack()");
mmov.reset();
continue;
}
}
// See if we move into (and fight) an unfriendly monster.
monsters* targ = monster_at(monster->pos() + mmov);
if (targ
&& targ != monster
&& !mons_aligned(monster->mindex(), targ->mindex())
&& monster_can_hit_monster(monster, targ))
{
// Maybe they can swap places?
if (_swap_monsters(monster, targ))
{
_swim_or_move_energy(monster);
continue;
}
// Figure out if they fight.
else if (monsters_fight(monster, targ))
{
if (mons_is_batty(monster))
{
monster->behaviour = BEH_WANDER;
set_random_target(monster);
// monster->speed_increment -= monster->speed;
}
mmov.reset();
DEBUG_ENERGY_USE("monsters_fight()");
continue;
}
}
if (invalid_monster(monster) || mons_is_stationary(monster))
{
if (monster->speed_increment == old_energy)
monster->speed_increment -= non_move_energy;
continue;
}
if (monster->cannot_move() || !_monster_move(monster))
monster->speed_increment -= non_move_energy;
}
you.update_beholder(monster);
// Reevaluate behaviour, since the monster's surroundings have
// changed (it may have moved, or died for that matter). Don't
// bother for dead monsters. :)
if (monster->alive())
{
handle_behaviour(monster);
ASSERT(in_bounds(monster->target) || monster->target.origin());
}
}
if (monster->type != MONS_NO_MONSTER && monster->hit_points < 1)
monster_die(monster, KILL_MISC, NON_MONSTER);
}
//---------------------------------------------------------------
//
// handle_monsters
//
// This is the routine that controls monster AI.
//
//---------------------------------------------------------------
void handle_monsters()
{
// Keep track of monsters that have already moved and don't allow
// them to move again.
memset(immobile_monster, 0, sizeof immobile_monster);
for (monster_iterator mi; mi; ++mi)
{
if (immobile_monster[mi->mindex()])
continue;
const coord_def oldpos = mi->pos();
mi->update_los();
_handle_monster_move(*mi);
if (!invalid_monster(*mi) && mi->pos() != oldpos)
immobile_monster[mi->mindex()] = true;
// If the player got banished, discard pending monster actions.
if (you.banished)
{
// Clear list of mesmerising monsters.
you.clear_beholders();
break;
}
}
// Clear any summoning flags so that lower indiced
// monsters get their actions in the next round.
for (int i = 0; i < MAX_MONSTERS; i++)
menv[i].flags &= ~MF_JUST_SUMMONED;
}
static bool _jelly_divide(monsters *parent)
{
if (!mons_class_flag(parent->type, M_SPLITS))
return (false);
const int reqd = std::max(parent->hit_dice * 8, 50);
if (parent->hit_points < reqd)
return (false);
monsters *child = NULL;
coord_def child_spot;
int num_spots = 0;
// First, find a suitable spot for the child {dlb}:
for (adjacent_iterator ai(parent->pos()); ai; ++ai)
if (actor_at(*ai) == NULL && parent->can_pass_through(*ai))
if ( one_chance_in(++num_spots) )
child_spot = *ai;
if ( num_spots == 0 )
return (false);
// Now that we have a spot, find a monster slot {dlb}:
child = get_free_monster();
if (!child)
return (false);
// Handle impact of split on parent {dlb}:
parent->max_hit_points /= 2;
if (parent->hit_points > parent->max_hit_points)
parent->hit_points = parent->max_hit_points;
parent->init_experience();
parent->experience = parent->experience * 3 / 5 + 1;
// Create child {dlb}:
// This is terribly partial and really requires
// more thought as to generation ... {dlb}
*child = *parent;
child->max_hit_points = child->hit_points;
child->speed_increment = 70 + random2(5);
child->moveto(child_spot);
mgrd(child->pos()) = child->mindex();
if (!simple_monster_message(parent, " splits in two!"))
if (player_can_hear(parent->pos()) || player_can_hear(child->pos()))
mpr("You hear a squelching noise.", MSGCH_SOUND);
if (crawl_state.arena)
arena_placed_monster(child);
return (true);
}
// XXX: This function assumes that only jellies eat items.
static bool _monster_eat_item(monsters *monster, bool nearby)
{
if (!mons_eats_items(monster))
return (false);
// Friendly jellies won't eat (unless worshipping Jiyva).
if (monster->friendly() && you.religion != GOD_JIYVA)
return (false);
int hps_changed = 0;
int max_eat = roll_dice(1, 10);
int eaten = 0;
bool eaten_net = false;
bool death_ooze_ate_good = false;
bool death_ooze_ate_corpse = false;
// Jellies can swim, so don't check water
for (stack_iterator si(monster->pos());
si && eaten < max_eat && hps_changed < 50; ++si)
{
if (!is_item_jelly_edible(*si))
continue;
#if DEBUG_DIAGNOSTICS || DEBUG_EATERS
mprf(MSGCH_DIAGNOSTICS,
"%s eating %s", monster->name(DESC_PLAIN, true).c_str(),
si->name(DESC_PLAIN).c_str());
#endif
int quant = si->quantity;
death_ooze_ate_good = (monster->type == MONS_DEATH_OOZE
&& (get_weapon_brand(*si) == SPWPN_HOLY_WRATH
|| get_ammo_brand(*si) == SPMSL_SILVER));
death_ooze_ate_corpse = (monster->type == MONS_DEATH_OOZE
&& ((si->base_type == OBJ_CORPSES
&& si->sub_type == CORPSE_BODY)
|| si->base_type == OBJ_FOOD
&& si->sub_type == FOOD_CHUNK));
if (si->base_type != OBJ_GOLD)
{
quant = std::min(quant, max_eat - eaten);
hps_changed += (quant * item_mass(*si)) / 20 + quant;
eaten += quant;
if (monster->caught()
&& si->base_type == OBJ_MISSILES
&& si->sub_type == MI_THROWING_NET
&& item_is_stationary(*si))
{
monster->del_ench(ENCH_HELD, true);
eaten_net = true;
}
}
else
{
// Shouldn't be much trouble to digest a huge pile of gold!
if (quant > 500)
quant = 500 + roll_dice(2, (quant - 500) / 2);
hps_changed += quant / 10 + 1;
eaten++;
}
if (you.religion == GOD_JIYVA)
{
const int quantity = si->quantity;
const int value = item_value(*si) / quantity;
int pg = 0;
int timeout = 0;
for (int m = 0; m < quantity; ++m)
{
if (x_chance_in_y(value / 4 + 1, 30 + you.piety / 4))
{
if (timeout <= 0)
{
if (value < 100)
pg += random2(item_value(*si) / 5);
else
pg += random2(item_value(*si) / 30);
}
else
timeout -= value / 5;
}
}
if (pg > 0)
{
simple_god_message(" appreciates your sacrifice.");
gain_piety(pg);
}
if (you.piety > 80 && random2(you.piety) > 50 && one_chance_in(4))
{
if (you.can_safely_mutate())
{
simple_god_message(" alters your body.");
bool success = false;
const int rand = random2(100);
if (rand < 40)
success = mutate(RANDOM_MUTATION, true, false, true);
else if (rand < 60)
{
success = delete_mutation(RANDOM_MUTATION, true, false,
true);
}
else
{
success = mutate(RANDOM_GOOD_MUTATION, true, false,
true);
}
if (success)
{
timeout = (700 + roll_dice(2, 4));
you.num_gifts[you.religion]++;
take_note(Note(NOTE_GOD_GIFT, you.religion));
}
else
mpr("You feel as though nothing has changed.");
}
}
}
if (quant >= si->quantity)
item_was_destroyed(*si, monster->mindex());
dec_mitm_item_quantity(si.link(), quant);
}
if (eaten > 0)
{
hps_changed = std::max(hps_changed, 1);
hps_changed = std::min(hps_changed, 50);
if (death_ooze_ate_good)
monster->hurt(NULL, hps_changed, BEAM_NONE, false);
else
{
// This is done manually instead of using heal_monster(),
// because that function doesn't work quite this way. - bwr
monster->hit_points += hps_changed;
monster->max_hit_points = std::max(monster->hit_points,
monster->max_hit_points);
}
if (player_can_hear(monster->pos()))
{
mprf(MSGCH_SOUND, "You hear a%s slurping noise.",
nearby ? "" : " distant");
}
if (death_ooze_ate_corpse)
{
place_cloud ( CLOUD_MIASMA, monster->pos(),
4 + random2(5), monster->kill_alignment(),
KILL_MON_MISSILE );
}
if (death_ooze_ate_good)
simple_monster_message(monster, " twists violently!");
else if (eaten_net)
simple_monster_message(monster, " devours the net!");
else
_jelly_divide(monster);
}
return (eaten > 0);
}
static bool _monster_eat_single_corpse(monsters *monster, item_def& item,
bool do_heal, bool nearby)
{
if (item.base_type != OBJ_CORPSES || item.sub_type != CORPSE_BODY)
return (false);
monster_type mt = static_cast<monster_type>(item.plus);
if (do_heal)
{
monster->hit_points += 1 + random2(mons_weight(mt)) / 100;
// Limited growth factor here - should 77 really be the cap? {dlb}:
monster->hit_points = std::min(100, monster->hit_points);
monster->max_hit_points = std::max(monster->hit_points,
monster->max_hit_points);
}
if (nearby)
{
mprf("%s eats %s.", monster->name(DESC_CAP_THE).c_str(),
item.name(DESC_NOCAP_THE).c_str());
}
// Assume that eating a corpse requires butchering it. Use logic
// from misc.cc:turn_corpse_into_chunks() and the butchery-related
// delays in delay.cc:stop_delay().
const int max_chunks = mons_weight(mt) / 150;
// Only fresh corpses bleed enough to colour the ground.
if (!food_is_rotten(item))
bleed_onto_floor(monster->pos(), mt, max_chunks, true);
if (mons_skeleton(mt) && one_chance_in(3))
turn_corpse_into_skeleton(item);
else
destroy_item(item.index());
return (true);
}
static bool _monster_eat_corpse(monsters *monster, bool do_heal, bool nearby)
{
if (!mons_eats_corpses(monster))
return (false);
int eaten = 0;
for (stack_iterator si(monster->pos()); si; ++si)
{
if (_monster_eat_single_corpse(monster, *si, do_heal, nearby))
{
eaten++;
break;
}
}
return (eaten > 0);
}
static bool _monster_eat_food(monsters *monster, bool nearby)
{
if (!mons_eats_food(monster))
return (false);
if (mons_is_fleeing(monster))
return (false);
int eaten = 0;
for (stack_iterator si(monster->pos()); si; ++si)
{
const bool is_food = (si->base_type == OBJ_FOOD);
const bool is_corpse = (si->base_type == OBJ_CORPSES
&& si->sub_type == CORPSE_BODY);
if (!is_food && !is_corpse)
continue;
if ((monster->wont_attack()
|| grid_distance(monster->pos(), you.pos()) > 1)
&& coinflip())
{
if (is_food)
{
if (nearby)
{
mprf("%s eats %s.", monster->name(DESC_CAP_THE).c_str(),
quant_name(*si, 1, DESC_NOCAP_THE).c_str());
}
dec_mitm_item_quantity(si.link(), 1);
eaten++;
break;
}
else
{
// Assume that only undead can heal from eating corpses.
if (_monster_eat_single_corpse(monster, *si,
monster->holiness() == MH_UNDEAD,
nearby))
{
eaten++;
break;
}
}
}
}
return (eaten > 0);
}
//---------------------------------------------------------------
//
// handle_pickup
//
// Returns false if monster doesn't spend any time picking something up.
//
//---------------------------------------------------------------
static bool _handle_pickup(monsters *monster)
{
if (monster->asleep() || monster->submerged())
return (false);
// Hack - Harpies fly over water, but we don't have a general
// system for monster igrd yet. Flying intelligent monsters
// (kenku!) would also count here.
dungeon_feature_type feat = grd(monster->pos());
if ((feat == DNGN_LAVA || feat == DNGN_DEEP_WATER) && !monster->flight_mode())
return (false);
const bool nearby = mons_near(monster);
int count_pickup = 0;
if (mons_itemeat(monster) != MONEAT_NOTHING)
{
if (mons_eats_items(monster))
{
if (_monster_eat_item(monster, nearby))
return (false);
}
else if (mons_eats_corpses(monster))
{
// Assume that only undead can heal from eating corpses.
if (_monster_eat_corpse(monster, monster->holiness() == MH_UNDEAD,
nearby))
{
return (false);
}
}
else if (mons_eats_food(monster))
{
if (_monster_eat_food(monster, nearby))
return (false);
}
}
if (mons_itemuse(monster) >= MONUSE_WEAPONS_ARMOUR)
{
// Note: Monsters only look at stuff near the top of stacks.
//
// XXX: Need to put in something so that monster picks up
// multiple items (e.g. ammunition) identical to those it's
// carrying.
//
// Monsters may now pick up up to two items in the same turn.
// (jpeg)
for (stack_iterator si(monster->pos()); si; ++si)
{
if (monster->pickup_item(*si, nearby))
count_pickup++;
if (count_pickup > 1 || coinflip())
break;
}
}
return (count_pickup > 0);
}
// Randomise potential damage.
static int _estimated_trap_damage(trap_type trap)
{
switch (trap)
{
case TRAP_BLADE: return (10 + random2(30));
case TRAP_DART: return (random2(4));
case TRAP_ARROW: return (random2(7));
case TRAP_SPEAR: return (random2(10));
case TRAP_BOLT: return (random2(13));
case TRAP_AXE: return (random2(15));
default: return (0);
}
}
// Check whether a given trap (described by trap position) can be
// regarded as safe. Takes into account monster intelligence and
// allegiance.
// (just_check is used for intelligent monsters trying to avoid traps.)
static bool _is_trap_safe(const monsters *monster, const coord_def& where,
bool just_check)
{
const int intel = mons_intel(monster);
const trap_def *ptrap = find_trap(where);
if (!ptrap)
return (true);
const trap_def& trap = *ptrap;
const bool player_knows_trap = (trap.is_known(&you));
// No friendly monsters will ever enter a Zot trap you know.
if (player_knows_trap && monster->friendly() && trap.type == TRAP_ZOT)
return (false);
// Dumb monsters don't care at all.
if (intel == I_PLANT)
return (true);
if (trap.type == TRAP_SHAFT && monster->will_trigger_shaft())
{
if (mons_is_fleeing(monster) && intel >= I_NORMAL
|| monster->pacified())
{
return (true);
}
return (false);
}
// Hostile monsters are not afraid of non-mechanical traps.
// Allies will try to avoid teleportation and zot traps.
const bool mechanical = (trap.category() == DNGN_TRAP_MECHANICAL);
if (trap.is_known(monster))
{
if (just_check)
return (false); // Square is blocked.
else
{
// Test for corridor-like environment.
const int x = where.x - monster->pos().x;
const int y = where.y - monster->pos().y;
// The question is whether the monster (m) can easily reach its
// presumable destination (x) without stepping on the trap. Traps
// in corridors do not allow this. See e.g
// #x# ##
// #^# or m^x
// m ##
//
// The same problem occurs if paths are blocked by monsters,
// hostile terrain or other traps rather than walls.
// What we do is check whether the squares with the relative
// positions (-1,0)/(+1,0) or (0,-1)/(0,+1) form a "corridor"
// (relative to the _trap_ position rather than the monster one).
// If they don't, the trap square is marked as "unsafe" (because
// there's a good alternative move for the monster to take),
// otherwise the decision will be made according to later tests
// (monster hp, trap type, ...)
// If a monster still gets stuck in a corridor it will usually be
// because it has less than half its maximum hp.
if ((_mon_can_move_to_pos(monster, coord_def(x-1, y), true)
|| _mon_can_move_to_pos(monster, coord_def(x+1,y), true))
&& (_mon_can_move_to_pos(monster, coord_def(x,y-1), true)
|| _mon_can_move_to_pos(monster, coord_def(x,y+1), true)))
{
return (false);
}
}
}
// Friendlies will try not to be parted from you.
if (intelligent_ally(monster) && trap.type == TRAP_TELEPORT
&& player_knows_trap && mons_near(monster))
{
return (false);
}
// Healthy monsters don't mind a little pain.
if (mechanical && monster->hit_points >= monster->max_hit_points / 2
&& (intel == I_ANIMAL
|| monster->hit_points > _estimated_trap_damage(trap.type)))
{
return (true);
}
// Friendly and good neutral monsters don't enjoy Zot trap perks;
// handle accordingly. In the arena Zot traps affect all monsters.
if (monster->wont_attack() || crawl_state.arena)
{
return (mechanical ? mons_flies(monster)
: !trap.is_known(monster) || trap.type != TRAP_ZOT);
}
else
return (!mechanical || mons_flies(monster));
}
static void _mons_open_door(monsters* monster, const coord_def &pos)
{
dungeon_feature_type grid = grd(pos);
const char *adj = "", *noun = "door";
bool was_secret = false;
bool was_seen = false;
std::set<coord_def> all_door;
find_connected_range(pos, DNGN_CLOSED_DOOR, DNGN_SECRET_DOOR, all_door);
get_door_description(all_door.size(), &adj, &noun);
for (std::set<coord_def>::iterator i = all_door.begin();
i != all_door.end(); ++i)
{
const coord_def& dc = *i;
if (grd(dc) == DNGN_SECRET_DOOR && you.see_cell(dc))
{
grid = grid_secret_door_appearance(dc);
was_secret = true;
}
if (you.see_cell(dc))
was_seen = true;
else
set_terrain_changed(dc);
grd(dc) = DNGN_OPEN_DOOR;
}
if (was_seen)
{
viewwindow(false);
if (was_secret)
{
mprf("%s was actually a secret door!",
feature_description(grid, NUM_TRAPS, false,
DESC_CAP_THE, false).c_str());
learned_something_new(TUT_SEEN_SECRET_DOOR, pos);
}
std::string open_str = "opens the ";
open_str += adj;
open_str += noun;
open_str += ".";
monster->seen_context = open_str;
if (!you.can_see(monster))
{
mprf("Something unseen %s", open_str.c_str());
interrupt_activity(AI_FORCE_INTERRUPT);
}
else if (!you_are_delayed())
{
mprf("%s %s", monster->name(DESC_CAP_A).c_str(),
open_str.c_str());
}
}
monster->lose_energy(EUT_MOVE);
}
static bool _habitat_okay(const monsters *monster, dungeon_feature_type targ)
{
return (monster_habitable_grid(monster, targ));
}
static bool _no_habitable_adjacent_grids(const monsters *mon)
{
for (adjacent_iterator ai(mon->pos()); ai; ++ai)
if (_habitat_okay(mon, grd(*ai)))
return (false);
return (true);
}
static bool _mons_can_displace(const monsters *mpusher,
const monsters *mpushee)
{
if (invalid_monster(mpusher) || invalid_monster(mpushee))
return (false);
const int ipushee = mpushee->mindex();
if (invalid_monster_index(ipushee))
return (false);
if (immobile_monster[ipushee])
return (false);
// Confused monsters can't be pushed past, sleeping monsters
// can't push. Note that sleeping monsters can't be pushed
// past, either, but they may be woken up by a crowd trying to
// elbow past them, and the wake-up check happens downstream.
if (mons_is_confused(mpusher) || mons_is_confused(mpushee)
|| mpusher->cannot_move() || mpushee->cannot_move()
|| mons_is_stationary(mpusher) || mons_is_stationary(mpushee)
|| mpusher->asleep())
{
return (false);
}
// Batty monsters are unpushable.
if (mons_is_batty(mpusher) || mons_is_batty(mpushee))
return (false);
if (!monster_shover(mpusher))
return (false);
// Fleeing monsters of the same type may push past higher ranking ones.
if (!monster_senior(mpusher, mpushee, mons_is_fleeing(mpusher)))
return (false);
return (true);
}
// Check whether a monster can move to given square (described by its relative
// coordinates to the current monster position). just_check is true only for
// calls from is_trap_safe when checking the surrounding squares of a trap.
static bool _mon_can_move_to_pos(const monsters *monster,
const coord_def& delta, bool just_check)
{
const coord_def targ = monster->pos() + delta;
// Bounds check: don't consider moving out of grid!
if (!in_bounds(targ))
return (false);
// No monster may enter the open sea.
if (grd(targ) == DNGN_OPEN_SEA)
return (false);
// Non-friendly and non-good neutral monsters won't enter
// sanctuaries.
if (!monster->wont_attack()
&& is_sanctuary(targ)
&& !is_sanctuary(monster->pos()))
{
return (false);
}
// Inside a sanctuary don't attack anything!
if (is_sanctuary(monster->pos()) && actor_at(targ))
return (false);
const dungeon_feature_type target_grid = grd(targ);
const habitat_type habitat = mons_primary_habitat(monster);
// The kraken is so large it cannot enter shallow water.
// Its tentacles can, and will, though.
if (mons_base_type(monster) == MONS_KRAKEN && target_grid == DNGN_SHALLOW_WATER)
return (false);
// Effectively slows down monster movement across water.
// Fire elementals can't cross at all.
bool no_water = false;
if (monster->type == MONS_FIRE_ELEMENTAL || one_chance_in(5))
no_water = true;
cloud_type targ_cloud_type;
const int targ_cloud_num = env.cgrid(targ);
if (mons_avoids_cloud(monster, targ_cloud_num, &targ_cloud_type))
return (false);
if (mons_class_flag(monster->type, M_BURROWS)
&& (target_grid == DNGN_ROCK_WALL
|| target_grid == DNGN_CLEAR_ROCK_WALL))
{
// Don't burrow out of bounds.
if (!in_bounds(targ))
return (false);
// Don't burrow at an angle (legacy behaviour).
if (delta.x != 0 && delta.y != 0)
return (false);
}
else if (!monster->can_pass_through_feat(target_grid)
|| no_water && feat_is_water(target_grid))
{
return (false);
}
else if (!_habitat_okay(monster, target_grid))
{
// If the monster somehow ended up in this habitat (and is
// not dead by now), give it a chance to get out again.
if (grd(monster->pos()) == target_grid
&& _no_habitable_adjacent_grids(monster))
{
return (true);
}
return (false);
}
// Wandering mushrooms don't move while you are looking.
if (monster->type == MONS_WANDERING_MUSHROOM && you.see_cell(targ))
return (false);
// Water elementals avoid fire and heat.
if (monster->type == MONS_WATER_ELEMENTAL
&& (target_grid == DNGN_LAVA
|| targ_cloud_type == CLOUD_FIRE
|| targ_cloud_type == CLOUD_FOREST_FIRE
|| targ_cloud_type == CLOUD_STEAM))
{
return (false);
}
// Fire elementals avoid water and cold.
if (monster->type == MONS_FIRE_ELEMENTAL
&& (feat_is_watery(target_grid)
|| targ_cloud_type == CLOUD_COLD))
{
return (false);
}
// Submerged water creatures avoid the shallows where
// they would be forced to surface. -- bwr
// [dshaligram] Monsters now prefer to head for deep water only if
// they're low on hitpoints. No point in hiding if they want a
// fight.
if (habitat == HT_WATER
&& targ != you.pos()
&& target_grid != DNGN_DEEP_WATER
&& grd(monster->pos()) == DNGN_DEEP_WATER
&& monster->hit_points < (monster->max_hit_points * 3) / 4)
{
return (false);
}
// Smacking the player is always a good move if we're
// hostile (even if we're heading somewhere else).
// Also friendlies want to keep close to the player
// so it's okay as well.
// Smacking another monster is good, if the monsters
// are aligned differently.
if (monsters *targmonster = monster_at(targ))
{
if (just_check)
{
if (targ == monster->pos())
return (true);
return (false); // blocks square
}
if (mons_aligned(monster->mindex(), targmonster->mindex())
&& !_mons_can_displace(monster, targmonster))
{
return (false);
}
}
// Friendlies shouldn't try to move onto the player's
// location, if they are aiming for some other target.
if (monster->wont_attack()
&& monster->foe != MHITYOU
&& (monster->foe != MHITNOT || monster->is_patrolling())
&& targ == you.pos())
{
return (false);
}
// Wandering through a trap is OK if we're pretty healthy,
// really stupid, or immune to the trap.
if (!_is_trap_safe(monster, targ, just_check))
return (false);
// If we end up here the monster can safely move.
return (true);
}
// Uses, and updates the global variable mmov.
static void _find_good_alternate_move(monsters *monster,
const FixedArray<bool, 3, 3>& good_move)
{
const int current_distance = distance(monster->pos(), monster->target);
int dir = -1;
for (int i = 0; i < 8; i++)
{
if (mon_compass[i] == mmov)
{
dir = i;
break;
}
}
// Only handle if the original move is to an adjacent square.
if (dir == -1)
return;
int dist[2];
// First 1 away, then 2 (3 is silly).
for (int j = 1; j <= 2; j++)
{
const int FAR_AWAY = 1000000;
// Try both directions (but randomise which one is first).
const int sdir = coinflip() ? j : -j;
const int inc = -2 * sdir;
for (int mod = sdir, i = 0; i < 2; mod += inc, i++)
{
const int newdir = (dir + 8 + mod) % 8;
if (good_move[mon_compass[newdir].x+1][mon_compass[newdir].y+1])
{
dist[i] = distance(monster->pos()+mon_compass[newdir],
monster->target);
}
else
{
dist[i] = (mons_is_fleeing(monster)) ? (-FAR_AWAY) : FAR_AWAY;
}
}
const int dir0 = ((dir + 8 + sdir) % 8);
const int dir1 = ((dir + 8 - sdir) % 8);
// Now choose.
if (dist[0] == dist[1] && abs(dist[0]) == FAR_AWAY)
continue;
// Which one was better? -- depends on FLEEING or not.
if (mons_is_fleeing(monster))
{
if (dist[0] >= dist[1] && dist[0] >= current_distance)
{
mmov = mon_compass[dir0];
break;
}
if (dist[1] >= dist[0] && dist[1] >= current_distance)
{
mmov = mon_compass[dir1];
break;
}
}
else
{
if (dist[0] <= dist[1] && dist[0] <= current_distance)
{
mmov = mon_compass[dir0];
break;
}
if (dist[1] <= dist[0] && dist[1] <= current_distance)
{
mmov = mon_compass[dir1];
break;
}
}
}
}
static void _jelly_grows(monsters *monster)
{
if (player_can_hear(monster->pos()))
{
mprf(MSGCH_SOUND, "You hear a%s slurping noise.",
mons_near(monster) ? "" : " distant");
}
monster->hit_points += 5;
// note here, that this makes jellies "grow" {dlb}:
if (monster->hit_points > monster->max_hit_points)
monster->max_hit_points = monster->hit_points;
_jelly_divide(monster);
}
static bool _monster_swaps_places( monsters *mon, const coord_def& delta )
{
if (delta.origin())
return (false);
monsters* const m2 = monster_at(mon->pos() + delta);
if (!m2)
return (false);
if (!_mons_can_displace(mon, m2))
return (false);
if (m2->asleep())
{
if (coinflip())
{
#ifdef DEBUG_DIAGNOSTICS
mprf(MSGCH_DIAGNOSTICS,
"Alerting monster %s at (%d,%d)",
m2->name(DESC_PLAIN).c_str(), m2->pos().x, m2->pos().y);
#endif
behaviour_event(m2, ME_ALERT, MHITNOT);
}
return (false);
}
// Check that both monsters will be happy at their proposed new locations.
const coord_def c = mon->pos();
const coord_def n = mon->pos() + delta;
if (!_habitat_okay(mon, grd(n)) || !_habitat_okay(m2, grd(c)))
return (false);
// Okay, do the swap!
_swim_or_move_energy(mon);
mon->set_position(n);
mgrd(n) = mon->mindex();
m2->set_position(c);
const int m2i = m2->mindex();
ASSERT(m2i >= 0 && m2i < MAX_MONSTERS);
mgrd(c) = m2i;
immobile_monster[m2i] = true;
mon->check_redraw(c);
mon->apply_location_effects(c);
m2->check_redraw(c);
m2->apply_location_effects(n);
// The seen context no longer applies if the monster is moving normally.
mon->seen_context.clear();
m2->seen_context.clear();
return (false);
}
static bool _do_move_monster(monsters *monster, const coord_def& delta)
{
const coord_def f = monster->pos() + delta;
if (!in_bounds(f))
return (false);
if (f == you.pos())
{
monster_attack(monster);
return (true);
}
// This includes the case where the monster attacks itself.
if (monsters* def = monster_at(f))
{
monsters_fight(monster, def);
return (true);
}
// The monster gave a "comes into view" message and then immediately
// moved back out of view, leaing the player nothing to see, so give
// this message to avoid confusion.
if (monster->seen_context == _just_seen && !you.see_cell(f))
simple_monster_message(monster, " moves out of view.");
else if (Tutorial.tutorial_left && (monster->flags & MF_WAS_IN_VIEW)
&& !you.see_cell(f))
{
learned_something_new(TUT_MONSTER_LEFT_LOS, monster->pos());
}
// The seen context no longer applies if the monster is moving normally.
monster->seen_context.clear();
// This appears to be the real one, ie where the movement occurs:
_swim_or_move_energy(monster);
if (grd(monster->pos()) == DNGN_DEEP_WATER && grd(f) != DNGN_DEEP_WATER
&& !monster_habitable_grid(monster, DNGN_DEEP_WATER))
{
monster->seen_context = "emerges from the water";
}
mgrd(monster->pos()) = NON_MONSTER;
coord_def old_pos = monster->pos();
monster->set_position(f);
mgrd(monster->pos()) = monster->mindex();
ballisto_on_move(monster, old_pos);
monster->check_redraw(monster->pos() - delta);
monster->apply_location_effects(monster->pos() - delta);
return (true);
}
static bool _monster_move(monsters *monster)
{
FixedArray<bool, 3, 3> good_move;
const habitat_type habitat = mons_primary_habitat(monster);
bool deep_water_available = false;
if (monster->type == MONS_TRAPDOOR_SPIDER)
{
if (monster->submerged())
return (false);
// Trapdoor spiders hide if they can't see their foe.
// (Note that friendly trapdoor spiders will thus hide even
// if they can see you.)
const actor *foe = monster->get_foe();
const bool can_see = foe && monster->can_see(foe);
if (monster_can_submerge(monster, grd(monster->pos()))
&& !can_see && !mons_is_confused(monster)
&& !monster->caught()
&& !monster->berserk())
{
monster->add_ench(ENCH_SUBMERGED);
monster->behaviour = BEH_LURK;
return (false);
}
}
// Berserking monsters make a lot of racket.
if (monster->berserk())
{
int noise_level = get_shout_noise_level(mons_shouts(monster->type));
if (noise_level > 0)
{
if (you.can_see(monster))
{
if (one_chance_in(10))
{
mprf(MSGCH_TALK_VISUAL, "%s rages.",
monster->name(DESC_CAP_THE).c_str());
}
noisy(noise_level, monster->pos(), monster->mindex());
}
else if (one_chance_in(5))
handle_monster_shouts(monster, true);
else
{
// Just be noisy without messaging the player.
noisy(noise_level, monster->pos(), monster->mindex());
}
}
}
if (monster->confused())
{
if (!mmov.origin() || one_chance_in(15))
{
const coord_def newpos = monster->pos() + mmov;
if (in_bounds(newpos)
&& (habitat == HT_LAND
|| monster_habitable_grid(monster, grd(newpos))))
{
return _do_move_monster(monster, mmov);
}
}
return (false);
}
// If a water monster is currently flopping around on land, it cannot
// really control where it wants to move, though there's a 50% chance
// of flopping into an adjacent water grid.
if (monster->has_ench(ENCH_AQUATIC_LAND))
{
std::vector<coord_def> adj_water;
std::vector<coord_def> adj_move;
for (adjacent_iterator ai(monster->pos()); ai; ++ai)
{
if (!cell_is_solid(*ai))
{
adj_move.push_back(*ai);
if (feat_is_watery(grd(*ai)))
adj_water.push_back(*ai);
}
}
if (adj_move.empty())
{
simple_monster_message(monster, " flops around on dry land!");
return (false);
}
std::vector<coord_def> moves = adj_water;
if (adj_water.empty() || coinflip())
moves = adj_move;
coord_def newpos = monster->pos();
int count = 0;
for (unsigned int i = 0; i < moves.size(); ++i)
if (one_chance_in(++count))
newpos = moves[i];
const monsters *mon2 = monster_at(newpos);
if (newpos == you.pos() && monster->wont_attack()
|| (mon2 && monster->wont_attack() == mon2->wont_attack()))
{
simple_monster_message(monster, " flops around on dry land!");
return (false);
}
return _do_move_monster(monster, newpos - monster->pos());
}
// Let's not even bother with this if mmov is zero.
if (mmov.origin())
return (false);
for (int count_x = 0; count_x < 3; count_x++)
for (int count_y = 0; count_y < 3; count_y++)
{
const int targ_x = monster->pos().x + count_x - 1;
const int targ_y = monster->pos().y + count_y - 1;
// Bounds check: don't consider moving out of grid!
if (!in_bounds(targ_x, targ_y))
{
good_move[count_x][count_y] = false;
continue;
}
dungeon_feature_type target_grid = grd[targ_x][targ_y];
if (target_grid == DNGN_DEEP_WATER)
deep_water_available = true;
good_move[count_x][count_y] =
_mon_can_move_to_pos(monster, coord_def(count_x-1, count_y-1));
}
// Now we know where we _can_ move.
const coord_def newpos = monster->pos() + mmov;
// Normal/smart monsters know about secret doors, since they live in
// the dungeon.
if (grd(newpos) == DNGN_CLOSED_DOOR
|| feat_is_secret_door(grd(newpos)) && mons_intel(monster) >= I_NORMAL)
{
if (mons_is_zombified(monster))
{
// For zombies, monster type is kept in mon->base_monster.
if (mons_class_itemuse(monster->base_monster) >= MONUSE_OPEN_DOORS)
{
_mons_open_door(monster, newpos);
return (true);
}
}
else if (mons_itemuse(monster) >= MONUSE_OPEN_DOORS)
{
_mons_open_door(monster, newpos);
return (true);
}
} // endif - secret/closed doors
// Monsters that eat items (currently only jellies) also eat doors.
// However, they don't realise that secret doors make good eating.
if ((grd(newpos) == DNGN_CLOSED_DOOR || grd(newpos) == DNGN_OPEN_DOOR)
&& mons_itemeat(monster) == MONEAT_ITEMS
// Doors with permarock marker cannot be eaten.
&& !feature_marker_at(newpos, DNGN_PERMAROCK_WALL))
{
grd(newpos) = DNGN_FLOOR;
_jelly_grows(monster);
if (you.see_cell(newpos))
{
viewwindow(false);
if (!you.can_see(monster))
{
mpr("The door mysteriously vanishes.");
interrupt_activity( AI_FORCE_INTERRUPT );
}
else
simple_monster_message(monster, " eats the door!");
}
} // done door-eating jellies
// Water creatures have a preference for water they can hide in -- bwr
// [ds] Weakened the powerful attraction to deep water if the monster
// is in good health.
if (habitat == HT_WATER
&& deep_water_available
&& grd(monster->pos()) != DNGN_DEEP_WATER
&& grd(newpos) != DNGN_DEEP_WATER
&& newpos != you.pos()
&& (one_chance_in(3)
|| monster->hit_points <= (monster->max_hit_points * 3) / 4))
{
int count = 0;
for (int cx = 0; cx < 3; cx++)
for (int cy = 0; cy < 3; cy++)
{
if (good_move[cx][cy]
&& grd[monster->pos().x + cx - 1][monster->pos().y + cy - 1]
== DNGN_DEEP_WATER)
{
if (one_chance_in(++count))
{
mmov.x = cx - 1;
mmov.y = cy - 1;
}
}
}
}
// Now, if a monster can't move in its intended direction, try
// either side. If they're both good, move in whichever dir
// gets it closer (farther for fleeing monsters) to its target.
// If neither does, do nothing.
if (good_move[mmov.x + 1][mmov.y + 1] == false)
_find_good_alternate_move(monster, good_move);
// ------------------------------------------------------------------
// If we haven't found a good move by this point, we're not going to.
// ------------------------------------------------------------------
// Take care of beetle burrowing.
if (mons_class_flag(monster->type, M_BURROWS))
{
const dungeon_feature_type feat = grd(monster->pos() + mmov);
if ((feat == DNGN_ROCK_WALL || feat == DNGN_CLEAR_ROCK_WALL)
&& good_move[mmov.x + 1][mmov.y + 1] == true)
{
grd(monster->pos() + mmov) = DNGN_FLOOR;
set_terrain_changed(monster->pos() + mmov);
if (player_can_hear(monster->pos() + mmov))
{
// Message depends on whether caused by boring beetle or
// acid (Dissolution).
mpr((monster->type == MONS_BORING_BEETLE) ?
"You hear a grinding noise." :
"You hear a sizzling sound.", MSGCH_SOUND);
}
}
}
bool ret = false;
if (good_move[mmov.x + 1][mmov.y + 1] && !mmov.origin())
{
// Check for attacking player.
if (monster->pos() + mmov == you.pos())
{
ret = monster_attack(monster);
mmov.reset();
}
// If we're following the player through stairs, the only valid
// movement is towards the player. -- bwr
if (testbits(monster->flags, MF_TAKING_STAIRS))
{
const delay_type delay = current_delay_action();
if (delay != DELAY_ASCENDING_STAIRS
&& delay != DELAY_DESCENDING_STAIRS)
{
monster->flags &= ~MF_TAKING_STAIRS;
#ifdef DEBUG_DIAGNOSTICS
mprf(MSGCH_DIAGNOSTICS,
"BUG: %s was marked as follower when not following!",
monster->name(DESC_PLAIN).c_str(), true);
#endif
}
else
{
ret = true;
mmov.reset();
#if DEBUG_DIAGNOSTICS
mprf(MSGCH_DIAGNOSTICS,
"%s is skipping movement in order to follow.",
monster->name(DESC_CAP_THE).c_str(), true );
#endif
}
}
// Check for attacking another monster.
if (monsters* targ = monster_at(monster->pos() + mmov))
{
if (mons_aligned(monster->mindex(), targ->mindex()))
ret = _monster_swaps_places(monster, mmov);
else
{
monsters_fight(monster, targ);
ret = true;
}
// If the monster swapped places, the work's already done.
mmov.reset();
}
if (monster->type == MONS_EFREET
|| monster->type == MONS_FIRE_ELEMENTAL)
{
place_cloud( CLOUD_FIRE, monster->pos(),
2 + random2(4), monster->kill_alignment(),
KILL_MON_MISSILE );
}
if (monster->type == MONS_ROTTING_DEVIL
|| monster->type == MONS_CURSE_TOE)
{
place_cloud( CLOUD_MIASMA, monster->pos(),
2 + random2(3), monster->kill_alignment(),
KILL_MON_MISSILE );
}
// Commented out, but left in as an example of gloom. {due}
//if (monster->type == MONS_SHADOW)
//{
// big_cloud (CLOUD_GLOOM, monster->kill_alignment(), monster->pos(), 10 + random2(5), 2 + random2(8));
//}
}
else
{
mmov.reset();
// Fleeing monsters that can't move will panic and possibly
// turn to face their attacker.
make_mons_stop_fleeing(monster);
}
if (mmov.x || mmov.y || (monster->confused() && one_chance_in(6)))
return (_do_move_monster(monster, mmov));
return (ret);
}
static void _mons_in_cloud(monsters *monster)
{
int wc = env.cgrid(monster->pos());
int hurted = 0;
bolt beam;
const int speed = ((monster->speed > 0) ? monster->speed : 10);
bool wake = false;
if (mons_is_mimic( monster->type ))
{
mimic_alert(monster);
return;
}
const cloud_struct &cloud(env.cloud[wc]);
switch (cloud.type)
{
case CLOUD_DEBUGGING:
mprf(MSGCH_ERROR,
"Monster %s stepped on a nonexistent cloud at (%d,%d)",
monster->name(DESC_PLAIN, true).c_str(),
monster->pos().x, monster->pos().y);
return;
case CLOUD_FIRE:
case CLOUD_FOREST_FIRE:
if (monster->type == MONS_FIRE_VORTEX
|| monster->type == MONS_EFREET
|| monster->type == MONS_FIRE_ELEMENTAL)
{
return;
}
simple_monster_message(monster, " is engulfed in flames!");
hurted +=
resist_adjust_damage( monster,
BEAM_FIRE,
monster->res_fire(),
((random2avg(16, 3) + 6) * 10) / speed );
hurted -= random2(1 + monster->ac);
break;
case CLOUD_STINK:
simple_monster_message(monster, " is engulfed in noxious gasses!");
if (monster->res_poison() > 0)
return;
beam.flavour = BEAM_CONFUSION;
beam.thrower = cloud.killer;
if (cloud.whose == KC_FRIENDLY)
beam.beam_source = ANON_FRIENDLY_MONSTER;
if (mons_class_is_confusable(monster->type)
&& 1 + random2(27) >= monster->hit_dice)
{
beam.apply_enchantment_to_monster(monster);
}
hurted += (random2(3) * 10) / speed;
break;
case CLOUD_COLD:
simple_monster_message(monster, " is engulfed in freezing vapours!");
hurted +=
resist_adjust_damage( monster,
BEAM_COLD,
monster->res_cold(),
((6 + random2avg(16, 3)) * 10) / speed );
hurted -= random2(1 + monster->ac);
break;
case CLOUD_POISON:
simple_monster_message(monster, " is engulfed in a cloud of poison!");
if (monster->res_poison() > 0)
return;
poison_monster(monster, cloud.whose);
// If the monster got poisoned, wake it up.
wake = true;
hurted += (random2(8) * 10) / speed;
if (monster->res_poison() < 0)
hurted += (random2(4) * 10) / speed;
break;
case CLOUD_STEAM:
{
// FIXME: couldn't be bothered coding for armour of res fire
simple_monster_message(monster, " is engulfed in steam!");
const int steam_base_damage = steam_cloud_damage(cloud);
hurted +=
resist_adjust_damage(
monster,
BEAM_STEAM,
monster->res_steam(),
(random2avg(steam_base_damage, 2) * 10) / speed);
hurted -= random2(1 + monster->ac);
break;
}
case CLOUD_MIASMA:
simple_monster_message(monster, " is engulfed in a dark miasma!");
if (monster->res_rotting())
return;
miasma_monster(monster, cloud.whose);
hurted += (10 * random2avg(12, 3)) / speed; // 3
break;
case CLOUD_RAIN:
if (monster->is_fiery())
{
if (!silenced(monster->pos()))
simple_monster_message(monster, " sizzles in the rain!");
else
simple_monster_message(monster, " steams in the rain!");
hurted += ((4 * random2(3)) - random2(monster->ac));
wake = true;
}
break;
case CLOUD_MUTAGENIC:
simple_monster_message(monster, " is engulfed in a mutagenic fog!");
// Will only polymorph a monster if they're not magic immune, can
// mutate, aren't res asphyx, and pass the same check as meph cloud.
if (monster->can_mutate() && !mons_immune_magic(monster)
&& 1 + random2(27) >= monster->hit_dice
&& !monster->res_asphyx())
{
if (monster->mutate())
wake = true;
}
break;
default: // 'harmless' clouds -- colored smoke, etc {dlb}.
return;
}
// A sleeping monster that sustains damage will wake up.
if ((wake || hurted > 0) && monster->asleep())
{
// We have no good coords to give the monster as the source of the
// disturbance other than the cloud itself.
behaviour_event(monster, ME_DISTURB, MHITNOT, monster->pos());
}
if (hurted > 0)
{
#ifdef DEBUG_DIAGNOSTICS
mprf(MSGCH_DIAGNOSTICS, "%s takes %d damage from cloud.",
monster->name(DESC_CAP_THE).c_str(), hurted);
#endif
monster->hurt(NULL, hurted, BEAM_MISSILE, false);
if (monster->hit_points < 1)
{
mon_enchant death_ench(ENCH_NONE, 0, cloud.whose);
monster_die(monster, cloud.killer, death_ench.kill_agent());
}
}
}
static spell_type _map_wand_to_mspell(int wand_type)
{
switch (wand_type)
{
case WAND_FLAME: return SPELL_THROW_FLAME;
case WAND_FROST: return SPELL_THROW_FROST;
case WAND_SLOWING: return SPELL_SLOW;
case WAND_HASTING: return SPELL_HASTE;
case WAND_MAGIC_DARTS: return SPELL_MAGIC_DART;
case WAND_HEALING: return SPELL_MINOR_HEALING;
case WAND_PARALYSIS: return SPELL_PARALYSE;
case WAND_FIRE: return SPELL_BOLT_OF_FIRE;
case WAND_COLD: return SPELL_BOLT_OF_COLD;
case WAND_CONFUSION: return SPELL_CONFUSE;
case WAND_INVISIBILITY: return SPELL_INVISIBILITY;
case WAND_TELEPORTATION: return SPELL_TELEPORT_OTHER;
case WAND_LIGHTNING: return SPELL_LIGHTNING_BOLT;
case WAND_DRAINING: return SPELL_BOLT_OF_DRAINING;
case WAND_DISINTEGRATION: return SPELL_DISINTEGRATE;
case WAND_POLYMORPH_OTHER: return SPELL_POLYMORPH_OTHER;
default: return SPELL_NO_SPELL;
}
}