From 204607ad3e8aecb32e01f303b1e86545d3d57f62 Mon Sep 17 00:00:00 2001 From: Matthew Cline Date: Sun, 1 Nov 2009 01:55:04 -0700 Subject: Split up monstuff.cc A lot of monstuff.cc was moved into mon-abil.cc (monster abilities), mon-act.cc (the main monster loop), mon-behv.cc (monster behaviour) and mon-cast.cc (monster spells). mstuff2.cc was completely merged into other files. --- crawl-ref/source/mon-act.cc | 3617 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3617 insertions(+) create mode 100644 crawl-ref/source/mon-act.cc (limited to 'crawl-ref/source/mon-act.cc') diff --git a/crawl-ref/source/mon-act.cc b/crawl-ref/source/mon-act.cc new file mode 100644 index 0000000000..42ead2a820 --- /dev/null +++ b/crawl-ref/source/mon-act.cc @@ -0,0 +1,3617 @@ +/* + * File: mon-act.cc + * Summary: Monsters doing stuff (monsters acting). + * Written by: Linley Henzell + */ + +#include "AppHdr.h" +#include "mon-act.h" + +#ifdef TARGET_OS_DOS +#include +#endif + +#include "arena.h" +#include "beam.h" +#include "cloud.h" +#include "delay.h" +#include "directn.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 "monplace.h" +#include "monstuff.h" +#include "mutation.h" +#include "notes.h" +#include "player.h" +#include "random.h" +#include "religion.h" +#include "shopping.h" // for item values +#include "state.h" +#include "terrain.h" +#include "traps.h" +#include "tutorial.h" +#include "view.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)) + { + heal_monster(monster, 1, false); + } +} + +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 (mons_wont_attack_real(mover) == mons_wont_attack_real(moved) + || 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 ((mons_friendly(mover) || mons_friendly(moved)) + && 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->pos() = moved_pos; + moved->pos() = 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 (!mons_is_shapeshifter(monster) + || !mons_class_flag(monster->type, M_ACTUAL_SPELLS)) + { + 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 && !mons_wont_attack(mon) + && 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 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 && !mons_wont_attack_real(mon) + && 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 (mons_is_holy(mon)); + + // 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) + && !mons_friendly(monster) + && !monster->is_patrolling() + && _mon_on_interesting_grid(monster)) + { + monster->patrol_point = monster->pos(); + } +} + +//--------------------------------------------------------------- +// +// 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->has_ench(ENCH_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(); + + if (crawl_state.arena && Options.arena_force_ai + && !mons_is_stationary(monster)) + { + const bool ranged = (mons_has_ranged_attack(monster) + || mons_has_ranged_spell(monster)); + + // Smiters are happy if they have clear visibility through + // glass, but other monsters must go around. + const bool glass_ok = mons_has_smite_attack(monster); + + // Monsters in the arena are smarter than the norm and + // always pathfind to their targets. + if (delta.abs() > 2 + && (!ranged + || !monster->mon_see_cell(monster->target, !glass_ok))) + { + monster_pathfind mp; + if (mp.init_pathfind(monster, monster->target)) + delta = mp.next_pos(monster->pos()) - monster->pos(); + } + } + } + + // Move the monster. + mmov.x = (delta.x > 0) ? 1 : ((delta.x < 0) ? -1 : 0); + mmov.y = (delta.y > 0) ? 1 : ((delta.y < 0) ? -1 : 0); + + if (mons_is_fleeing(monster) && monster->travel_target != MTRAV_WALL + && (!mons_friendly(monster) + || 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() + || mgrd(newpos) != NON_MONSTER && monster->foe == mgrd(newpos)) + && mons_intel(monster) >= I_ANIMAL + && coinflip() + && !mons_is_confused(monster) && !mons_is_caught(monster) + && !monster->has_ench(ENCH_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 + && !mons_wont_attack_real(monster) + && _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 + && !mons_wont_attack_real(monster) + && _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 + && !mons_wont_attack_real(monster) + && _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 + && !mons_wont_attack_real(monster) + && _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 (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 && 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.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; + const int wpn = monster->inv[MSLOT_WEAPON]; + + if (monster->submerged()) + return (false); + + if (mons_aligned(monster_index(monster), monster->foe)) + return (false); + + if (wpn != NON_ITEM && get_weapon_brand(mitm[wpn]) == SPWPN_REACHING) + { + if (monster->foe == MHITYOU) + { + const coord_def delta = monster->pos() - you.pos(); + const int x_middle = std::max(monster->pos().x, you.pos().x) + - (abs(delta.x) / 2); + const int y_middle = std::max(monster->pos().y, you.pos().y) + - (abs(delta.y) / 2); + const coord_def middle(x_middle, y_middle); + + // This check isn't redundant -- player may be invisible. + if (monster->target == you.pos() + && grid_distance(monster->pos(), you.pos()) == 2 + && (see_cell_no_trans(monster->pos()) + || grd(middle) > DNGN_MAX_NONREACH)) + { + ret = true; + monster_attack(monster, false); + } + } + else if (monster->foe != MHITNOT) + { + monsters& mfoe = menv[monster->foe]; + coord_def foepos = mfoe.pos(); + // Same comments as to invisibility as above. + if (monster->target == foepos + && monster->mon_see_cell(foepos, true) + && grid_distance(monster->pos(), foepos) == 2) + { + ret = true; + monsters_fight(monster, &mfoe, false); + } + } + } + + // Player saw the item reach. + if (ret && !is_artefact(mitm[wpn]) && you.can_see(monster)) + set_ident_flags(mitm[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 (mons_is_caught(monster) || mons_is_fleeing(monster) + || mons_is_pacified(monster)) + { + simple_monster_message(monster, " reads a scroll."); + monster_teleport(monster, false); + read = true; + ident = ID_KNOWN_TYPE; + } + } + break; + + case SCR_BLINKING: + if (mons_is_caught(monster) || mons_is_fleeing(monster) + || mons_is_pacified(monster)) + { + 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), + 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_index(monster); + 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) + && (!mons_friendly(monster) || 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 + || mons_is_caught(monster)) + { + 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_index(monster); + + 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 (mons_friendly(monster)) + 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) + 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_HAND_CROSSBOW: + baseHit = 2; + hitMult = 50; + damMult = 20; + 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); + } + } + + // [dshaligram] When changing bolt names here, you must edit + // hiscores.cc (scorefile_entry::terse_missile_cause()) to match. + char throw_buff[ITEMNAME_SIZE]; + if (projected == LRET_LAUNCHED) + { + snprintf(throw_buff, sizeof(throw_buff), "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 + { + snprintf(throw_buff, sizeof(throw_buff), "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()); + } + + pbolt.aux_source = throw_buff; + + // 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(true, false); + pbolt.fire(); + + // The item can be destroyed before returning. + if (really_returns && thrown_object_destroyed(&item, pbolt.target, true)) + { + really_returns = false; + } + + if (really_returns) + { + // Fire beam in reverse. + pbolt.setup_retrace(); + viewwindow(true, 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); + + // Greatly lowered chances if the monster is fleeing or pacified and + // leaving the level. + if ((mons_is_fleeing(monster) || mons_is_pacified(monster)) + && !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 || !is_valid_item(mitm[mon_item])) + 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) + if (mons_near(monster)) + 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); + + // 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; + + 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 (mons_cannot_act(monster)) + { + 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 (!mons_neutral(monster) && !monster->has_ench(ENCH_CHARM) + || (you.religion == GOD_JIYVA && mons_is_slime(monster)) + && (!mons_friendly(monster) + || 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 (mons_is_caught(monster)) + { + // Struggling against the net takes time. + _swim_or_move_energy(monster); + } + else if (!mons_is_petrified(monster)) + { + // Calculates mmov based on monster target. + _handle_movement(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->has_ench(ENCH_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 = + mons_friendly(monster) || 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 (!mons_is_caught(monster)) + { + if (monster->pos() + mmov == you.pos()) + { + ASSERT(!crawl_state.arena); + + if (!mons_friendly(monster)) + { + // 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 (mons_cannot_move(monster) || !_monster_move(monster)) + monster->speed_increment -= non_move_energy; + } + update_beholders(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 (int i = 0; i < MAX_MONSTERS; ++i) + { + monsters *monster = &menv[i]; + + if (!monster->alive() || immobile_monster[i]) + continue; + + const coord_def oldpos = monster->pos(); + + _handle_monster_move(monster); + + if (!invalid_monster(monster) && monster->pos() != oldpos) + immobile_monster[i] = true; + + // If the player got banished, discard pending monster actions. + if (you.banished) + { + // Clear list of mesmerising monsters. + if (you.duration[DUR_MESMERISED]) + { + you.mesmerised_by.clear(); + you.duration[DUR_MESMERISED] = 0; + } + 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); + + int k = 0; + + // Now that we have a spot, find a monster slot {dlb}: + for (k = 0; k < MAX_MONSTERS; k++) + { + child = &menv[k]; + + if (child->type == -1) + break; + else if (k == MAX_MONSTERS - 1) + 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()) = k; + + 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 (mons_friendly(monster) && you.religion != GOD_JIYVA) + return (false); + + int hps_gained = 0; + int max_eat = roll_dice(1, 10); + int eaten = 0; + bool eaten_net = false; + + for (stack_iterator si(monster->pos()); + si && eaten < max_eat && hps_gained < 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; + + if (si->base_type != OBJ_GOLD) + { + quant = std::min(quant, max_eat - eaten); + + hps_gained += (quant * item_mass(*si)) / 20 + quant; + eaten += quant; + + if (mons_is_caught(monster) + && 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_gained += 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 / 2 + 1, 30 + you.piety / 4)) + { + if (timeout <= 0) + pg += random2(item_value(*si) / 6); + 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 = (100 + 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_gained = std::max(hps_gained, 1); + hps_gained = std::min(hps_gained, 50); + + // This is done manually instead of using heal_monster(), + // because that function doesn't work quite this way. -- bwr + monster->hit_points += hps_gained; + 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 (eaten_net) + simple_monster_message(monster, " devours the net!"); + + _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(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 ((mons_wont_attack(monster) + || 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); + + 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 && mons_friendly(monster) && 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 + || mons_is_pacified(monster)) + { + 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 (mons_wont_attack(monster) || 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 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::iterator i = all_door.begin(); + i != all_door.end(); ++i) + { + const coord_def& dc = *i; + if (grd(dc) == DNGN_SECRET_DOOR && see_cell(dc)) + { + grid = grid_secret_door_appearance(dc); + was_secret = true; + } + + if (see_cell(dc)) + was_seen = true; + else + set_terrain_changed(dc); + + grd[dc.x][dc.y] = DNGN_OPEN_DOOR; + } + + if (was_seen) + { + viewwindow(true, 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 = monster_index(mpushee); + 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) + || mons_cannot_move(mpusher) || mons_cannot_move(mpushee) + || 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 (!mons_wont_attack(monster) + && 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 (monster->type == 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 && 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 (mons_wont_attack(monster) + && 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& 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->pos() = n; + mgrd(n) = monster_index(mon); + m2->pos() = c; + const int m2i = monster_index(m2); + 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 && !see_cell(f)) + simple_monster_message(monster, " moves out of view."); + else if (Options.tutorial_left && (monster->flags & MF_WAS_IN_VIEW) + && !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; + + monster->pos() = f; + + mgrd(monster->pos()) = monster_index(monster); + + monster->check_redraw(monster->pos() - delta); + monster->apply_location_effects(monster->pos() - delta); + + return (true); +} + +static bool _monster_move(monsters *monster) +{ + FixedArray 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->has_ench(ENCH_BERSERK)) + { + monster->add_ench(ENCH_SUBMERGED); + monster->behaviour = BEH_LURK; + return (false); + } + } + + // Berserking monsters make a lot of racket. + if (monster->has_ench(ENCH_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 adj_water; + std::vector 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 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() && mons_wont_attack(monster) + || (mon2 && mons_wont_attack(monster) == mons_wont_attack(mon2))) + { + + 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 (see_cell(newpos)) + { + viewwindow(true, false); + + if (!you.can_see(monster)) + { + mpr("The door mysteriously vanishes."); + interrupt_activity( AI_FORCE_INTERRUPT ); + } + } + } // 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() ); + } + + if (monster->type == MONS_ROTTING_DEVIL + || monster->type == MONS_CURSE_TOE) + { + place_cloud( CLOUD_MIASMA, monster->pos(), + 2 + random2(3), monster->kill_alignment() ); + } + } + 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; + } +} + -- cgit v1.2.3-54-g00ecf