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