/* * File: monstuff.cc * Summary: Misc monster related functions. * Written by: Linley Henzell * * Modified for Crawl Reference by $Author$ on $Date$ */ #include "AppHdr.h" REVISION("$Rev$"); #include "monstuff.h" #include #include #include #include #ifdef DOS #include #endif #include "externs.h" #include "arena.h" #include "beam.h" #include "cloud.h" #include "database.h" #include "debug.h" #include "delay.h" #include "describe.h" #include "dgnevent.h" #include "fight.h" #include "files.h" #include "ghost.h" #include "hiscores.h" #include "it_use2.h" #include "itemname.h" #include "items.h" #include "itemprop.h" #include "Kills.h" #include "makeitem.h" #include "mapmark.h" #include "message.h" #include "misc.h" #include "monplace.h" #include "monspeak.h" #include "mon-pick.h" #include "mon-util.h" #include "mstuff2.h" #include "notes.h" #include "player.h" #include "randart.h" #include "religion.h" #include "spl-mis.h" #include "spl-util.h" #include "spells3.h" #include "spells4.h" #include "state.h" #include "stuff.h" #include "terrain.h" #include "traps.h" #include "tutorial.h" #include "view.h" #include "stash.h" #include "xom.h" static bool _wounded_damaged(int wound_class); static bool _handle_special_ability(monsters *monster, bolt & beem); static bool _handle_pickup(monsters *monster); static void _handle_behaviour(monsters *monster); static void _set_nearest_monster_foe(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); static bool _try_pathfind(monsters *mon, const dungeon_feature_type can_move, bool potentially_blocking); // [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"); #define ENERGY_SUBMERGE(entry) (std::max(entry->energy_usage.swim / 2, 1)) // This function creates an artificial item to represent a mimic's appearance. // Eventually, mimics could be redone to be more like dancing weapons... // there'd only be one type and it would look like the item it carries. -- bwr void get_mimic_item( const monsters *mimic, item_def &item ) { ASSERT( mimic != NULL && mons_is_mimic( mimic->type ) ); item.base_type = OBJ_UNASSIGNED; item.sub_type = 0; item.special = 0; item.colour = 0; item.flags = 0; item.quantity = 1; item.plus = 0; item.plus2 = 0; item.pos = mimic->pos(); item.link = NON_ITEM; int prop = 127 * mimic->pos().x + 269 * mimic->pos().y; rng_save_excursion exc; seed_rng( prop ); switch (mimic->type) { case MONS_WEAPON_MIMIC: item.base_type = OBJ_WEAPONS; item.sub_type = (59 * mimic->pos().x + 79 * mimic->pos().y) % (WPN_MAX_NONBLESSED + 1); prop %= 100; if (prop < 20) make_item_randart(item); else if (prop < 50) set_equip_desc(item, ISFLAG_GLOWING); else if (prop < 80) set_equip_desc(item, ISFLAG_RUNED); else if (prop < 85) set_equip_race(item, ISFLAG_ORCISH); else if (prop < 90) set_equip_race(item, ISFLAG_DWARVEN); else if (prop < 95) set_equip_race(item, ISFLAG_ELVEN); break; case MONS_ARMOUR_MIMIC: item.base_type = OBJ_ARMOUR; item.sub_type = (59 * mimic->pos().x + 79 * mimic->pos().y) % NUM_ARMOURS; prop %= 100; if (prop < 20) make_item_randart(item); else if (prop < 40) set_equip_desc(item, ISFLAG_GLOWING); else if (prop < 60) set_equip_desc(item, ISFLAG_RUNED); else if (prop < 80) set_equip_desc(item, ISFLAG_EMBROIDERED_SHINY); else if (prop < 85) set_equip_race(item, ISFLAG_ORCISH); else if (prop < 90) set_equip_race(item, ISFLAG_DWARVEN); else if (prop < 95) set_equip_race(item, ISFLAG_ELVEN); break; case MONS_SCROLL_MIMIC: item.base_type = OBJ_SCROLLS; item.sub_type = prop % NUM_SCROLLS; break; case MONS_POTION_MIMIC: item.base_type = OBJ_POTIONS; item.sub_type = prop % NUM_POTIONS; break; case MONS_GOLD_MIMIC: default: item.base_type = OBJ_GOLD; item.quantity = 5 + prop % 30; break; } item_colour(item); // also sets special vals for scrolls/potions } // Sets the colour of a mimic to match its description... should be called // whenever a mimic is created or teleported. -- bwr int get_mimic_colour( const monsters *mimic ) { ASSERT( mimic != NULL && mons_is_mimic( mimic->type ) ); if (mimic->type == MONS_SCROLL_MIMIC) return (LIGHTGREY); else if (mimic->type == MONS_GOLD_MIMIC) return (YELLOW); item_def item; get_mimic_item( mimic, item ); return (item.colour); } // Monster curses a random player inventory item. bool curse_an_item( bool decay_potions, bool quiet ) { int count = 0; int item = ENDOFPACK; for (int i = 0; i < ENDOFPACK; i++) { if (!is_valid_item( you.inv[i] )) continue; if (you.inv[i].base_type == OBJ_WEAPONS || you.inv[i].base_type == OBJ_ARMOUR || you.inv[i].base_type == OBJ_JEWELLERY || you.inv[i].base_type == OBJ_POTIONS) { if (item_cursed( you.inv[i] )) continue; if (you.inv[i].base_type != OBJ_POTIONS && !you_tran_can_wear(you.inv[i]) && item_is_equipped(you.inv[i])) { // Melded items cannot be cursed. continue; } if (you.inv[i].base_type == OBJ_POTIONS && (!decay_potions || you.inv[i].sub_type == POT_DECAY)) { continue; } // Item is valid for cursing, so we'll give it a chance. count++; if (one_chance_in( count )) item = i; } } // Any item to curse? if (item == ENDOFPACK) return (false); // Curse item. if (decay_potions && !quiet) // Just for mummies. mpr("You feel nervous for a moment...", MSGCH_MONSTER_SPELL); if (you.inv[item].base_type == OBJ_POTIONS) { int amount; // Decay at least two of the stack. if (you.inv[item].quantity <= 2) amount = you.inv[item].quantity; else amount = 2 + random2(you.inv[item].quantity - 1); split_potions_into_decay(item, amount); if (item_value(you.inv[item], true) / amount > 2) xom_is_stimulated(32 * amount); } else { do_curse_item( you.inv[item], false ); } return (true); } void monster_drop_ething(monsters *monster, bool mark_item_origins, int owner_id) { const bool hostile_grid = grid_destroys_items(grd(monster->pos())); bool destroyed = false; // Drop weapons & missiles last (ie on top) so others pick up. for (int i = NUM_MONSTER_SLOTS - 1; i >= 0; i--) { int item = monster->inv[i]; if (item != NON_ITEM) { const bool summoned_item = testbits(mitm[item].flags, ISFLAG_SUMMONED); if (hostile_grid || summoned_item) { item_was_destroyed(mitm[item], monster->mindex()); destroy_item( item ); if (!summoned_item) destroyed = true; } else { if (mons_friendly(monster) && is_valid_item(mitm[item])) mitm[item].flags |= ISFLAG_DROPPED_BY_ALLY; move_item_to_grid(&item, monster->pos()); if (mark_item_origins && is_valid_item(mitm[item])) origin_set_monster(mitm[item], monster); } monster->inv[i] = NON_ITEM; } } if (destroyed) mprf(MSGCH_SOUND, grid_item_destruction_message(grd(monster->pos()))); } int fill_out_corpse(const monsters* monster, item_def& corpse, bool allow_weightless) { ASSERT(monster->type != -1 && monster->type != MONS_PROGRAM_BUG); corpse.clear(); int summon_type; if (mons_is_summoned(monster, NULL, &summon_type) || (monster->flags & (MF_BANISHED | MF_HARD_RESET))) { return (-1); } int corpse_class = mons_species(monster->type); // If this was a corpse that was temporarily animated then turn the // monster back into a corpse. if (mons_class_is_zombified(monster->type) && (summon_type == SPELL_ANIMATE_DEAD || summon_type == SPELL_ANIMATE_SKELETON || summon_type == MON_SUMM_ANIMATE)) { corpse_class = mons_zombie_base(monster); } if (corpse_class == MONS_DRACONIAN) { if (monster->type == MONS_TIAMAT) corpse_class = MONS_DRACONIAN; else corpse_class = draco_subspecies(monster); } if (monster->has_ench(ENCH_SHAPESHIFTER)) corpse_class = MONS_SHAPESHIFTER; else if (monster->has_ench(ENCH_GLOWING_SHAPESHIFTER)) corpse_class = MONS_GLOWING_SHAPESHIFTER; // Doesn't leave a corpse. if (mons_weight(corpse_class) == 0 && !allow_weightless) return (-1); corpse.flags = 0; corpse.base_type = OBJ_CORPSES; corpse.plus = corpse_class; corpse.plus2 = 0; // butcher work done corpse.sub_type = CORPSE_BODY; corpse.special = FRESHEST_CORPSE; // rot time corpse.quantity = 1; corpse.orig_monnum = monster->type + 1; corpse.props[MONSTER_NUMBER] = short(monster->number); corpse.colour = mons_class_colour(corpse_class); if (corpse.colour == BLACK) corpse.colour = monster->colour; if (!monster->mname.empty()) corpse.props[CORPSE_NAME_KEY] = monster->mname; else if (mons_is_unique(monster->type)) { corpse.props[CORPSE_NAME_KEY] = mons_type_name(monster->type, DESC_PLAIN); } return (corpse_class); } int place_monster_corpse(const monsters *monster, bool silent, bool force) { // The game can attempt to place a corpse for an out-of-bounds monster // if a shifter turns into a giant spore and explodes. In this // case we place no corpse since the explosion means anything left // over would be scattered, tiny chunks of shifter. if (!in_bounds(monster->pos())) return (-1); // Don't attempt to place corpses within walls, either. // Currently, this only applies to (shapeshifter) rock worms. if (grid_is_wall(grd(monster->pos()))) return (-1); item_def corpse; const int corpse_class = fill_out_corpse(monster, corpse); // Don't place a corpse? If a zombified monster is somehow capable // of leaving a corpse, then always place it. if (mons_class_is_zombified(monster->type)) force = true; if (corpse_class == -1 || (!force && coinflip())) return (-1); if (grid_destroys_items(grd(monster->pos()))) { item_was_destroyed(corpse); return (-1); } int o = get_item_slot(); if (o == NON_ITEM) return (-1); mitm[o] = corpse; origin_set_monster(mitm[o], monster); // Don't care if 'o' is changed, and it shouldn't be (corpses don't // stack). move_item_to_grid(&o, monster->pos()); if (see_grid(monster->pos())) { if (force && !silent) { if (you.can_see(monster)) simple_monster_message(monster, " turns back into a corpse!"); else { mprf("%s appears out of nowhere!", mitm[o].name(DESC_CAP_A).c_str()); } } const bool poison = (mons_corpse_effect(corpse_class) == CE_POISONOUS && player_res_poison() <= 0); tutorial_dissection_reminder(!poison); } return (o); } static void _tutorial_inspect_kill() { if (Options.tutorial_events[TUT_KILLED_MONSTER]) learned_something_new(TUT_KILLED_MONSTER); } #ifdef DGL_MILESTONES static std::string _milestone_kill_verb(killer_type killer) { return (killer == KILL_RESET ? "banished " : "killed "); } static void _check_kill_milestone(const monsters *mons, killer_type killer, int i) { if (mons->type == MONS_PLAYER_GHOST) { std::string milestone = _milestone_kill_verb(killer) + "the ghost of "; milestone += get_ghost_description(*mons, true); milestone += "."; mark_milestone("ghost", milestone); } else if (mons_is_unique(mons->type)) { mark_milestone("unique", _milestone_kill_verb(killer) + mons->name(DESC_NOCAP_THE, true) + "."); } } #endif // DGL_MILESTONES static void _give_monster_experience(monsters *victim, int killer_index, int experience, bool victim_was_born_friendly) { if (invalid_monster_index(killer_index)) return; monsters *mon = &menv[killer_index]; if (!mon->alive()) return; if ((!victim_was_born_friendly || !mons_friendly(mon)) && !mons_aligned(killer_index, monster_index(victim))) { if (mon->gain_exp(experience)) { if (you.religion != GOD_SHINING_ONE && you.religion != GOD_BEOGH || player_under_penance() || !one_chance_in(3)) { return; } // Randomly bless the follower who gained experience. if (you.religion == GOD_SHINING_ONE && random2(you.piety) >= piety_breakpoint(0) || you.religion == GOD_BEOGH && random2(you.piety) >= piety_breakpoint(2)) { bless_follower(mon); } } } } static void _give_adjusted_experience(monsters *monster, killer_type killer, bool pet_kill, int killer_index, unsigned int *exp_gain, unsigned int *avail_gain) { const int experience = exper_value(monster); const bool created_friendly = testbits(monster->flags, MF_CREATED_FRIENDLY); const bool was_neutral = testbits(monster->flags, MF_WAS_NEUTRAL); const bool no_xp = monster->has_ench(ENCH_ABJ) || !experience; const bool already_got_half_xp = testbits(monster->flags, MF_GOT_HALF_XP); bool need_xp_msg = false; if (created_friendly || was_neutral || no_xp) ; // No experience if monster was created friendly or summoned. else if (YOU_KILL(killer)) { int old_lev = you.experience_level; if (already_got_half_xp) gain_exp( experience / 2, exp_gain, avail_gain ); else gain_exp( experience, exp_gain, avail_gain ); if (old_lev == you.experience_level) need_xp_msg = true; } else if (pet_kill && !already_got_half_xp) { int old_lev = you.experience_level; gain_exp( experience / 2 + 1, exp_gain, avail_gain ); if (old_lev == you.experience_level) need_xp_msg = true; } // FIXME: Since giant spores get detached from mgrd early // on, we can't tell by this point if they were visible when // they exploded. Rather than bothering to remember this, we // just suppress the message. if (monster->type == MONS_GIANT_SPORE || monster->type == MONS_BALL_LIGHTNING) { need_xp_msg = false; } // Give a message for monsters dying out of sight. if (need_xp_msg && exp_gain > 0 && !you.can_see(monster) && !crawl_state.arena) { mpr("You feel a bit more experienced."); } if (MON_KILL(killer) && !no_xp) { _give_monster_experience( monster, killer_index, experience, created_friendly ); } } static bool _is_pet_kill(killer_type killer, int i) { if (!MON_KILL(killer)) return (false); if (i == ANON_FRIENDLY_MONSTER) return (true); if (invalid_monster_index(i)) return (false); const monsters *m = &menv[i]; if (mons_friendly(m)) // This includes enslaved monsters. return (true); // Check if the monster was confused by you or a friendly, which // makes casualties to this monster collateral kills. const mon_enchant me = m->get_ench(ENCH_CONFUSION); return (me.ench == ENCH_CONFUSION && (me.who == KC_YOU || me.who == KC_FRIENDLY)); } // Elyvilon will occasionally (5% chance) protect the life of one of // your allies. static bool _ely_protect_ally(monsters *monster) { if (you.religion != GOD_ELYVILON) return (false); if (mons_holiness(monster) != MH_NATURAL && mons_holiness(monster) != MH_HOLY || !mons_friendly(monster) || !you.can_see(monster) // for simplicity || !one_chance_in(20)) { return (false); } monster->hit_points = 1; snprintf(info, INFO_SIZE, " protects %s from harm!%s", monster->name(DESC_NOCAP_THE).c_str(), coinflip() ? "" : " You feel responsible."); simple_god_message(info); lose_piety(1); return (true); } // Elyvilon retribution effect: Heal hostile monsters that were about to // be killed by you or one of your friends. static bool _ely_heal_monster(monsters *monster, killer_type killer, int i) { if (you.religion == GOD_ELYVILON) return (false); god_type god = GOD_ELYVILON; if (!you.penance[god] || !god_hates_your_god(god)) return (false); const int ely_penance = you.penance[god]; if (mons_friendly(monster) || !one_chance_in(10)) return (false); if (MON_KILL(killer) && !invalid_monster_index(i)) { monsters *mon = &menv[i]; if (!mons_friendly(mon) || !one_chance_in(3)) return (false); if (!mons_near(monster)) return (false); } else if (!YOU_KILL(killer)) return (false); #ifdef DEBUG_DIAGNOSTICS mprf(MSGCH_DIAGNOSTICS, "monster hp: %d, max hp: %d", monster->hit_points, monster->max_hit_points); #endif monster->hit_points = std::min(1 + random2(ely_penance/3), monster->max_hit_points); #ifdef DEBUG_DIAGNOSTICS mprf(MSGCH_DIAGNOSTICS, "new hp: %d, ely penance: %d", monster->hit_points, ely_penance); #endif snprintf(info, INFO_SIZE, "%s heals %s%s", god_name(god, false).c_str(), monster->name(DESC_NOCAP_THE).c_str(), monster->hit_points * 2 <= monster->max_hit_points ? "." : "!"); god_speaks(god, info); dec_penance(god, 1 + random2(monster->hit_points/2)); return (true); } static bool _yred_enslave_soul(monsters *monster, killer_type killer) { if (you.religion == GOD_YREDELEMNUL && mons_enslaved_body_and_soul(monster) && mons_near(monster) && killer != KILL_RESET && killer != KILL_DISMISSED) { yred_make_enslaved_soul(monster, player_under_penance()); return (true); } return (false); } static bool _beogh_forcibly_convert_orc(monsters *monster, killer_type killer, int i) { if (you.religion == GOD_BEOGH && mons_species(monster->type) == MONS_ORC && !mons_is_summoned(monster) && !mons_is_shapeshifter(monster) && !player_under_penance() && you.piety >= piety_breakpoint(2) && mons_near(monster)) { bool convert = false; if (YOU_KILL(killer)) convert = true; else if (MON_KILL(killer) && !invalid_monster_index(i)) { monsters *mon = &menv[i]; if (is_follower(mon) && !one_chance_in(3)) convert = true; } // Orcs may convert to Beogh under threat of death, either from // you or, less often, your followers. In both cases, the // checks are made against your stats. You're the potential // messiah, after all. if (convert) { #ifdef DEBUG_DIAGNOSTICS mprf(MSGCH_DIAGNOSTICS, "Death convert attempt on %s, HD: %d, " "your xl: %d", monster->name(DESC_PLAIN).c_str(), monster->hit_dice, you.experience_level); #endif if (random2(you.piety) >= piety_breakpoint(0) && random2(you.experience_level) >= random2(monster->hit_dice) // Bias beaten-up-conversion towards the stronger orcs. && random2(monster->hit_dice) > 2) { beogh_convert_orc(monster, true, MON_KILL(killer)); return (true); } } } return (false); } static bool _monster_avoided_death(monsters *monster, killer_type killer, int i) { if (monster->hit_points < -25 || monster->hit_points < -monster->max_hit_points || monster->max_hit_points <= 0 || monster->hit_dice < 1) { return (false); } // Elyvilon specials. if (_ely_protect_ally(monster)) return (true); if (_ely_heal_monster(monster, killer, i)) return (true); // Yredelemnul special. if (_yred_enslave_soul(monster, killer)) return (true); // Beogh special. if (_beogh_forcibly_convert_orc(monster, killer, i)) return (true); return (false); } static bool _slime_pit_unlock(bool silent) { unset_level_flags(LFLAG_NO_TELE_CONTROL, silent); bool in_los = false; if (!silent) { for (int x = 0; x < GXM && !in_los; ++x) { for (int y = 0; y < GYM; ++y) { if ((grd[x][y] == DNGN_STONE_WALL || grd[x][y] == DNGN_CLEAR_ROCK_WALL) && see_grid(x, y)) { in_los = true; break; } } } } replace_area_wrapper(DNGN_STONE_WALL, DNGN_CLEAR_ROCK_WALL); // In case it was already vitrified, but then it's less noticeable. replace_area_wrapper(DNGN_CLEAR_STONE_WALL, DNGN_CLEAR_ROCK_WALL); if (!silent) { if (in_los) { mpr("Suddenly, all colour oozes out of the stone walls.", MSGCH_MONSTER_ENCHANT); } else { mpr("You feel a strange vibration for a moment.", MSGCH_MONSTER_ENCHANT); } } return (true); } static bool _slime_pit_unlock_offlevel() { return _slime_pit_unlock(true); } static bool _slime_pit_unlock_onlevel() { return _slime_pit_unlock(false); } static void _fire_monster_death_event(monsters *monster, killer_type killer, int i, bool polymorph) { int type = monster->type; // Treat whatever the Royal Jelly polymorphed into as if it were still // the Royal Jelly (but if a player chooses the character name // "shaped Royal Jelly" don't unlock the vaults when the player's // ghost is killed). if (monster->mname == "shaped Royal Jelly" && monster->type != MONS_PLAYER_GHOST) { type = MONS_ROYAL_JELLY; } // Banished monsters aren't technically dead, so no death event // for them. if (killer == KILL_RESET) { // Give player a hint that banishing the Royal Jelly means the // Slime:6 vaults stay locked. if (type == MONS_ROYAL_JELLY) { if (you.can_see(monster)) mpr("You feel a great sense of loss."); else mpr("You feel a great sense of loss, and the brush of the " "Abyss."); } return; } dungeon_events.fire_event( dgn_event(DET_MONSTER_DIED, monster->pos(), 0, monster_index(monster), killer)); // Don't unlock the Slime:6 vaults if the "death" was actually the // Royal Jelly polymorphing into something else; the player still // has to kill whatever it polymorphed into. if (type == MONS_ROYAL_JELLY && !polymorph) { const level_id target(BRANCH_SLIME_PITS, 6); if (is_existing_level(target)) { apply_to_level( target, true, target == level_id::current() ? _slime_pit_unlock_onlevel : _slime_pit_unlock_offlevel); } } } static void _mummy_curse(monsters* monster, killer_type killer, int index) { int pow; switch (killer) { // Mummy killed by trap or something other than the player or // another monster, so no curse. case KILL_MISC: // Mummy sent to the Abyss wasn't actually killed, so no curse. case KILL_RESET: case KILL_DISMISSED: return; default: break; } switch (monster->type) { case MONS_MUMMY: pow = 1; break; case MONS_GUARDIAN_MUMMY: pow = 3; break; case MONS_MUMMY_PRIEST: pow = 8; break; case MONS_GREATER_MUMMY: pow = 11; break; default: mpr("Unknown mummy type.", MSGCH_DIAGNOSTICS); return; } // beam code might give an index of MHITYOU for the player. if (YOU_KILL(killer)) index = NON_MONSTER; // Killed by a Zot trap, a god, etc. if (index != NON_MONSTER && invalid_monster_index(index)) return; actor* target; if (index == NON_MONSTER) target = &you; else { // Mummies committing suicide don't cause a death curse. if (index == monster->mindex()) return; target = &menv[index]; } // Mummy was killed by a giant spore or ball lightning? if (!target->alive()) return; if (monster->type == MONS_MUMMY && YOU_KILL(killer)) curse_an_item(true); else { if (index == NON_MONSTER) { mpr("You feel extremely nervous for a moment...", MSGCH_MONSTER_SPELL); } else if (you.can_see(target)) { mprf(MSGCH_MONSTER_SPELL, "A malignant aura surrounds %s.", target->name(DESC_NOCAP_THE).c_str()); } MiscastEffect(target, monster_index(monster), SPTYP_NECROMANCY, pow, random2avg(88, 3), "a mummy death curse"); } } static bool _spore_goes_pop(monsters *monster, killer_type killer, int killer_index, bool pet_kill, bool wizard) { if (monster->hit_points > 0 || monster->hit_points <= -15 || wizard || killer == KILL_RESET || killer == KILL_DISMISSED) { return (false); } if (killer == KILL_MISC) killer = KILL_MON; bolt beam; const int type = monster->type; beam.is_tracer = false; beam.is_explosion = true; beam.beam_source = monster_index(monster); beam.type = dchar_glyph(DCHAR_FIRED_BURST); beam.source = monster->pos(); beam.target = monster->pos(); beam.thrower = killer; beam.aux_source.clear(); if (YOU_KILL(killer)) beam.aux_source = "set off by themselves"; else if (pet_kill) beam.aux_source = "set off by their pet"; const char* msg = NULL; const char* sanct_msg = NULL; if (type == MONS_GIANT_SPORE) { beam.flavour = BEAM_SPORE; beam.damage = dice_def(3, 15); beam.name = "explosion of spores"; beam.colour = LIGHTGREY; beam.ex_size = 2; msg = "The giant spore explodes!"; sanct_msg = "By Zin's power, the giant spore's explosion is " "contained."; } else if (type == MONS_BALL_LIGHTNING) { beam.flavour = BEAM_ELECTRICITY; beam.damage = dice_def(3, 20); beam.name = "blast of lightning"; beam.colour = LIGHTCYAN; beam.ex_size = coinflip() ? 3 : 2; msg = "The ball lightning explodes!"; sanct_msg = "By Zin's power, the ball lightning's explosion " "is contained."; } else { msg::streams(MSGCH_DIAGNOSTICS) << "Unknown spore type: " << static_cast(type) << std::endl; return (false); } bool saw = false; if (you.can_see(monster)) { saw = true; viewwindow(true, false); if (is_sanctuary(monster->pos())) mpr(sanct_msg, MSGCH_GOD); else mprf(MSGCH_MONSTER_DAMAGE, MDAM_DEAD, msg); } if (is_sanctuary(monster->pos())) return (false); // Detach monster from the grid first, so it doesn't get hit by // its own explosion. (GDL) mgrd(monster->pos()) = NON_MONSTER; // The explosion might cause a monster to be placed where the spore // used to be, so make sure that mgrd() doesn't get cleared a second // time (causing the new monster to become floating) when // monster->reset() is called. monster->pos().reset(); // Exploding kills the monster a bit earlier than normal. monster->hit_points = -16; if (saw) viewwindow(true, false); // FIXME: show_more == mons_near(monster) beam.explode(); // Monster died in explosion, so don't re-attach it to the grid. return (true); } void _monster_die_cloud(const monsters* monster, bool corpse, bool silent, bool summoned) { // Chaos spawn always leave behind a cloud of chaos. if (monster->type == MONS_CHAOS_SPAWN) { summoned = true; corpse = false; } if (!summoned) return; std::string prefix = " "; if (corpse) { if (mons_weight(mons_species(monster->type)) == 0) return; prefix = "'s corpse "; } std::string msg = summoned_poof_msg(monster); msg += "!"; cloud_type cloud = CLOUD_NONE; if (msg.find("smoke") != std::string::npos) cloud = random_smoke_type(); else if (msg.find("chaos") != std::string::npos) cloud = CLOUD_CHAOS; if (!silent) simple_monster_message(monster, (prefix + msg).c_str()); if (cloud != CLOUD_NONE) { place_cloud(cloud, monster->pos(), 1 + random2(3), monster->kill_alignment()); } } int monster_die(monsters *monster, killer_type killer, int killer_index, bool silent, bool wizard) { if (invalid_monster(monster)) return (-1); // If a monster was banished to the Abyss and then killed there, // then its death wasn't a banishment. if (you.level_type == LEVEL_ABYSS) monster->flags &= ~MF_BANISHED; if (!silent && _monster_avoided_death(monster, killer, killer_index)) return (-1); crawl_state.inc_mon_acting(monster); mons_clear_trapping_net(monster); // Update list of monsters beholding player. update_beholders(monster, true); // Clear auto exclusion now the monster is killed -- if we know about it. if (mons_near(monster) || wizard) remove_auto_exclude(monster); int summon_type = 0; int duration = 0; const bool summoned = mons_is_summoned(monster, &duration, &summon_type); const int monster_killed = monster_index(monster); const bool hard_reset = testbits(monster->flags, MF_HARD_RESET); const bool gives_xp = (!summoned && !mons_class_flag(monster->type, M_NO_EXP_GAIN)); const bool drop_items = !hard_reset; const bool mons_reset(killer == KILL_RESET || killer == KILL_DISMISSED); const bool submerged = monster->submerged(); bool in_transit = false; #ifdef DGL_MILESTONES _check_kill_milestone(monster, killer, killer_index); #endif // Award experience for suicide if the suicide was caused by the // player. if (MON_KILL(killer) && monster_killed == killer_index) { if (monster->confused_by_you()) killer = KILL_YOU_CONF; } else if (MON_KILL(killer) && monster->has_ench(ENCH_CHARM)) killer = KILL_YOU_CONF; // Well, it was confused in a sense... (jpeg) // Take note! if (!mons_reset && !crawl_state.arena && MONST_INTERESTING(monster)) { take_note(Note(NOTE_KILL_MONSTER, monster->type, mons_friendly(monster), monster->full_name(DESC_NOCAP_A).c_str())); } // From time to time Trog gives you a little bonus. if (killer == KILL_YOU && you.duration[DUR_BERSERKER]) { if (you.religion == GOD_TROG && !player_under_penance() && you.piety > random2(1000)) { const int bonus = 3 + random2avg( 10, 2 ); you.duration[DUR_BERSERKER] += bonus; you.duration[DUR_MIGHT] += bonus; haste_player(bonus); mpr("You feel the power of Trog in you as your rage grows.", MSGCH_GOD, GOD_TROG); } else if (wearing_amulet(AMU_RAGE) && one_chance_in(30)) { const int bonus = 2 + random2(4); you.duration[DUR_BERSERKER] += bonus; you.duration[DUR_MIGHT] += bonus; haste_player(bonus); mpr("Your amulet glows a violent red."); } } if (you.prev_targ == monster_killed) { you.prev_targ = MHITNOT; crawl_state.cancel_cmd_repeat(); } if (killer == KILL_YOU) crawl_state.cancel_cmd_repeat(); const bool pet_kill = _is_pet_kill(killer, killer_index); bool did_death_message = false; if (monster->type == MONS_GIANT_SPORE || monster->type == MONS_BALL_LIGHTNING) { did_death_message = _spore_goes_pop(monster, killer, killer_index, pet_kill, wizard); } else if (monster->type == MONS_FIRE_VORTEX || monster->type == MONS_SPATIAL_VORTEX) { if (!silent && !mons_reset) { simple_monster_message(monster, " dissipates!", MSGCH_MONSTER_DAMAGE, MDAM_DEAD); silent = true; } if (monster->type == MONS_FIRE_VORTEX && !wizard && !mons_reset && !submerged) { place_cloud(CLOUD_FIRE, monster->pos(), 2 + random2(4), monster->kill_alignment()); } if (killer == KILL_RESET) killer = KILL_DISMISSED; } else if (monster->type == MONS_SIMULACRUM_SMALL || monster->type == MONS_SIMULACRUM_LARGE) { if (!silent && !mons_reset) { simple_monster_message(monster, " vapourises!", MSGCH_MONSTER_DAMAGE, MDAM_DEAD); silent = true; } if (!wizard && !mons_reset && !submerged) { place_cloud(CLOUD_COLD, monster->pos(), 2 + random2(4), monster->kill_alignment()); } if (killer == KILL_RESET) killer = KILL_DISMISSED; } else if (monster->type == MONS_DANCING_WEAPON) { if (!hard_reset) { if (killer == KILL_RESET) killer = KILL_DISMISSED; } if (!silent && !hard_reset) { int w_idx = monster->inv[MSLOT_WEAPON]; if (w_idx != NON_ITEM && !(mitm[w_idx].flags & ISFLAG_SUMMONED)) { simple_monster_message(monster, " falls from the air.", MSGCH_MONSTER_DAMAGE, MDAM_DEAD); silent = true; } else killer = KILL_RESET; } } const bool death_message = !silent && !did_death_message && mons_near(monster) && (player_monster_visible(monster) || crawl_state.arena); const bool created_friendly = testbits(monster->flags, MF_CREATED_FRIENDLY); bool anon = (killer_index == ANON_FRIENDLY_MONSTER); const mon_holy_type targ_holy = mons_holiness(monster); switch (killer) { case KILL_YOU: // You kill in combat. case KILL_YOU_MISSILE: // You kill by missile or beam. case KILL_YOU_CONF: // You kill by confusion. { const bool bad_kill = god_hates_killing(you.religion, monster); const bool was_neutral = testbits(monster->flags, MF_WAS_NEUTRAL); if (death_message) { if (killer == KILL_YOU_CONF && (anon || !invalid_monster_index(killer_index))) { mprf(MSGCH_MONSTER_DAMAGE, MDAM_DEAD, "%s is %s!", monster->name(DESC_CAP_THE).c_str(), _wounded_damaged(monster->type) ? "destroyed" : "killed"); } else { mprf(MSGCH_MONSTER_DAMAGE, MDAM_DEAD, "You %s %s!", _wounded_damaged(monster->type) ? "destroy" : "kill", monster->name(DESC_NOCAP_THE).c_str()); } if ((created_friendly || was_neutral) && gives_xp) mpr("That felt strangely unrewarding."); } if (crawl_state.arena) break; // Killing triggers tutorial lesson. if (gives_xp) _tutorial_inspect_kill(); // Prevent summoned creatures from being good kills. if (bad_kill || !created_friendly && gives_xp) { if (targ_holy == MH_NATURAL) { did_god_conduct(DID_KILL_LIVING, monster->hit_dice, true, monster); if (mons_is_evil(monster)) { did_god_conduct(DID_KILL_NATURAL_EVIL, monster->hit_dice, true, monster); } } else if (targ_holy == MH_UNDEAD) { did_god_conduct(DID_KILL_UNDEAD, monster->hit_dice, true, monster); } else if (targ_holy == MH_DEMONIC) { did_god_conduct(DID_KILL_DEMON, monster->hit_dice, true, monster); } // Zin hates chaotic beings. if (mons_is_chaotic(monster)) { did_god_conduct(DID_KILL_CHAOTIC, monster->hit_dice, true, monster); } // jmf: Trog hates wizards. if (mons_is_magic_user(monster)) { did_god_conduct(DID_KILL_WIZARD, monster->hit_dice, true, monster); } // Beogh hates priests of other gods. if (mons_class_flag(monster->type, M_PRIEST)) { did_god_conduct(DID_KILL_PRIEST, monster->hit_dice, true, monster); } // Holy kills are always noticed. if (targ_holy == MH_HOLY) { did_god_conduct(DID_KILL_HOLY, monster->hit_dice, true, monster); } } // Divine health and mana restoration doesn't happen when // killing born-friendly monsters. The mutation still // applies, however. if (player_mutation_level(MUT_DEATH_STRENGTH) || (!created_friendly && gives_xp && (you.religion == GOD_MAKHLEB || you.religion == GOD_SHINING_ONE && mons_is_evil_or_unholy(monster)) && !player_under_penance() && random2(you.piety) >= piety_breakpoint(0))) { if (you.hp < you.hp_max) { mpr("You feel a little better."); inc_hp(monster->hit_dice + random2(monster->hit_dice), false); } } if (!created_friendly && gives_xp && (you.religion == GOD_MAKHLEB || you.religion == GOD_VEHUMET || you.religion == GOD_SHINING_ONE && mons_is_evil_or_unholy(monster)) && !player_under_penance() && random2(you.piety) >= piety_breakpoint(0)) { if (you.magic_points < you.max_magic_points) { mpr("You feel your power returning."); inc_mp(1 + random2(monster->hit_dice / 2), false); } } // Randomly bless a follower. if (!created_friendly && gives_xp && (you.religion == GOD_BEOGH && random2(you.piety) >= piety_breakpoint(2)) && !player_under_penance()) { bless_follower(); } if (you.duration[DUR_DEATH_CHANNEL] && mons_holiness(monster) == MH_NATURAL && mons_can_be_zombified(monster) && gives_xp) { const monster_type spectre_type = mons_species(monster->type); // Don't allow 0-headed hydras to become spectral hydras. if (spectre_type != MONS_HYDRA || monster->number != 0) { const int spectre = create_monster( mgen_data(MONS_SPECTRAL_THING, BEH_FRIENDLY, 0, 0, monster->pos(), MHITYOU, 0, static_cast(you.attribute[ATTR_DIVINE_DEATH_CHANNEL]), spectre_type, monster->number)); if (spectre != -1) { if (death_message) mpr("A glowing mist starts to gather..."); name_zombie(&menv[spectre], monster); } } } break; } case KILL_MON: // Monster kills in combat. case KILL_MON_MISSILE: // Monster kills by missile or beam. if (!silent) { simple_monster_message(monster, _wounded_damaged(monster->type) ? " is destroyed!" : " dies!", MSGCH_MONSTER_DAMAGE, MDAM_DEAD); } if (crawl_state.arena) break; // No piety loss if god gifts killed by other monsters. // Also, dancing weapons aren't really friendlies. if (mons_friendly(monster) && !mons_is_god_gift(monster) && monster->type != MONS_DANCING_WEAPON) { did_god_conduct(DID_FRIEND_DIED, 1 + (monster->hit_dice / 2), true, monster); } // Trying to prevent summoning abuse here, so we're trying to // prevent summoned creatures from being done_good kills. Only // affects creatures which were friendly when summoned. if (!created_friendly && gives_xp && pet_kill && (anon || !invalid_monster_index(killer_index))) { bool notice = false; monsters *killer_mon = NULL; if (!anon) { killer_mon = &menv[killer_index]; // If the killer is already dead treat it like an // anonymous monster. if (killer_mon->type == -1) anon = true; } const mon_holy_type killer_holy = anon ? MH_NATURAL : mons_holiness(killer_mon); if (you.religion == GOD_SHINING_ONE || you.religion == GOD_YREDELEMNUL || you.religion == GOD_KIKUBAAQUDGHA || you.religion == GOD_VEHUMET || you.religion == GOD_MAKHLEB || you.religion == GOD_LUGONU || !anon && mons_is_god_gift(killer_mon)) { if (killer_holy == MH_UNDEAD) { const bool confused = anon ? false : !mons_friendly(killer_mon); // Yes, these are hacks, but they make sure that // confused monsters doing kills are not // referred to as "slaves", and I think it's // okay that e.g. Yredelemnul ignores kills done // by confused monsters as opposed to enslaved // or friendly ones. (jpeg) if (targ_holy == MH_NATURAL) { notice |= did_god_conduct( !confused ? DID_LIVING_KILLED_BY_UNDEAD_SLAVE : DID_LIVING_KILLED_BY_SERVANT, monster->hit_dice); } else if (targ_holy == MH_UNDEAD) { notice |= did_god_conduct( !confused ? DID_UNDEAD_KILLED_BY_UNDEAD_SLAVE : DID_UNDEAD_KILLED_BY_SERVANT, monster->hit_dice); } else if (targ_holy == MH_DEMONIC) { notice |= did_god_conduct( !confused ? DID_DEMON_KILLED_BY_UNDEAD_SLAVE : DID_DEMON_KILLED_BY_SERVANT, monster->hit_dice); } } // Yes, we are splitting undead pets from the others // as a way to focus Necromancy vs. Summoning // (ignoring Summon Wraith here)... at least we're // being nice and putting the natural creature // summons together with the demonic ones. Note // that Vehumet gets a free pass here since those // followers are assumed to come from summoning // spells... the others are from invocations (TSO, // Makhleb, Kiku). - bwr else if (targ_holy == MH_NATURAL) { notice |= did_god_conduct(DID_LIVING_KILLED_BY_SERVANT, monster->hit_dice); if (mons_is_evil(monster)) { notice |= did_god_conduct( DID_NATURAL_EVIL_KILLED_BY_SERVANT, monster->hit_dice); } } else if (targ_holy == MH_UNDEAD) { notice |= did_god_conduct(DID_UNDEAD_KILLED_BY_SERVANT, monster->hit_dice); } else if (targ_holy == MH_DEMONIC) { notice |= did_god_conduct(DID_DEMON_KILLED_BY_SERVANT, monster->hit_dice); } } // Holy kills are always noticed. if (targ_holy == MH_HOLY) { if (killer_holy == MH_UNDEAD) { const bool confused = anon ? false : !mons_friendly(killer_mon); // Yes, this is a hack, but it makes sure that // confused monsters doing kills are not // referred to as "slaves", and I think it's // okay that Yredelemnul ignores kills done by // confused monsters as opposed to enslaved or // friendly ones. (jpeg) notice |= did_god_conduct( !confused ? DID_HOLY_KILLED_BY_UNDEAD_SLAVE : DID_HOLY_KILLED_BY_SERVANT, monster->hit_dice); } else notice |= did_god_conduct(DID_HOLY_KILLED_BY_SERVANT, monster->hit_dice); } if (you.religion == GOD_VEHUMET && notice && !player_under_penance() && random2(you.piety) >= piety_breakpoint(0)) { // Vehumet - only for non-undead servants (coding // convenience, no real reason except that Vehumet // prefers demons). if (you.magic_points < you.max_magic_points) { mpr("You feel your power returning."); inc_mp(1 + random2(monster->hit_dice / 2), false); } } if (you.religion == GOD_SHINING_ONE && mons_is_evil_or_unholy(monster) && !player_under_penance() && random2(you.piety) >= piety_breakpoint(0) && !invalid_monster_index(killer_index)) { // Randomly bless the follower who killed. if (!one_chance_in(3) && killer_mon->alive() && bless_follower(killer_mon)) { break; } if (killer_mon->alive() && killer_mon->hit_points < killer_mon->max_hit_points) { simple_monster_message(killer_mon, " looks invigorated."); heal_monster(killer_mon, 1 + random2(monster->hit_dice / 4), false); } } // Randomly bless the follower who killed. if (you.religion == GOD_BEOGH && random2(you.piety) >= piety_breakpoint(2) && !player_under_penance() && !one_chance_in(3) && !invalid_monster_index(killer_index)) { bless_follower(killer_mon); } } break; // Monster killed by trap/inanimate thing/itself/poison not from you. case KILL_MISC: if (!silent) { simple_monster_message(monster, _wounded_damaged(monster->type) ? " is destroyed!" : " dies!", MSGCH_MONSTER_DAMAGE, MDAM_DEAD); } break; case KILL_RESET: // Monster doesn't die, just goes back to wherever it came from. // This must only be called by monsters running out of time (or // abjuration), because it uses the beam variables! Or does it??? // Pacified monsters leave the level when this happens. // KILL_RESET monsters no longer lose their whole inventory, only // items they were generated with. if (mons_is_pacified(monster) || !monster->needs_transit()) { // A banished monster that doesn't go on the transit list // loses all items. if (!mons_is_summoned(monster)) monster->destroy_inventory(); break; } // Monster goes to the Abyss. monster->flags |= MF_BANISHED; monster->set_transit(level_id(LEVEL_ABYSS)); in_transit = true; monster->destroy_inventory(); // Make monster stop patrolling and/or travelling. monster->patrol_point.reset(); monster->travel_path.clear(); monster->travel_target = MTRAV_NONE; break; case KILL_DISMISSED: break; default: monster->destroy_inventory(); break; } // Make sure Boris has a foe to address. if (monster->foe == MHITNOT) { if (!mons_wont_attack(monster) && !crawl_state.arena) monster->foe = MHITYOU; else if (!invalid_monster_index(killer_index)) monster->foe = killer_index; } if (!silent && !wizard && see_grid(monster->pos())) { // Make sure that the monster looks dead. if (monster->alive() && !in_transit && (!summoned || duration > 0)) monster->hit_points = -1; mons_speaks(monster); } if (monster->type == MONS_BORIS && !in_transit) { // XXX: Actual blood curse effect for Boris? -- bwr // Now that Boris is dead, he's a valid target for monster // creation again. -- bwr you.unique_creatures[monster->type] = false; } else if (!mons_is_summoned(monster)) { if (mons_genus(monster->type) == MONS_MUMMY) _mummy_curse(monster, killer, killer_index); } if (!wizard && !submerged) _monster_die_cloud(monster, !mons_reset, silent, summoned); int corpse = -1; if (!mons_reset) { // Have to add case for disintegration effect here? {dlb} if (!summoned) corpse = place_monster_corpse(monster, silent); } if (!mons_reset && !crawl_state.arena) { you.kills->record_kill(monster, killer, pet_kill); kill_category kc = (killer == KILL_YOU || killer == KILL_YOU_MISSILE) ? KC_YOU : (pet_kill)? KC_FRIENDLY : KC_OTHER; unsigned int exp_gain = 0, avail_gain = 0; _give_adjusted_experience(monster, killer, pet_kill, killer_index, &exp_gain, &avail_gain); PlaceInfo& curr_PlaceInfo = you.get_place_info(); PlaceInfo delta; delta.mon_kill_num[kc]++; delta.mon_kill_exp += exp_gain; delta.mon_kill_exp_avail += avail_gain; you.global_info += delta; you.global_info.assert_validity(); curr_PlaceInfo += delta; curr_PlaceInfo.assert_validity(); } _fire_monster_death_event(monster, killer, killer_index, false); if (crawl_state.arena) arena_monster_died(monster, killer, killer_index, silent, corpse); const coord_def mwhere = monster->pos(); if (drop_items) monster_drop_ething(monster, YOU_KILL(killer) || pet_kill); else { // Destroy the items belonging to MF_HARD_RESET monsters so they // don't clutter up mitm[]. monster->destroy_inventory(); } if (!silent && !wizard && !mons_reset && corpse != -1 && !(monster->flags & MF_KNOWN_MIMIC) && mons_is_shapeshifter(monster)) { simple_monster_message(monster, "'s shape twists and changes " "as it dies."); } // If we kill an invisible monster reactivate autopickup. if (mons_near(monster) && !player_monster_visible(monster)) autotoggle_autopickup(false); crawl_state.dec_mon_acting(monster); monster_cleanup(monster); // Force redraw for monsters that die. if (see_grid(mwhere)) { view_update_at(mwhere); update_screen(); } return (corpse); } void monster_cleanup(monsters *monster) { crawl_state.mon_gone(monster); unsigned int monster_killed = monster_index(monster); monster->reset(); for (int dmi = 0; dmi < MAX_MONSTERS; dmi++) if (menv[dmi].foe == monster_killed) menv[dmi].foe = MHITNOT; if (you.pet_target == monster_killed) you.pet_target = MHITNOT; } 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); } // If you're invis and throw/zap whatever, alerts menv to your position. void alert_nearby_monsters(void) { monsters *monster = 0; // NULL {dlb} for (int it = 0; it < MAX_MONSTERS; it++) { monster = &menv[it]; // Judging from the above comment, this function isn't // intended to wake up monsters, so we're only going to // alert monsters that aren't sleeping. For cases where an // event should wake up monsters and alert them, I'd suggest // calling noisy() before calling this function. -- bwr if (monster->alive() && mons_near(monster) && !mons_is_sleeping(monster)) { behaviour_event(monster, ME_ALERT, MHITYOU); } } } static bool _valid_morph( monsters *monster, int new_mclass ) { const dungeon_feature_type current_tile = grd(monster->pos()); // 'morph targets are _always_ "base" classes, not derived ones. new_mclass = mons_species(new_mclass); // [ds] Non-base draconians are much more trouble than their HD // suggests. if (mons_genus(new_mclass) == MONS_DRACONIAN && new_mclass != MONS_DRACONIAN && !player_in_branch(BRANCH_HALL_OF_ZOT) && !one_chance_in(10)) { return (false); } // Various inappropriate polymorph targets. if (mons_class_holiness( new_mclass ) != mons_holiness( monster ) || mons_class_flag( new_mclass, M_UNIQUE) // no uniques || mons_class_flag( new_mclass, M_NO_EXP_GAIN ) // not helpless || new_mclass == mons_species( monster->type ) // must be different || new_mclass == MONS_PROGRAM_BUG || new_mclass == MONS_SHAPESHIFTER || new_mclass == MONS_GLOWING_SHAPESHIFTER || new_mclass == MONS_MERFOLK // inhabitants of (deactivated) Shoals || new_mclass == MONS_MERMAID || new_mclass == MONS_SIREN || new_mclass == MONS_HARPY || new_mclass == MONS_SHARK // These require manual setting of mons.base_monster to indicate // what they are a skeleton/zombie/simulacrum/spectral thing of, // which we currently aren't smart enough to handle. || mons_class_is_zombified(new_mclass) // These shouldn't happen anyways (demons unaffected + holiness check), // but if we ever do have polydemon, these will be needed: || new_mclass == MONS_PLAYER_GHOST || new_mclass == MONS_PANDEMONIUM_DEMON // Only for use by game testers or in the arena. || new_mclass == MONS_TEST_SPAWNER // Other poly-unsuitable things. || new_mclass == MONS_ORB_GUARDIAN || mons_is_statue(new_mclass)) { return (false); } // Determine if the monster is happy on current tile. return (monster_habitable_grid(new_mclass, current_tile)); } static bool _is_poly_power_unsuitable( poly_power_type power, int src_pow, int tgt_pow, int relax ) { switch (power) { case PPT_LESS: return (tgt_pow > src_pow - 3 + (relax * 3) / 2) || (power == PPT_LESS && (tgt_pow < src_pow - (relax / 2))); case PPT_MORE: return (tgt_pow < src_pow + 2 - relax) || (power == PPT_MORE && (tgt_pow > src_pow + relax)); default: case PPT_SAME: return (tgt_pow < src_pow - relax) || (tgt_pow > src_pow + (relax * 3) / 2); } } // If targetc == RANDOM_MONSTER, then relpower indicates the desired // power of the new monster, relative to the current monster. // Relaxation still takes effect when needed, no matter what relpower // says. bool monster_polymorph(monsters *monster, monster_type targetc, poly_power_type power, bool force_beh) { ASSERT(!(monster->flags & MF_TAKING_STAIRS)); ASSERT(!(monster->flags & MF_BANISHED) || you.level_type == LEVEL_ABYSS); std::string str_polymon; int source_power, target_power, relax; int tries = 1000; // Used to be mons_power, but that just returns hit_dice // for the monster class. By using the current hit dice // the player gets the opportunity to use draining more // effectively against shapeshifters. -- bwr source_power = monster->hit_dice; relax = 1; if (targetc == RANDOM_MONSTER) { do { // Pick a monster that's guaranteed happy at this grid. targetc = random_monster_at_grid(monster->pos()); // Valid targets are always base classes ([ds] which is unfortunate // in that well-populated monster classes will dominate polymorphs). targetc = mons_species(targetc); target_power = mons_power(targetc); if (one_chance_in(200)) relax++; if (relax > 50) return (simple_monster_message(monster, " shudders.")); } while (tries-- && (!_valid_morph(monster, targetc) || _is_poly_power_unsuitable(power, source_power, target_power, relax))); } if (!_valid_morph(monster, targetc)) { return (simple_monster_message(monster, " looks momentarily different.")); } // Messaging. bool can_see = you.can_see(monster); bool can_see_new = !mons_class_flag(targetc, M_INVIS) || player_see_invis(); bool need_note = false; std::string old_name = monster->full_name(DESC_CAP_A); // If old monster is visible to the player, and is interesting, // then note why the interesting monster went away. if (can_see && MONST_INTERESTING(monster)) need_note = true; std::string new_name = ""; if (monster->type == MONS_OGRE && targetc == MONS_TWO_HEADED_OGRE) str_polymon = " grows a second head"; else { if (mons_is_shapeshifter(monster)) str_polymon = " changes into "; else if (targetc == MONS_PULSATING_LUMP) str_polymon = " degenerates into "; else str_polymon = " evaporates and reforms as "; if (!can_see_new) { new_name = "something unseen"; str_polymon += "something you cannot see"; } else { str_polymon += mons_type_name(targetc, DESC_NOCAP_A); if (targetc == MONS_PULSATING_LUMP) str_polymon += " of flesh"; } } str_polymon += "!"; bool player_messaged = can_see && simple_monster_message(monster, str_polymon.c_str()); // Even if the monster transforms from one type that can behold the // player into a different type which can also behold the player, // the polymorph disrupts the beholding process. Do this before // changing monster->type, since unbeholding can only happen while // the monster is still a mermaid/siren. update_beholders(monster, true); // Inform listeners that the original monster is gone. _fire_monster_death_event(monster, KILL_MISC, NON_MONSTER, true); // the actual polymorphing: unsigned long flags = monster->flags & ~(MF_INTERESTING | MF_SEEN | MF_ATT_CHANGE_ATTEMPT | MF_WAS_IN_VIEW | MF_BAND_MEMBER | MF_HONORARY_UNDEAD | MF_KNOWN_MIMIC); std::string name; // Preserve the names of uniques and named monsters. if (!monster->mname.empty()) name = monster->mname; else if (mons_is_unique(monster->type)) { flags |= MF_INTERESTING; name = monster->name(DESC_PLAIN, true); if (monster->type == MONS_ROYAL_JELLY) { name = "shaped Royal Jelly"; flags |= MF_NAME_SUFFIX; } else if (monster->type == MONS_LERNAEAN_HYDRA) { name = "shaped Lernaean hydra"; flags |= MF_NAME_SUFFIX; } // "Blork the orc" and similar. const size_t the_pos = name.find(" the "); if (the_pos != std::string::npos) name = name.substr(0, the_pos); } const monster_type real_targetc = (monster->has_ench(ENCH_GLOWING_SHAPESHIFTER)) ? MONS_GLOWING_SHAPESHIFTER : (monster->has_ench(ENCH_SHAPESHIFTER)) ? MONS_SHAPESHIFTER : targetc; const god_type god = (player_will_anger_monster(real_targetc) || (you.religion == GOD_BEOGH && mons_species(real_targetc) != MONS_ORC)) ? GOD_NO_GOD : monster->god; if (god == GOD_NO_GOD) flags &= ~MF_GOD_GIFT; const int old_hp = monster->hit_points; const int old_hp_max = monster->max_hit_points; const bool old_mon_caught = mons_is_caught(monster); const char old_ench_countdown = monster->ench_countdown; mon_enchant abj = monster->get_ench(ENCH_ABJ); mon_enchant charm = monster->get_ench(ENCH_CHARM); mon_enchant neutral = monster->get_ench(ENCH_NEUTRAL); mon_enchant shifter = monster->get_ench(ENCH_GLOWING_SHAPESHIFTER, ENCH_SHAPESHIFTER); mon_enchant sub = monster->get_ench(ENCH_SUBMERGED); mon_enchant summon = monster->get_ench(ENCH_SUMMON); mon_enchant tp = monster->get_ench(ENCH_TP); monster_spells spl = monster->spells; const bool need_save_spells = (!name.empty() && (!mons_class_flag(monster->type, M_SPELLCASTER) || mons_class_flag(monster->type, M_ACTUAL_SPELLS))); // deal with mons_sec monster->type = targetc; monster->base_monster = MONS_PROGRAM_BUG; monster->number = 0; // Note: define_monster() will clear out all enchantments! - bwr define_monster(monster_index(monster)); monster->mname = name; monster->flags = flags; monster->god = god; // Keep spells for named monsters, but don't override innate ones // for dragons and the like. This means that Sigmund polymorphed // into a goblin will still cast spells, but if he ends up as a // swamp drake he'll breathe fumes and, if polymorphed further, // won't remember his spells anymore. if (need_save_spells && (!mons_class_flag(monster->type, M_SPELLCASTER) || mons_class_flag(monster->type, M_ACTUAL_SPELLS))) { monster->spells = spl; } monster->add_ench(abj); monster->add_ench(charm); monster->add_ench(neutral); monster->add_ench(shifter); monster->add_ench(sub); monster->add_ench(summon); monster->add_ench(tp); // Allows for handling of submerged monsters which polymorph into // monsters that can't submerge on this square. if (monster->has_ench(ENCH_SUBMERGED) && !monster_can_submerge(monster, grd(monster->pos()))) { monster->del_ench(ENCH_SUBMERGED); } monster->ench_countdown = old_ench_countdown; if (mons_class_flag(monster->type, M_INVIS)) monster->add_ench(ENCH_INVIS); if (!player_messaged && you.can_see(monster)) { mprf("%s appears out of thin air!", monster->name(DESC_CAP_A).c_str()); autotoggle_autopickup(false); player_messaged = true; } monster->hit_points = monster->max_hit_points * ((old_hp * 100) / old_hp_max) / 100 + random2(monster->max_hit_points); monster->hit_points = std::min(monster->max_hit_points, monster->hit_points); // Don't kill it. monster->hit_points = std::max(monster->hit_points, 1); monster->speed_increment = 67 + random2(6); monster_drop_ething(monster); // New monster type might be interesting. mark_interesting_monst(monster); if (new_name.empty()) new_name = monster->full_name(DESC_NOCAP_A); if (need_note || can_see && you.can_see(monster) && MONST_INTERESTING(monster)) { take_note(Note(NOTE_POLY_MONSTER, 0, 0, old_name.c_str(), new_name.c_str())); if (you.can_see(monster)) monster->flags |= MF_SEEN; } // If new monster is visible to player, then we've seen it. if (you.can_see(monster)) { seen_monster(monster); // If the player saw both the beginning and end results of a // shifter changing, then s/he knows it must be a shifter. if (can_see && shifter.ench != ENCH_NONE) monster->flags |= MF_KNOWN_MIMIC; } if (old_mon_caught) check_net_will_hold_monster(monster); if (!force_beh) player_angers_monster(monster); // Xom likes watching monsters being polymorphed. xom_is_stimulated(mons_is_shapeshifter(monster) ? 16 : power == PPT_LESS || mons_friendly(monster) ? 32 : power == PPT_SAME ? 64 : 128); return (player_messaged); } // If the returned value is mon.pos(), then nothing was found. static coord_def _random_monster_nearby_habitable_space(const monsters& mon, bool allow_adjacent, bool respect_los) { const bool respect_sanctuary = mons_wont_attack(&mon); coord_def target; int tries; for (tries = 0; tries < 150; ++tries) { const coord_def delta(random2(13) - 6, random2(13) - 6); // Check that we don't get something too close to the // starting point. if (delta.origin()) continue; if (delta.rdist() == 1 && !allow_adjacent) continue; // Update target. target = delta + mon.pos(); // Check that the target is valid and survivable. if (!in_bounds(target)) continue; if (!monster_habitable_grid(&mon, grd(target))) continue; if (respect_sanctuary && is_sanctuary(target)) continue; if (target == you.pos()) continue; // Check that we didn't go through clear walls. if (num_feats_between(mon.pos(), target, DNGN_CLEAR_ROCK_WALL, DNGN_CLEAR_PERMAROCK_WALL, true, true) > 0) { continue; } if (respect_los && !mon.mon_see_grid(target)) continue; // Survived everything, break out (with a good value of target.) break; } if (tries == 150) target = mon.pos(); return (target); } bool monster_blink(monsters *monster, bool quiet) { coord_def near = _random_monster_nearby_habitable_space(*monster, false, true); if (near == monster->pos()) return (false); if (!quiet) simple_monster_message(monster, " blinks!"); if (!(monster->flags & MF_WAS_IN_VIEW)) monster->seen_context = "thin air"; const coord_def oldplace = monster->pos(); if (!monster->move_to_pos(near)) return (false); monster->check_redraw(oldplace); monster->apply_location_effects(oldplace); return (true); } static void _set_random_target(monsters* mon) { mon->target = random_in_bounds(); // If we don't find anything better for (int tries = 0; tries < 150; ++tries) { coord_def delta = coord_def(random2(13), random2(13)) - coord_def(6,6); if (delta.origin()) continue; const coord_def newtarget = delta + mon->pos(); if (!in_bounds(newtarget)) continue; mon->target = newtarget; break; } } // allow_adjacent: allow target to be adjacent to origin. // restrict_LOS: restrict target to be within PLAYER line of sight. bool random_near_space(const coord_def& origin, coord_def& target, bool allow_adjacent, bool restrict_LOS, bool forbid_dangerous, bool forbid_sanctuary) { // This might involve ray tracing (via num_feats_between()), so // cache results to avoid duplicating ray traces. FixedArray tried; tried.init(false); // Is the monster on the other side of a transparent wall? const bool trans_wall_block = trans_wall_blocking(origin); const bool origin_is_player = (you.pos() == origin); int min_walls_between = 0; // Skip ray tracing if possible. if (trans_wall_block) { min_walls_between = num_feats_between(origin, you.pos(), DNGN_CLEAR_ROCK_WALL, DNGN_CLEAR_PERMAROCK_WALL); } int tries = 0; while (tries++ < 150) { coord_def delta(random2(13), random2(13)); target = origin - coord_def(6, 6) + delta; // Origin is not 'near'. if (target == origin) continue; if (tried(delta)) continue; tried(delta) = true; if (!in_bounds(target) || restrict_LOS && !see_grid(target) || grd(target) < DNGN_SHALLOW_WATER || actor_at(target) || !allow_adjacent && distance(origin, target) <= 2 || forbid_sanctuary && is_sanctuary(target)) { continue; } // Don't pick grids that contain a dangerous cloud. if (forbid_dangerous) { const int cloud = env.cgrid(target); if (cloud != EMPTY_CLOUD && is_damaging_cloud(env.cloud[cloud].type, true)) { continue; } } if (!trans_wall_block && !origin_is_player) return (true); // If the monster is on a visible square which is on the other // side of one or more translucent walls from the player, then it // can only blink through translucent walls if the end point // is either not visible to the player, or there are at least // as many translucent walls between the player and the end // point as between the player and the start point. However, // monsters can still blink through translucent walls to get // away from the player, since in the absence of translucent // walls monsters can blink to places which are not in either // the monster's nor the player's LOS. if (!origin_is_player && !see_grid(target)) return (true); // Player can't randomly pass through translucent walls. if (origin_is_player) { if (see_grid_no_trans(target)) return (true); continue; } int walls_passed = num_feats_between(target, origin, DNGN_CLEAR_ROCK_WALL, DNGN_CLEAR_PERMAROCK_WALL, true, true); if (walls_passed == 0) return (true); // Player can't randomly pass through translucent walls. if (origin_is_player) continue; int walls_between = num_feats_between(target, you.pos(), DNGN_CLEAR_ROCK_WALL, DNGN_CLEAR_PERMAROCK_WALL); if (walls_between >= min_walls_between) return (true); } return (false); } static bool _habitat_okay( const monsters *monster, dungeon_feature_type targ ) { return (monster_habitable_grid(monster, targ)); } // This doesn't really swap places, it just sets the monster's // position equal to the player (the player has to be moved afterwards). // It also has a slight problem with the fact that if the player is // levitating over an inhospitable habitat for the monster the monster // will be put in a place it normally couldn't go (this could be a // feature because it prevents insta-killing). In order to prevent // that little problem, we go looking for a square for the monster // to "scatter" to instead... and if we can't find one the monster // just refuses to be swapped (not a bug, this is intentionally // avoiding the insta-kill). Another option is to look a bit // wider for a vaild square (either by a last attempt blink, or // by looking at a wider radius)... insta-killing should be a // last resort in this function (especially since Tome, Dig, and // Summoning can be used to set up death traps). If worse comes // to worse, at least consider making the Swap spell not work // when the player is over lava or water (if the player wants to // swap pets to their death, we can let that go). -- bwr bool swap_places(monsters *monster) { coord_def loc; if (swap_check(monster, loc)) { swap_places(monster, loc); return true; } return false; } // Swap monster to this location. Player is swapped elsewhere. bool swap_places(monsters *monster, const coord_def &loc) { ASSERT(map_bounds(loc)); ASSERT(_habitat_okay(monster, grd(loc))); if (mgrd(loc) != NON_MONSTER) { mpr("Something prevents you from swapping places."); return (false); } mpr("You swap places."); mgrd(monster->pos()) = NON_MONSTER; monster->moveto(loc); mgrd(monster->pos()) = monster_index(monster); return true; } // Returns true if this is a valid swap for this monster. If true, then // the valid location is set in loc. (Otherwise loc becomes garbage.) bool swap_check(monsters *monster, coord_def &loc, bool quiet) { loc = you.pos(); // Don't move onto dangerous terrain. if (is_grid_dangerous(grd(monster->pos()))) { canned_msg(MSG_UNTHINKING_ACT); return (false); } if (mons_is_caught(monster)) { if (!quiet) simple_monster_message(monster, " is held in a net!"); return (false); } // First try: move monster onto your position. bool swap = _habitat_okay( monster, grd(loc) ); // Choose an appropriate habitat square at random around the target. if (!swap) { int num_found = 0; for (adjacent_iterator ai; ai; ++ai) if (mgrd(*ai) == NON_MONSTER && _habitat_okay( monster, grd(*ai)) && one_chance_in(++num_found)) { loc = *ai; } if (num_found) swap = true; } if (!swap && !quiet) { // Might not be ideal, but it's better than insta-killing // the monster... maybe try for a short blink instead? -- bwr simple_monster_message( monster, " resists." ); // FIXME: AI_HIT_MONSTER isn't ideal. interrupt_activity( AI_HIT_MONSTER, monster ); } return (swap); } void mons_get_damage_level(const monsters* monster, std::string& desc, mon_dam_level_type& dam_level) { if (monster->hit_points <= monster->max_hit_points / 6) { desc += "almost "; desc += _wounded_damaged(monster->type) ? "destroyed" : "dead"; dam_level = MDAM_ALMOST_DEAD; return; } if (monster->hit_points <= monster->max_hit_points / 4) { desc += "severely "; dam_level = MDAM_SEVERELY_DAMAGED; } else if (monster->hit_points <= monster->max_hit_points / 3) { desc += "heavily "; dam_level = MDAM_HEAVILY_DAMAGED; } else if (monster->hit_points <= monster->max_hit_points * 3 / 4) { desc += "moderately "; dam_level = MDAM_MODERATELY_DAMAGED; } else if (monster->hit_points < monster->max_hit_points) { desc += "lightly "; dam_level = MDAM_LIGHTLY_DAMAGED; } else { desc += "not "; dam_level = MDAM_OKAY; } desc += _wounded_damaged(monster->type) ? "damaged" : "wounded"; } std::string get_wounds_description(const monsters *monster) { if (!monster->alive() || monster->hit_points == monster->max_hit_points) return ""; if (monster_descriptor(monster->type, MDSC_NOMSG_WOUNDS)) return ""; std::string desc; mon_dam_level_type dam_level; mons_get_damage_level(monster, desc, dam_level); desc.insert(0, " is "); desc += "."; return desc; } void print_wounds(const monsters *monster) { if (!monster->alive() || monster->hit_points == monster->max_hit_points) return; if (monster_descriptor(monster->type, MDSC_NOMSG_WOUNDS)) return; std::string desc; mon_dam_level_type dam_level; mons_get_damage_level(monster, desc, dam_level); desc.insert(0, " is "); desc += "."; simple_monster_message(monster, desc.c_str(), MSGCH_MONSTER_DAMAGE, dam_level); } // (true == 'damaged') [constructs, undead, etc.] // and (false == 'wounded') [living creatures, etc.] {dlb} static bool _wounded_damaged(int monster_type) { // this schema needs to be abstracted into real categories {dlb}: const mon_holy_type holy = mons_class_holiness(monster_type); if (holy == MH_UNDEAD || holy == MH_NONLIVING || holy == MH_PLANT) return (true); return (false); } //--------------------------------------------------------------- // // behaviour_event // // 1. Change any of: monster state, foe, and attitude // 2. Call handle_behaviour to re-evaluate AI state and target x,y // //--------------------------------------------------------------- void behaviour_event(monsters *mon, mon_event_type event, int src, coord_def src_pos, bool allow_shout) { ASSERT(src >= 0 && src <= MHITYOU); ASSERT(!crawl_state.arena || src != MHITYOU); ASSERT(in_bounds(src_pos) || src_pos.origin()); const beh_type old_behaviour = mon->behaviour; bool isSmart = (mons_intel(mon) > I_ANIMAL); bool wontAttack = mons_wont_attack_real(mon); bool sourceWontAttack = false; bool setTarget = false; bool breakCharm = false; bool was_sleeping = mons_is_sleeping(mon); if (src == MHITYOU) sourceWontAttack = true; else if (src != MHITNOT) sourceWontAttack = mons_wont_attack_real( &menv[src] ); if (is_sanctuary(mon->pos()) && mons_is_fleeing_sanctuary(mon)) { mon->behaviour = BEH_FLEE; mon->foe = MHITYOU; mon->target = env.sanctuary_pos; return; } switch (event) { case ME_DISTURB: // Assumes disturbed by noise... if (mons_is_sleeping(mon)) { mon->behaviour = BEH_WANDER; if (mons_near(mon)) remove_auto_exclude(mon, true); } // A bit of code to make Project Noise actually do // something again. Basically, dumb monsters and // monsters who aren't otherwise occupied will at // least consider the (apparent) source of the noise // interesting for a moment. -- bwr if (!isSmart || mon->foe == MHITNOT || mons_is_wandering(mon)) { if (mon->is_patrolling()) break; ASSERT(!src_pos.origin()); mon->target = src_pos; } break; case ME_WHACK: case ME_ANNOY: // Will turn monster against , unless they // are BOTH friendly or good neutral AND stupid, // or else fleeing anyway. Hitting someone over // the head, of course, always triggers this code. if (event == ME_WHACK || ((wontAttack != sourceWontAttack || isSmart) && !mons_is_fleeing(mon) && !mons_is_panicking(mon))) { // (Plain) plants and fungi cannot fight back. if (mon->type == MONS_FUNGUS || mon->type == MONS_PLANT) return; mon->foe = src; if (mons_is_sleeping(mon) && mons_near(mon)) remove_auto_exclude(mon, true); if (!mons_is_cornered(mon)) mon->behaviour = BEH_SEEK; if (src == MHITYOU) { mon->attitude = ATT_HOSTILE; breakCharm = true; } } // Now set target so that monster can whack back (once) at an // invisible foe. if (event == ME_WHACK) setTarget = true; break; case ME_ALERT: // Allow monsters falling asleep while patrolling (can happen if // they're left alone for a long time) to be woken by this event. if (mons_friendly(mon) && mon->is_patrolling() && !mons_is_sleeping(mon)) { break; } if (mons_is_sleeping(mon) && mons_near(mon)) remove_auto_exclude(mon, true); // Will alert monster to and turn them // against them, unless they have a current foe. // It won't turn friends hostile either. if (!mons_is_fleeing(mon) && !mons_is_panicking(mon) && !mons_is_cornered(mon)) { mon->behaviour = BEH_SEEK; } if (mon->foe == MHITNOT) mon->foe = src; if (!src_pos.origin() && (mon->foe == MHITNOT || mon->foe == src || mons_is_wandering(mon))) { if (mon->is_patrolling()) break; mon->target = src_pos; // XXX: Should this be done in _handle_behaviour()? if (src == MHITYOU && src_pos == you.pos() && !see_grid(mon->pos())) { const dungeon_feature_type can_move = (mons_amphibious(mon)) ? DNGN_DEEP_WATER : DNGN_SHALLOW_WATER; _try_pathfind(mon, can_move, true); } } break; case ME_SCARE: // Stationary monsters can't flee, and berserking monsters // are too enraged. if (mons_is_stationary(mon) || mon->has_ench(ENCH_BERSERK)) { mon->del_ench(ENCH_FEAR, true, true); break; } // Neither do plants or nonliving beings. if (mons_class_holiness(mon->type) == MH_PLANT || mons_class_holiness(mon->type) == MH_NONLIVING) { mon->del_ench(ENCH_FEAR, true, true); break; } // Assume monsters know where to run from, even if player is // invisible. mon->behaviour = BEH_FLEE; mon->foe = src; mon->target = src_pos; if (src == MHITYOU) { // Friendly monsters don't become hostile if you read a // scroll of fear, but enslaved ones will. // Send friendlies off to a random target so they don't cling // to you in fear. if (mons_friendly(mon)) { breakCharm = true; mon->foe = MHITNOT; _set_random_target(mon); } else setTarget = true; } else if (mons_friendly(mon)) mon->foe = MHITYOU; if (see_grid(mon->pos())) learned_something_new(TUT_FLEEING_MONSTER); break; case ME_CORNERED: // Some monsters can't flee. if (mon->behaviour != BEH_FLEE && !mon->has_ench(ENCH_FEAR)) break; // Pacified monsters shouldn't change their behaviour. if (mons_is_pacified(mon)) break; // Just set behaviour... foe doesn't change. if (!mons_is_cornered(mon)) { if (mons_friendly(mon)) { mon->foe = MHITYOU; simple_monster_message(mon, " returns to your side!"); } else simple_monster_message(mon, " turns to fight!"); } mon->behaviour = BEH_CORNERED; break; case ME_EVAL: break; } if (setTarget) { if (src == MHITYOU) { mon->target = you.pos(); mon->attitude = ATT_HOSTILE; } else if (src != MHITNOT) mon->target = menv[src].pos(); } // Now, break charms if appropriate. if (breakCharm) mon->del_ench(ENCH_CHARM); // Do any resultant foe or state changes. _handle_behaviour(mon); ASSERT(in_bounds(mon->target) || mon->target.origin()); // If it woke up and you're its new foe, it might shout. if (was_sleeping && !mons_is_sleeping(mon) && allow_shout && mon->foe == MHITYOU && !mons_wont_attack(mon)) { handle_monster_shouts(mon); } const bool wasLurking = (old_behaviour == BEH_LURK && !mons_is_lurking(mon)); const bool isPacified = mons_is_pacified(mon); if ((wasLurking || isPacified) && (event == ME_DISTURB || event == ME_ALERT || event == ME_EVAL)) { // Lurking monsters or pacified monsters leaving the level won't // stop doing so just because they noticed something. mon->behaviour = old_behaviour; } else if (wasLurking && mon->has_ench(ENCH_SUBMERGED) && !mon->del_ench(ENCH_SUBMERGED)) { // The same goes for lurking submerged monsters, if they can't // unsubmerge. mon->behaviour = BEH_LURK; } ASSERT(!crawl_state.arena || mon->foe != MHITYOU && mon->target != you.pos()); } static bool _choose_random_patrol_target_grid(monsters *mon) { const int intel = mons_intel(mon); // Zombies will occasionally just stand around. // This does not mean that they don't move every second turn. Rather, // once they reach their chosen target, there's a 50% chance they'll // just remain there until next turn when this function is called // again. if (intel == I_PLANT && coinflip()) return (true); // If there's no chance we'll find the patrol point, quit right away. if (grid_distance(mon->pos(), mon->patrol_point) > 2 * LOS_RADIUS) return (false); // Can the monster see the patrol point from its current position? const bool patrol_seen = mon->mon_see_grid(mon->patrol_point, habitat2grid(mons_primary_habitat(mon))); if (intel == I_PLANT && !patrol_seen) { // Really stupid monsters won't even try to get back into the // patrol zone. return (false); } // While the patrol point is in easy reach, monsters of insect/plant // intelligence will only use a range of 5 (distance from the patrol point). // Otherwise, try to get back using the full LOS. const int rad = (intel >= I_ANIMAL || !patrol_seen) ? LOS_RADIUS : 5; const bool is_smart = (intel >= I_NORMAL); monster_los patrol; // Set monster to make monster_los respect habitat restrictions. patrol.set_monster(mon); patrol.set_los_centre(mon->patrol_point); patrol.set_los_range(rad); patrol.fill_los_field(); monster_los lm; if (is_smart || !patrol_seen) { // For stupid monsters, don't bother if the patrol point is in sight. lm.set_monster(mon); lm.fill_los_field(); } int count_grids = 0; for (radius_iterator ri(mon->patrol_point, LOS_RADIUS, true, false); ri; ++ri) { // Don't bother for the current position. If everything fails, // we'll stay here anyway. if (*ri == mon->pos()) continue; if (!mon->can_pass_through_feat(grd(*ri))) continue; // Don't bother moving to squares (currently) occupied by a // monster. We'll usually be able to find other target squares // (and if we're not, we couldn't move anyway), and this avoids // monsters trying to move onto a grid occupied by a plant or // sleeping monster. if (monster_at(*ri)) continue; if (patrol_seen) { // If the patrol point can be easily (within LOS) reached // from the current position, it suffices if the target is // within reach of the patrol point OR the current position: // we can easily get there. // Only smart monsters will even attempt to move out of the // patrol area. // NOTE: Either of these can take us into a position where the // target cannot be easily reached (e.g. blocked by a wall) // and the patrol point is out of sight, too. Such a case // will be handled below, though it might take a while until // a monster gets out of a deadlock. (5% chance per turn.) if (!patrol.in_sight(*ri) && (!is_smart || !lm.in_sight(*ri))) continue; } else { // If, however, the patrol point is out of reach, we have to // make sure the new target brings us into reach of it. // This means that the target must be reachable BOTH from // the patrol point AND the current position. if (!patrol.in_sight(*ri) || !lm.in_sight(*ri)) continue; // If this fails for all surrounding squares (probably because // we're too far away), we fall back to heading directly for // the patrol point. } bool set_target = false; if (intel == I_PLANT && *ri == mon->patrol_point) { // Slightly greater chance to simply head for the centre. count_grids += 3; if (x_chance_in_y(3, count_grids)) set_target = true; } else if (one_chance_in(++count_grids)) set_target = true; if (set_target) mon->target = *ri; } return (count_grids); } //#define DEBUG_PATHFIND // Check all grids in LoS and mark lava and/or water as seen if the // appropriate grids are encountered, so we later only need to do the // visibility check for monsters that can't pass a feature potentially in // the way. We don't care about shallow water as most monsters can safely // cross that, and fire elementals alone aren't really worth the extra // hassle. :) static void _check_lava_water_in_sight() { you.lava_in_sight = you.water_in_sight = 0; for (radius_iterator ri(you.pos(), LOS_RADIUS); ri; ++ri) { const coord_def ep = *ri - you.pos() + coord_def(9, 9); if (env.show(ep)) { const dungeon_feature_type feat = grd(*ri); if (feat == DNGN_LAVA) { you.lava_in_sight = 1; if (you.water_in_sight > 0) break; } else if (feat == DNGN_DEEP_WATER) { you.water_in_sight = 1; if (you.lava_in_sight > 0) break; } } } } // If a monster can see but not directly reach the target, and then fails to // find a path to get there, mark all surrounding (in a radius of 2) monsters // of the same (or greater) movement restrictions as also being unable to // find a path, so we won't need to calculate again. // Should there be a direct path to the target for a monster thus marked, it // will still be able to come nearer (and the mark will then be cleared). static void _mark_neighbours_target_unreachable(monsters *mon) { // Highly intelligent monsters are perfectly capable of pathfinding // and don't need their neighbour's advice. const mon_intel_type intel = mons_intel(mon); if (intel > I_NORMAL) return; const bool flies = mons_flies(mon); const bool amphibious = mons_amphibious(mon); const habitat_type habit = mons_primary_habitat(mon); for (radius_iterator ri(mon->pos(), 2, true, false); ri; ++ri) { if (*ri == mon->pos()) continue; // Don't alert monsters out of sight (e.g. on the other side of // a wall). if (!mon->mon_see_grid(*ri)) continue; monsters* const m = monster_at(*ri); if (m == NULL) continue; // Don't restrict smarter monsters as they might find a path // a dumber monster wouldn't. if (mons_intel(m) > intel) continue; // Monsters of differing habitats might prefer different routes. if (mons_primary_habitat(m) != habit) continue; // A flying monster has an advantage over a non-flying one. if (!flies && mons_flies(m)) continue; // Same for a swimming one, around water. if (you.water_in_sight > 0 && !amphibious && mons_amphibious(m)) continue; if (m->travel_target == MTRAV_NONE) m->travel_target = MTRAV_UNREACHABLE; } } static bool _is_level_exit(const coord_def& pos) { // All types of stairs. if (is_stair(grd(pos))) return (true); // Teleportation and shaft traps. const trap_type tt = get_trap_type(pos); if (tt == TRAP_TELEPORT || tt == TRAP_SHAFT) return (true); return (false); } static void _find_all_level_exits(std::vector &e) { e.clear(); for (rectangle_iterator ri(1); ri; ++ri) { if (!in_bounds(*ri)) continue; if (_is_level_exit(*ri)) e.push_back(level_exit(*ri, false)); } } static int _mons_find_nearest_level_exit(const monsters *mon, std::vector &e, bool reset = false) { if (e.empty() || reset) _find_all_level_exits(e); int retval = -1; int old_dist = -1; for (unsigned int i = 0; i < e.size(); ++i) { if (e[i].unreachable) continue; int dist = grid_distance(mon->pos(), e[i].target); if (old_dist == -1 || old_dist >= dist) { // Ignore teleportation and shaft traps that the monster // shouldn't know about. if (!mons_is_native_in_branch(mon) && grd(e[i].target) == DNGN_UNDISCOVERED_TRAP) { continue; } retval = i; old_dist = dist; } } return (retval); } // If _mons_find_level_exits() is ever expanded to handle more grid // types, this should be expanded along with it. static void _mons_indicate_level_exit(const monsters *mon) { const dungeon_feature_type gridc = grd(mon->pos()); const bool is_shaft = (get_trap_type(mon->pos()) == TRAP_SHAFT); if (is_gate(gridc)) simple_monster_message(mon, " passes through the gate."); else if (is_travelable_stair(gridc)) { command_type dir = grid_stair_direction(gridc); simple_monster_message(mon, make_stringf(" %s the %s.", dir == CMD_GO_UPSTAIRS ? "goes up" : dir == CMD_GO_DOWNSTAIRS ? "goes down" : "takes", is_escape_hatch(gridc) ? "escape hatch" : "stairs").c_str()); } else if (is_shaft) { simple_monster_message(mon, make_stringf(" %s the shaft.", mons_flies(mon) ? "goes down" : "jumps into").c_str()); } } void make_mons_leave_level(monsters *mon) { if (mons_is_pacified(mon)) { if (you.can_see(mon)) _mons_indicate_level_exit(mon); // Pacified monsters leaving the level take their stuff with // them. mon->flags |= MF_HARD_RESET; monster_die(mon, KILL_DISMISSED, NON_MONSTER); } } static void _set_no_path_found(monsters *mon) { #ifdef DEBUG_PATHFIND mpr("No path found!"); #endif mon->travel_target = MTRAV_UNREACHABLE; // Pass information on to nearby monsters. _mark_neighbours_target_unreachable(mon); } static bool _target_is_unreachable(monsters *mon) { return (mon->travel_target == MTRAV_UNREACHABLE || mon->travel_target == MTRAV_KNOWN_UNREACHABLE); } // The monster is trying to get to the player (MHITYOU). // Check whether there's an unobstructed path to the player (in sight!), // either by using an existing travel_path or calculating a new one. // Returns true if no further handling necessary, else false. static bool _try_pathfind(monsters *mon, const dungeon_feature_type can_move, bool potentially_blocking) { // Just because we can *see* the player, that doesn't mean // we can actually get there. To find about that, we first // check for transparent walls. If there are transparent // walls in the way we'll need pathfinding, no matter what. // (Though monsters with a los attack don't need to get any // closer to hurt the player.) // If no walls are detected, there could still be a river // or a pool of lava in the way. So we check whether there // is water or lava in LoS (boolean) and if so, try to find // a way around it. It's possible that the player can see // lava but it actually has no influence on the monster's // movement (because it's lying in the opposite direction) // but if so, we'll find that out during path finding. // In another attempt of optimization, don't bother with // path finding if the monster in question has no trouble // travelling through water or flying across lava. // Also, if no path is found (too far away, perhaps) set a // flag, so we don't directly calculate the whole thing again // next turn, and even extend that flag to neighbouring // monsters of similar movement restrictions. // Smart monsters that can fire through walls won't use // pathfinding, and it's also not necessary if the monster // is already adjacent to you. if (potentially_blocking && mons_intel(mon) >= I_NORMAL && !mons_friendly(mon) && mons_has_los_ability(mon->type) || grid_distance(mon->pos(), you.pos()) == 1) { potentially_blocking = false; } else { // If we don't already know whether there's water or lava // in LoS of the player, find out now. if (you.lava_in_sight == -1 || you.water_in_sight == -1) _check_lava_water_in_sight(); // Flying monsters don't see water/lava as obstacle. // Also don't use pathfinding if the monster can shoot // across the blocking terrain, and is smart enough to // realize that. if (!potentially_blocking && !mons_flies(mon) && (mons_intel(mon) < I_NORMAL || mons_friendly(mon) || (!mons_has_ranged_spell(mon, true) && !mons_has_ranged_attack(mon)))) { const habitat_type habit = mons_primary_habitat(mon); if (you.lava_in_sight > 0 && habit != HT_LAVA || you.water_in_sight > 0 && habit != HT_WATER && can_move != DNGN_DEEP_WATER) { potentially_blocking = true; } } } if (!potentially_blocking || grid_see_grid(mon->pos(), you.pos(), can_move)) { // The player is easily reachable. // Clear travel path and target, if necessary. if (mon->travel_target != MTRAV_PATROL && mon->travel_target != MTRAV_NONE) { if (mon->is_travelling()) mon->travel_path.clear(); mon->travel_target = MTRAV_NONE; } return (false); } // Even if the target has been to "unreachable" (the monster already tried, // and failed, to find a path) there's a chance of trying again. if (!_target_is_unreachable(mon) || one_chance_in(12)) { #ifdef DEBUG_PATHFIND mprf("%s: Player out of reach! What now?", mon->name(DESC_PLAIN).c_str()); #endif // If we're already on our way, do nothing. if (mon->is_travelling() && mon->travel_target == MTRAV_PLAYER) { const int len = mon->travel_path.size(); const coord_def targ = mon->travel_path[len - 1]; // Current target still valid? if (grid_see_grid(targ, you.pos(), can_move)) { // Did we reach the target? if (mon->pos() == mon->travel_path[0]) { // Get next waypoint. mon->travel_path.erase( mon->travel_path.begin() ); if (!mon->travel_path.empty()) { mon->target = mon->travel_path[0]; return (true); } } else if (grid_see_grid(mon->pos(), mon->travel_path[0], can_move)) { mon->target = mon->travel_path[0]; return (true); } } } // Use pathfinding to find a (new) path to the player. const int dist = grid_distance(mon->pos(), you.pos()); #ifdef DEBUG_PATHFIND mprf("Need to calculate a path... (dist = %d)", dist); #endif const int range = mons_tracking_range(mon); if (range > 0 && dist > range) { mon->travel_target = MTRAV_UNREACHABLE; #ifdef DEBUG_PATHFIND mprf("Distance too great, don't attempt pathfinding! (%s)", mon->name(DESC_PLAIN).c_str()); #endif return (false); } #ifdef DEBUG_PATHFIND mprf("Need a path for %s from (%d, %d) to (%d, %d), max. dist = %d", mon->name(DESC_PLAIN).c_str(), mon->pos(), you.pos(), range); #endif monster_pathfind mp; if (range > 0) mp.set_range(range); if (mp.init_pathfind(mon, you.pos())) { mon->travel_path = mp.calc_waypoints(); if (!mon->travel_path.empty()) { // Okay then, we found a path. Let's use it! mon->target = mon->travel_path[0]; mon->travel_target = MTRAV_PLAYER; return (true); } else _set_no_path_found(mon); } else _set_no_path_found(mon); } // We didn't find a path. return (false); } // Returns true if a monster left the level. static bool _pacified_leave_level(monsters *mon, std::vector e, int e_index) { // If a pacified monster is leaving the level, and has reached an // exit (whether that exit was its target or not), handle it here. // Likewise, if a pacified monster is far enough away from the // player, make it leave the level. if (_is_level_exit(mon->pos()) || (e_index != -1 && mon->pos() == e[e_index].target) || grid_distance(mon->pos(), you.pos()) >= LOS_RADIUS * 4) { make_mons_leave_level(mon); return (true); } return (false); } // Counts deep water twice. static int _count_water_neighbours(coord_def p) { int water_count = 0; for (adjacent_iterator ai(p); ai; ++ai) { if (grd(*ai) == DNGN_SHALLOW_WATER) water_count++; else if (grd(*ai) == DNGN_DEEP_WATER) water_count += 2; } return (water_count); } // Pick the nearest water grid that is surrounded by the most // water squares within LoS. static bool _find_siren_water_target(monsters *mon) { ASSERT(mon->type == MONS_SIREN); // Moving away could break the entrancement, so don't do this. if ((mon->pos() - you.pos()).rdist() >= 6) return (false); // Already completely surrounded by deep water. if (_count_water_neighbours(mon->pos()) >= 16) return (true); if (mon->travel_target == MTRAV_SIREN) { coord_def targ_pos(mon->travel_path[mon->travel_path.size() - 1]); #ifdef DEBUG_PATHFIND mprf("siren target is (%d, %d), dist = %d", targ_pos.x, targ_pos.y, (int) (mon->pos() - targ_pos).rdist()); #endif if ((mon->pos() - targ_pos).rdist() > 2) return (true); } monster_los lm; lm.set_monster(mon); lm.set_los_range(LOS_RADIUS); lm.fill_los_field(); int best_water_count = 0; coord_def best_target; bool first = true; while (true) { int best_num = 0; for (radius_iterator ri(mon->pos(), LOS_RADIUS, true, false); ri; ++ri) { if (!grid_is_water(grd(*ri))) continue; // In the first iteration only count water grids that are // not closer to the player than to the siren. if (first && (mon->pos() - *ri).rdist() > (you.pos() - *ri).rdist()) continue; // Counts deep water twice. const int water_count = _count_water_neighbours(*ri); if (water_count < best_water_count) continue; if (water_count > best_water_count) { best_water_count = water_count; best_target = *ri; best_num = 1; } else // water_count == best_water_count { const int old_dist = (mon->pos() - best_target).rdist(); const int new_dist = (mon->pos() - *ri).rdist(); if (new_dist > old_dist) continue; if (new_dist < old_dist) { best_target = *ri; best_num = 1; } else if (one_chance_in(++best_num)) best_target = *ri; } } if (!first || best_water_count > 0) break; // Else start the second iteration. first = false; } if (!best_water_count) return (false); // We're already optimally placed. if (best_target == mon->pos()) return (true); monster_pathfind mp; #ifdef WIZARD // Remove old highlighted areas to make place for the new ones. for (rectangle_iterator ri(1); ri; ++ri) env.map(*ri).property &= ~(FPROP_HIGHLIGHT); #endif if (mp.init_pathfind(mon, best_target)) { mon->travel_path = mp.calc_waypoints(); if (!mon->travel_path.empty()) { #ifdef WIZARD for (unsigned int i = 0; i < mon->travel_path.size(); i++) env.map(mon->travel_path[i]).property |= FPROP_HIGHLIGHT; #endif #ifdef DEBUG_PATHFIND mprf("Found a path to (%d, %d) with %d surrounding water squares", best_target.x, best_target.y, best_water_count); #endif // Okay then, we found a path. Let's use it! mon->target = mon->travel_path[0]; mon->travel_target = MTRAV_SIREN; return (true); } } return (false); } static bool _find_wall_target(monsters *mon) { ASSERT(mons_wall_shielded(mon)); if (mon->travel_target == MTRAV_WALL) { coord_def targ_pos(mon->travel_path[mon->travel_path.size() - 1]); // Target grid might have changed since we started, like if the // player destroys the wall the monster wants to hide in. if (grid_is_solid(targ_pos) && monster_habitable_grid(mon, grd(targ_pos))) { // Wall is still good. #ifdef DEBUG_PATHFIND mprf("%s target is (%d, %d), dist = %d", mon->name(DESC_PLAIN, true).c_str(), targ_pos.x, targ_pos.y, (int) (mon->pos() - targ_pos).rdist()); #endif return (true); } mon->travel_path.clear(); mon->travel_target = MTRAV_NONE; } monster_los lm; lm.set_monster(mon); lm.set_los_range(LOS_RADIUS); lm.fill_los_field(); int best_dist = INT_MAX; bool best_closer_to_player = false; coord_def best_target; for (radius_iterator ri(mon->pos(), LOS_RADIUS, true, false); ri; ++ri) { if (!grid_is_solid(*ri) || !monster_habitable_grid(mon, grd(*ri))) { continue; } int dist = (mon->pos() - *ri).rdist(); bool closer_to_player = false; if (dist > (you.pos() - *ri).rdist()) closer_to_player = true; if (dist < best_dist) { best_dist = dist; best_closer_to_player = closer_to_player; best_target = *ri; } else if (best_closer_to_player && !closer_to_player && dist == best_dist) { best_closer_to_player = false; best_target = *ri; } } if (best_dist == INT_MAX || !in_bounds(best_target)) return (false); monster_pathfind mp; #ifdef WIZARD // Remove old highlighted areas to make place for the new ones. for (rectangle_iterator ri(1); ri; ++ri) env.map(*ri).property &= ~(FPROP_HIGHLIGHT); #endif if (mp.init_pathfind(mon, best_target)) { mon->travel_path = mp.calc_waypoints(); if (!mon->travel_path.empty()) { #ifdef WIZARD for (unsigned int i = 0; i < mon->travel_path.size(); i++) env.map(mon->travel_path[i]).property |= FPROP_HIGHLIGHT; #endif #ifdef DEBUG_PATHFIND mprf("Found a path to (%d, %d)", best_target.x, best_target.y); #endif // Okay then, we found a path. Let's use it! mon->target = mon->travel_path[0]; mon->travel_target = MTRAV_WALL; return (true); } } return (false); } // Returns true if further handling neeeded. static bool _handle_monster_travelling(monsters *mon, const dungeon_feature_type can_move) { #ifdef DEBUG_PATHFIND mprf("Monster %s reached target (%d, %d)", mon->name(DESC_PLAIN).c_str(), mon->target.x, mon->target.y); #endif // Hey, we reached our first waypoint! if (mon->pos() == mon->travel_path[0]) { #ifdef DEBUG_PATHFIND mpr("Arrived at first waypoint."); #endif mon->travel_path.erase( mon->travel_path.begin() ); if (mon->travel_path.empty()) { #ifdef DEBUG_PATHFIND mpr("We reached the end of our path: stop travelling."); #endif mon->travel_target = MTRAV_NONE; return (true); } else { mon->target = mon->travel_path[0]; #ifdef DEBUG_PATHFIND mprf("Next waypoint: (%d, %d)", mon->target.x, mon->target.y); #endif return (false); } } // Can we still see our next waypoint? if (!grid_see_grid(mon->pos(), mon->travel_path[0], can_move)) { #ifdef DEBUG_PATHFIND mpr("Can't see waypoint grid."); #endif // Apparently we got sidetracked a bit. // Check the waypoints vector backwards and pick the first waypoint // we can see. // XXX: Note that this might still not be the best thing to do // since another path might be even *closer* to our actual target now. // Not by much, though, since the original path was optimal (A*) and // the distance between the waypoints is rather small. int erase = -1; // Erase how many waypoints? const int size = mon->travel_path.size(); for (int i = size - 1; i >= 0; --i) { if (grid_see_grid(mon->pos(), mon->travel_path[i], can_move)) { mon->target = mon->travel_path[i]; erase = i; break; } } if (erase > 0) { #ifdef DEBUG_PATHFIND mprf("Need to erase %d of %d waypoints.", erase, size); #endif // Erase all waypoints that came earlier: // we don't need them anymore. while (0 < erase--) mon->travel_path.erase( mon->travel_path.begin() ); } else { // We can't reach our old path from our current // position, so calculate a new path instead. monster_pathfind mp; // The last coordinate in the path vector is our destination. const int len = mon->travel_path.size(); if (mp.init_pathfind(mon, mon->travel_path[len-1])) { mon->travel_path = mp.calc_waypoints(); if (!mon->travel_path.empty()) { mon->target = mon->travel_path[0]; #ifdef DEBUG_PATHFIND mprf("Next waypoint: (%d, %d)", mon->target.x, mon->target.y); #endif } else { mon->travel_target = MTRAV_NONE; return (true); } } else { // Or just forget about the whole thing. mon->travel_path.clear(); mon->travel_target = MTRAV_NONE; return (true); } } } // Else, we can see the next waypoint and are making good progress. // Carry on, then! return (false); } // Returns true if further handling neeeded. static bool _handle_monster_patrolling(monsters *mon) { if (!_choose_random_patrol_target_grid(mon)) { // If we couldn't find a target that is within easy reach // of the monster and close to the patrol point, depending // on monster intelligence, do one of the following: // * set current position as new patrol point // * forget about patrolling // * head back to patrol point if (mons_intel(mon) == I_PLANT) { // Really stupid monsters forget where they're supposed to be. if (mons_friendly(mon)) { // Your ally was told to wait, and wait it will! // (Though possibly not where you told it to.) mon->patrol_point = mon->pos(); } else { // Stop patrolling. mon->patrol_point.reset(); mon->travel_target = MTRAV_NONE; return (true); } } else { // It's time to head back! // Other than for tracking the player, there's currently // no distinction between smart and stupid monsters when // it comes to travelling back to the patrol point. This // is in part due to the flavour of e.g. bees finding // their way back to the Hive (and patrolling should // really be restricted to cases like this), and for the // other part it's not all that important because we // calculate the path once and then follow it home, and // the player won't ever see the orderly fashion the // bees will trudge along. // What he will see is them swarming back to the Hive // entrance after some time, and that is what matters. monster_pathfind mp; if (mp.init_pathfind(mon, mon->patrol_point)) { mon->travel_path = mp.calc_waypoints(); if (!mon->travel_path.empty()) { mon->target = mon->travel_path[0]; mon->travel_target = MTRAV_PATROL; } else { // We're so close we don't even need a path. mon->target = mon->patrol_point; } } else { // Stop patrolling. mon->patrol_point.reset(); mon->travel_target = MTRAV_NONE; return (true); } } } else { #ifdef DEBUG_PATHFIND mprf("Monster %s (pp: %d, %d) is now patrolling to (%d, %d)", mon->name(DESC_PLAIN).c_str(), mon->patrol_point.x, mon->patrol_point.y, mon->target.x, mon->target.y); #endif } return (false); } static void _check_wander_target(monsters *mon, bool isPacified = false, dungeon_feature_type can_move = DNGN_UNSEEN) { // default wander behaviour if (mon->pos() == mon->target || mons_is_batty(mon) || !isPacified && one_chance_in(20)) { bool need_target = true; if (!can_move) { can_move = (mons_amphibious(mon) ? DNGN_DEEP_WATER : DNGN_SHALLOW_WATER); } if (mon->is_travelling()) need_target = _handle_monster_travelling(mon, can_move); // If we still need a target because we're not travelling // (any more), check for patrol routes instead. if (need_target && mon->is_patrolling()) need_target = _handle_monster_patrolling(mon); // XXX: This is really dumb wander behaviour... instead of // changing the goal square every turn, better would be to // have the monster store a direction and have the monster // head in that direction for a while, then shift the // direction to the left or right. We're changing this so // wandering monsters at least appear to have some sort of // attention span. -- bwr if (need_target) _set_random_target(mon); } } static void _arena_set_foe(monsters *mons) { const int mind = monster_index(mons); int nearest = -1; int best_distance = -1; int nearest_unseen = -1; int best_unseen_distance = -1; for (int i = 0; i < MAX_MONSTERS; ++i) { if (mind == i) continue; const monsters *other(&menv[i]); if (!other->alive() || mons_aligned(mind, i)) continue; // Don't fight test spawners, since they're only pseudo-monsters // placed to spawn real monsters, plus they're impossible to // kill. But test spawners can fight each other, to give them a // target to spawn against. if (other->type == MONS_TEST_SPAWNER && mons->type != MONS_TEST_SPAWNER) { continue; } const int distance = grid_distance(mons->pos(), other->pos()); const bool seen = mons->can_see(other); if (seen) { if (best_distance == -1 || distance < best_distance) { best_distance = distance; nearest = i; } } else { if (best_unseen_distance == -1 || distance < best_unseen_distance) { best_unseen_distance = distance; nearest_unseen = i; } } if ((best_distance == -1 || distance < best_distance) && mons->can_see(other)) { best_distance = distance; nearest = i; } } if (nearest != -1) { mons->foe = nearest; mons->target = menv[nearest].pos(); mons->behaviour = BEH_SEEK; } else if (nearest_unseen != -1) { mons->target = menv[nearest_unseen].pos(); if (mons->type == MONS_TEST_SPAWNER) { mons->foe = nearest_unseen; mons->behaviour = BEH_SEEK; } else mons->behaviour = BEH_WANDER; } else { mons->foe = MHITNOT; mons->behaviour = BEH_WANDER; } if (mons->behaviour == BEH_WANDER) _check_wander_target(mons); ASSERT(mons->foe == MHITNOT || !mons->target.origin()); } //--------------------------------------------------------------- // // handle_behaviour // // 1. Evaluates current AI state // 2. Sets monster target x,y based on current foe // // XXX: Monsters of I_NORMAL or above should select a new target // if their current target is another monster which is sitting in // a wall and is immune to most attacks while in a wall, unless // the monster has a spell or special/nearby ability which isn't // affected by the wall. //--------------------------------------------------------------- static void _handle_behaviour(monsters *mon) { bool changed = true; bool isFriendly = mons_friendly(mon); bool isNeutral = mons_neutral(mon); bool wontAttack = mons_wont_attack_real(mon); bool proxPlayer = mons_near(mon) && !crawl_state.arena; bool trans_wall_block = trans_wall_blocking(mon->pos()); #ifdef WIZARD // If stealth is greater than actually possible (wizmode level) // pretend the player isn't there, but only for hostile monsters. if (proxPlayer && you.skills[SK_STEALTH] > 27 && !mons_wont_attack(mon)) proxPlayer = false; #endif bool proxFoe; bool isHurt = (mon->hit_points <= mon->max_hit_points / 4 - 1); bool isHealthy = (mon->hit_points > mon->max_hit_points / 2); bool isSmart = (mons_intel(mon) > I_ANIMAL); bool isScared = mon->has_ench(ENCH_FEAR); bool isMobile = !mons_is_stationary(mon); bool isPacified = mons_is_pacified(mon); bool patrolling = mon->is_patrolling(); static std::vector e; static int e_index = -1; // Check for confusion -- early out. if (mon->has_ench(ENCH_CONFUSION)) { _set_random_target(mon); return; } if (mons_is_fleeing_sanctuary(mon) && mons_is_fleeing(mon) && is_sanctuary(you.pos())) { return; } if (crawl_state.arena) { if (Options.arena_force_ai) { if (!mon->get_foe() || mon->target.origin() || one_chance_in(3)) mon->foe = MHITNOT; if (mon->foe == MHITNOT || mon->foe == MHITYOU) _arena_set_foe(mon); return; } // If we're not forcing monsters to attack, just make sure they're // not targeting the player in arena mode. else if (mon->foe == MHITYOU) mon->foe = MHITNOT; } if (mons_wall_shielded(mon) && grid_is_solid(mon->pos())) { // Monster is safe, so its behaviour can be simplified to fleeing. if (mon->behaviour == BEH_CORNERED || mon->behaviour == BEH_PANIC || isScared) { mon->behaviour = BEH_FLEE; } } const dungeon_feature_type can_move = (mons_amphibious(mon)) ? DNGN_DEEP_WATER : DNGN_SHALLOW_WATER; // Validate current target exists. if (mon->foe != MHITNOT && mon->foe != MHITYOU) { const monsters& foe_monster = menv[mon->foe]; if (!foe_monster.alive()) mon->foe = MHITNOT; if (mons_friendly(&foe_monster) == isFriendly) mon->foe = MHITNOT; } // Change proxPlayer depending on invisibility and standing // in shallow water. if (proxPlayer && you.invisible()) { if (!mons_player_visible( mon )) proxPlayer = false; // Must be able to see each other. else if (!see_grid(mon->pos())) proxPlayer = false; const int intel = mons_intel(mon); // Now, the corollary to that is that sometimes, if a // player is right next to a monster, they will 'see'. if (grid_distance( you.pos(), mon->pos() ) == 1 && one_chance_in(3)) { proxPlayer = true; } // [dshaligram] Very smart monsters have a chance of clueing in to // invisible players in various ways. else if (intel == I_NORMAL && one_chance_in(13) || intel == I_HIGH && one_chance_in(6)) { proxPlayer = true; } } // Set friendly target, if they don't already have one. // Berserking allies ignore your commands! if (isFriendly && you.pet_target != MHITNOT && (mon->foe == MHITNOT || mon->foe == MHITYOU) && !mon->has_ench(ENCH_BERSERK)) { mon->foe = you.pet_target; } // Instead, berserkers attack nearest monsters. if (mon->has_ench(ENCH_BERSERK) && (mon->foe == MHITNOT || isFriendly && mon->foe == MHITYOU)) { // Intelligent monsters prefer to attack the player, // even when berserking. if (!isFriendly && proxPlayer && mons_intel(mon) >= I_NORMAL) mon->foe = MHITYOU; else _set_nearest_monster_foe(mon); } // Pacified monsters leaving the level prefer not to attack. if (isNeutral && !isPacified && mon->foe == MHITNOT) _set_nearest_monster_foe(mon); // Monsters do not attack themselves. {dlb} if (mon->foe == monster_index(mon)) mon->foe = MHITNOT; // Friendly and good neutral monsters do not attack other friendly // and good neutral monsters. if (mon->foe != MHITNOT && mon->foe != MHITYOU && wontAttack && mons_wont_attack_real(&menv[mon->foe])) { mon->foe = MHITNOT; } // Neutral monsters prefer not to attack players, or other neutrals. if (isNeutral && mon->foe != MHITNOT && (mon->foe == MHITYOU || mons_neutral(&menv[mon->foe]))) { mon->foe = MHITNOT; } // Unfriendly monsters fighting other monsters will usually // target the player, if they're healthy. if (!isFriendly && !isNeutral && mon->foe != MHITYOU && mon->foe != MHITNOT && proxPlayer && !(mon->has_ench(ENCH_BERSERK)) && isHealthy && !one_chance_in(3)) { mon->foe = MHITYOU; } // Validate current target again. if (mon->foe != MHITNOT && mon->foe != MHITYOU) { const monsters& foe_monster = menv[mon->foe]; if (!foe_monster.alive()) mon->foe = MHITNOT; if (mons_friendly(&foe_monster) == isFriendly) mon->foe = MHITNOT; } while (changed) { coord_def foepos = you.pos(); // Evaluate these each time; they may change. if (mon->foe == MHITNOT) proxFoe = false; else { if (mon->foe == MHITYOU) { foepos = you.pos(); proxFoe = proxPlayer; // Take invis into account. } else { proxFoe = mons_near(mon, mon->foe); if (!mon_can_see_monster(mon, &menv[mon->foe])) proxFoe = false; foepos = menv[mon->foe].pos(); } } // Track changes to state; attitude never changes here. beh_type new_beh = mon->behaviour; unsigned short new_foe = mon->foe; // Take care of monster state changes. switch (mon->behaviour) { case BEH_SLEEP: // default sleep state mon->target = mon->pos(); new_foe = MHITNOT; break; case BEH_LURK: case BEH_SEEK: // No foe? Then wander or seek the player. if (mon->foe == MHITNOT) { if (!proxPlayer || isNeutral || patrolling) new_beh = BEH_WANDER; else { new_foe = MHITYOU; mon->target = you.pos(); } break; } // Foe gone out of LOS? if (!proxFoe) { if (mon->travel_target == MTRAV_SIREN) mon->travel_target = MTRAV_NONE; if (mon->foe == MHITYOU && mon->is_travelling() && mon->travel_target == MTRAV_PLAYER) { // We've got a target, so we'll continue on our way. #ifdef DEBUG_PATHFIND mpr("Player out of LoS... start wandering."); #endif new_beh = BEH_WANDER; break; } if (isFriendly) { if (patrolling) { new_foe = MHITNOT; new_beh = BEH_WANDER; } else { new_foe = MHITYOU; mon->target = foepos; } break; } if (mon->foe_memory > 0 && mon->foe != MHITNOT) { // If we've arrived at our target x,y // do a stealth check. If the foe // fails, monster will then start // tracking foe's CURRENT position, // but only for a few moves (smell and // intuition only go so far). if (mon->pos() == mon->target) { if (mon->foe == MHITYOU) { if (one_chance_in(you.skills[SK_STEALTH]/3)) mon->target = you.pos(); else mon->foe_memory = 1; } else { if (coinflip()) // XXX: cheesy! mon->target = menv[mon->foe].pos(); else mon->foe_memory = 1; } } // Either keep chasing, or start wandering. if (mon->foe_memory < 2) { mon->foe_memory = 0; new_beh = BEH_WANDER; } break; } // Hack: smarter monsters will tend to pursue the player longer. int memory = 0; switch (mons_intel(mon)) { case I_HIGH: memory = 100 + random2(200); break; case I_NORMAL: memory = 50 + random2(100); break; case I_ANIMAL: case I_INSECT: memory = 25 + random2(75); break; case I_PLANT: memory = 10 + random2(50); break; } mon->foe_memory = memory; break; // switch/case BEH_SEEK } // Monster can see foe: continue 'tracking' // by updating target x,y. if (mon->foe == MHITYOU) { // The foe is the player. if (mon->type == MONS_SIREN && player_mesmerised_by(mon) && _find_siren_water_target(mon)) { break; } if (_try_pathfind(mon, can_move, trans_wall_block)) break; // Whew. If we arrived here, path finding didn't yield anything // (or wasn't even attempted) and we need to set our target // the traditional way. // Sometimes, your friends will wander a bit. if (isFriendly && one_chance_in(8)) { _set_random_target(mon); mon->foe = MHITNOT; new_beh = BEH_WANDER; } else { mon->target = you.pos(); } } else { // We have a foe but it's not the player. mon->target = menv[mon->foe].pos(); } // Smart monsters, zombified monsters other than spectral // things, plants, and nonliving monsters cannot flee. if (isHurt && !isSmart && isMobile && (!mons_is_zombified(mon) || mon->type == MONS_SPECTRAL_THING) && mons_class_holiness(mon->type) != MH_PLANT && mons_class_holiness(mon->type) != MH_NONLIVING) { new_beh = BEH_FLEE; } break; case BEH_WANDER: if (isPacified) { // If a pacified monster isn't travelling toward // someplace from which it can leave the level, make it // start doing so. If there's no such place, either // search the level for such a place again, or travel // randomly. if (mon->travel_target != MTRAV_PATROL) { new_foe = MHITNOT; mon->travel_path.clear(); e_index = _mons_find_nearest_level_exit(mon, e); if (e_index == -1 || one_chance_in(20)) e_index = _mons_find_nearest_level_exit(mon, e, true); if (e_index != -1) { mon->travel_target = MTRAV_PATROL; patrolling = true; mon->patrol_point = e[e_index].target; mon->target = e[e_index].target; } else { mon->travel_target = MTRAV_NONE; patrolling = false; mon->patrol_point.reset(); _set_random_target(mon); } } if (_pacified_leave_level(mon, e, e_index)) return; } // Is our foe in LOS? // Batty monsters don't automatically reseek so that // they'll flitter away, we'll reset them just before // they get movement in handle_monsters() instead. -- bwr if (proxFoe && !mons_is_batty(mon)) { new_beh = BEH_SEEK; break; } _check_wander_target(mon, isPacified, can_move); // During their wanderings, monsters will eventually relax // their guard (stupid ones will do so faster, smart // monsters have longer memories). Pacified monsters will // also eventually switch the place from which they want to // leave the level, in case their current choice is blocked. if (!proxFoe && mon->foe != MHITNOT && one_chance_in(isSmart ? 60 : 20) || isPacified && one_chance_in(isSmart ? 40 : 120)) { new_foe = MHITNOT; if (mon->is_travelling() && mon->travel_target != MTRAV_PATROL || isPacified) { #ifdef DEBUG_PATHFIND mpr("It's been too long! Stop travelling."); #endif mon->travel_path.clear(); mon->travel_target = MTRAV_NONE; if (isPacified && e_index != -1) e[e_index].unreachable = true; } } break; case BEH_FLEE: // Check for healed. if (isHealthy && !isScared) new_beh = BEH_SEEK; // Smart monsters flee until they can flee no more... // possible to get a 'CORNERED' event, at which point // we can jump back to WANDER if the foe isn't present. if (isFriendly) { // Special-cased below so that it will flee *towards* you. if (mon->foe == MHITYOU) mon->target = you.pos(); } else if (mons_wall_shielded(mon) && _find_wall_target(mon)) ; // Wall target found. else if (proxFoe) { // Special-cased below so that it will flee *from* the // correct position. mon->target = foepos; } break; case BEH_CORNERED: // Plants or nonliving monsters cannot fight back. if (mons_class_holiness(mon->type) == MH_PLANT || mons_class_holiness(mon->type) == MH_NONLIVING) { break; } if (isHealthy) new_beh = BEH_SEEK; // Foe gone out of LOS? if (!proxFoe) { if ((isFriendly || proxPlayer) && !isNeutral && !patrolling) new_foe = MHITYOU; else new_beh = BEH_WANDER; } else { mon->target = foepos; } break; default: return; // uh oh } changed = (new_beh != mon->behaviour || new_foe != mon->foe); mon->behaviour = new_beh; if (mon->foe != new_foe) mon->foe_memory = 0; mon->foe = new_foe; } if (mon->travel_target == MTRAV_WALL && grid_is_solid(mon->pos())) { if (mon->behaviour == BEH_FLEE) { // Monster is safe, so stay put. mon->target = mon->pos(); mon->foe = MHITNOT; } } } static bool _mons_check_set_foe(monsters *mon, const coord_def& p, bool friendly, bool neutral) { if (!inside_level_bounds(p)) return (false); if (!friendly && !neutral && p == you.pos() && mons_player_visible(mon) && !is_sanctuary(p)) { mon->foe = MHITYOU; return (true); } if (monsters *foe = monster_at(p)) { if (foe != mon && mon_can_see_monster(mon, foe) && (friendly || !is_sanctuary(p)) && (mons_friendly(foe) != friendly || (neutral && !mons_neutral(foe)))) { mon->foe = foe->mindex(); return (true); } } return (false); } // Choose random nearest monster as a foe. (Used for berserking monsters.) void _set_nearest_monster_foe(monsters *mon) { const bool friendly = mons_friendly_real(mon); const bool neutral = mons_neutral(mon); for (int k = 1; k <= LOS_RADIUS; ++k) { int count = 0; bool success = false; for (int i = -k; i <= k; ++i) for (int j = -k; j <= k; (abs(i) == k ? j++ : j += 2*k)) { const coord_def p = mon->pos() + coord_def(i, j); if (one_chance_in(++count) && _mons_check_set_foe(mon, p, friendly, neutral)) { success = true; } } if (success) break; } } // The default suitable() function for choose_random_nearby_monster(). bool choose_any_monster(const monsters* mon) { return (true); } // Find a nearby monster and return its index, including you as a // possibility with probability weight. suitable() should return true // for the type of monster wanted. // If prefer_named is true, named monsters (including uniques) are twice // as likely to get chosen compared to non-named ones. // If prefer_priest is true, priestly monsters (including uniques) are // twice as likely to get chosen compared to non-priestly ones. monsters *choose_random_nearby_monster(int weight, bool (*suitable)(const monsters* mon), bool in_sight, bool prefer_named, bool prefer_priest) { return choose_random_monster_on_level(weight, suitable, in_sight, true, prefer_named, prefer_priest); } monsters *choose_random_monster_on_level(int weight, bool (*suitable)(const monsters* mon), bool in_sight, bool near_by, bool prefer_named, bool prefer_priest) { monsters *chosen = NULL; // A radius_iterator with radius == max(GXM, GYM) will sweep the // whole level. radius_iterator ri(you.pos(), near_by ? 9 : std::max(GXM, GYM), true, in_sight); for (; ri; ++ri) { if (monsters *mon = monster_at(*ri)) { if (suitable(mon)) { // FIXME: if the intent is to favour monsters // named by $DEITY, we should set a flag on the // monster (something like MF_DEITY_PREFERRED) and // use that instead of checking the name, given // that other monsters can also have names. // True, but it's currently only used for orcs, and // Blork and Urug also being preferred to non-named orcs // is fine, I think. Once more gods name followers (and // prefer them) that should be changed, of course. (jpeg) // Named or priestly monsters have doubled chances. int mon_weight = 1; if (prefer_named && mon->is_named()) mon_weight++; if (prefer_priest && mons_class_flag(mon->type, M_PRIEST)) mon_weight++; if (x_chance_in_y(mon_weight, (weight += mon_weight))) chosen = mon; } } } return chosen; } // Note that this function *completely* blocks messaging for monsters // distant or invisible to the player ... look elsewhere for a function // permitting output of "It" messages for the invisible {dlb} // Intentionally avoids info and str_pass now. -- bwr bool simple_monster_message(const monsters *monster, const char *event, msg_channel_type channel, int param, description_level_type descrip) { if ((mons_near(monster) || crawl_state.arena) && (channel == MSGCH_MONSTER_SPELL || channel == MSGCH_FRIEND_SPELL || player_monster_visible(monster) || crawl_state.arena)) { std::string msg = monster->name(descrip); msg += event; msg = apostrophise_fixup(msg); if (channel == MSGCH_PLAIN && mons_wont_attack_real(monster)) channel = MSGCH_FRIEND_ACTION; mpr(msg.c_str(), channel, param); 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(); } } // 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); } // 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); } //--------------------------------------------------------------- // // 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_grid(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) && grid_is_rock(grd(t)) && !grid_is_permarock(grd(t))) updown = true; t = monster->pos() + coord_def(0, mmov.y); if (in_bounds(t) && grid_is_rock(grd(t)) && !grid_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) && grid_is_rock(grd(t)) && !grid_is_permarock(grd(t))) left = true; t = monster->pos() + coord_def(1, mmov.y); if (in_bounds(t) && grid_is_rock(grd(t)) && !grid_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) && grid_is_rock(grd(t)) && !grid_is_permarock(grd(t))) up = true; t = monster->pos() + coord_def(mmov.x, 1); if (in_bounds(t) && grid_is_rock(grd(t)) && !grid_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_grid(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_grid(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; } static void _make_mons_stop_fleeing(monsters *mon) { if (mons_is_fleeing(mon)) behaviour_event(mon, ME_CORNERED); } static bool _is_player_or_mon_sanct(const monsters* monster) { return (is_sanctuary(you.pos()) || is_sanctuary(monster->pos())); } bool mons_avoids_cloud(const monsters *monster, cloud_type cl_type, bool placement) { bool extra_careful = placement; if (placement) extra_careful = true; switch (cl_type) { case CLOUD_MIASMA: // Even the dumbest monsters will avoid miasma if they can. return (mons_res_miasma(monster) <= 0); case CLOUD_FIRE: if (mons_res_fire(monster) > 1) return (false); if (extra_careful) return (true); if (monster->hit_points >= 15 + random2avg(46, 5)) return (false); break; case CLOUD_STINK: if (mons_res_poison(monster) > 0) return (false); if (extra_careful) return (true); if (mons_intel(monster) >= I_ANIMAL && mons_res_poison(monster) < 0) return (true); if (x_chance_in_y(monster->hit_dice - 1, 5)) return (false); if (monster->hit_points >= random2avg(19, 2)) return (false); break; case CLOUD_COLD: if (mons_res_cold(monster) > 1) return (false); if (extra_careful) return (true); if (mons_intel(monster) >= I_ANIMAL && mons_res_fire(monster) < 0) return (true); if (mons_intel(monster) >= I_ANIMAL && mons_res_cold(monster) < 0) return (true); if (monster->hit_points >= 15 + random2avg(46, 5)) return (false); break; case CLOUD_POISON: if (mons_res_poison(monster) > 0) return (false); if (extra_careful) return (true); if (mons_intel(monster) >= I_ANIMAL && mons_res_poison(monster) < 0) return (true); if (monster->hit_points >= random2avg(37, 4)) return (false); break; case CLOUD_GREY_SMOKE: if (placement) return (false); // This isn't harmful, but dumb critters might think so. if (mons_intel(monster) > I_ANIMAL || coinflip()) return (false); if (mons_res_fire(monster) > 0) return (false); if (monster->hit_points >= random2avg(19, 2)) return (false); break; default: break; } // Exceedingly dumb creatures will wander into harmful clouds. if (is_harmless_cloud(cl_type) || mons_intel(monster) == I_PLANT && !extra_careful) { return (false); } // If we get here, the cloud is potentially harmful. return (true); } // Like the above, but allow a monster to move from one damaging cloud // to another, even if they're of different types. bool mons_avoids_cloud(const monsters *monster, int cloud_num, cloud_type *cl_type, bool placement) { if (cloud_num == EMPTY_CLOUD) { if (cl_type != NULL) *cl_type = CLOUD_NONE; return (false); } const cloud_struct &cloud = env.cloud[cloud_num]; if (cl_type != NULL) *cl_type = cloud.type; // Is the target cloud okay? if (!mons_avoids_cloud(monster, cloud.type, placement)) return (false); // If we're already in a cloud that we'd want to avoid then moving // from one to the other is okay. if (!in_bounds(monster->pos()) || monster->pos() == cloud.pos) return (true); const int our_cloud_num = env.cgrid(monster->pos()); if (our_cloud_num == EMPTY_CLOUD) return (true); const cloud_struct &our_cloud = env.cloud[our_cloud_num]; return (!mons_avoids_cloud(monster, our_cloud.type, true)); } //--------------------------------------------------------------- // // handle_nearby_ability // // Gives monsters a chance to use a special ability when they're // next to the player. // //--------------------------------------------------------------- static void _handle_nearby_ability(monsters *monster) { actor *foe = monster->get_foe(); if (!foe || !mons_near(monster, monster->foe) || !monster->can_see(foe) || mons_is_sleeping(monster) || mons_is_submerged(monster)) { return; } #define MON_SPEAK_CHANCE 21 if (monster->is_patrolling() || mons_is_wandering(monster) || monster->attitude == ATT_NEUTRAL) { // Very fast wandering/patrolling monsters might, in one monster turn, // move into the player's LOS and then back out (or the player // might move into their LOS and the monster move back out before // the player's view has a chance to update) so prevent them // from speaking. ; } else if ((mons_class_flag(monster->type, M_SPEAKS) || !monster->mname.empty()) && one_chance_in(MON_SPEAK_CHANCE)) { mons_speaks(monster); } else if (get_mon_shape(monster) >= MON_SHAPE_QUADRUPED) { // Non-humanoid-ish monsters have a low chance of speaking // without the M_SPEAKS flag, to give the dungeon some // atmosphere/flavour. int chance = MON_SPEAK_CHANCE * 4; // Band members are a lot less likely to speak, since there's // a lot of them. if (testbits(monster->flags, MF_BAND_MEMBER)) chance *= 10; // However, confused and fleeing monsters are more interesting. if (mons_is_fleeing(monster)) chance /= 2; if (monster->has_ench(ENCH_CONFUSION)) chance /= 2; if (one_chance_in(chance)) mons_speaks(monster); } // Okay then, don't speak. if (monster_can_submerge(monster, grd(monster->pos())) && !monster->caught() // No submerging while caught. && !player_mesmerised_by(monster) // No submerging if player entranced. && !mons_is_lurking(monster) // Handled elsewhere. && monster->wants_submerge()) { monsterentry* entry = get_monster_data(monster->type); monster->add_ench(ENCH_SUBMERGED); monster->speed_increment -= ENERGY_SUBMERGE(entry); update_beholders(monster); return; } switch (monster->type) { case MONS_SPATIAL_VORTEX: case MONS_KILLER_KLOWN: // Choose random colour. monster->colour = random_colour(); break; case MONS_GIANT_EYEBALL: if (coinflip() && !mons_is_wandering(monster) && !mons_is_fleeing(monster) && !mons_is_pacified(monster) && !_is_player_or_mon_sanct(monster)) { if (you.can_see(monster) && you.can_see(foe)) mprf("%s stares at %s.", monster->name(DESC_CAP_THE).c_str(), foe->name(DESC_NOCAP_THE).c_str()); // Subtly different from old paralysis behaviour, but // it'll do. foe->paralyse(monster, 2 + random2(3)); } break; case MONS_EYE_OF_DRAINING: if (coinflip() && foe->atype() == ACT_PLAYER && !mons_is_wandering(monster) && !mons_is_fleeing(monster) && !mons_is_pacified(monster) && !_is_player_or_mon_sanct(monster)) { simple_monster_message(monster, " stares at you."); dec_mp(5 + random2avg(13, 3)); heal_monster(monster, 10, true); // heh heh {dlb} } break; case MONS_AIR_ELEMENTAL: if (one_chance_in(5)) monster->add_ench(ENCH_SUBMERGED); break; case MONS_PANDEMONIUM_DEMON: if (monster->ghost->cycle_colours) monster->colour = random_colour(); break; default: break; } } // Returns true if you resist the siren's call. static bool _siren_movement_effect(const monsters *monster) { bool do_resist = (you.attribute[ATTR_HELD] || you_resist_magic(70)); if (!do_resist) { coord_def dir(coord_def(0,0)); if (monster->pos().x < you.pos().x) dir.x = -1; else if (monster->pos().x > you.pos().x) dir.x = 1; if (monster->pos().y < you.pos().y) dir.y = -1; else if (monster->pos().y > you.pos().y) dir.y = 1; const coord_def newpos = you.pos() + dir; if (!in_bounds(newpos) || is_grid_dangerous(grd(newpos)) || !you.can_pass_through_feat(grd(newpos))) { do_resist = true; } else { bool swapping = false; monsters *mon = monster_at(newpos); if (mon) { if (mons_wont_attack(mon) && !mons_is_stationary(mon) && !mons_cannot_act(mon) && !mons_is_sleeping(mon) && swap_check(mon, you.pos(), true)) { swapping = true; } else if (!mons_is_submerged(mon)) do_resist = true; } if (!do_resist) { const coord_def oldpos = you.pos(); mprf("The pull of her song draws you forwards."); if (swapping) { if (mgrd(oldpos) != NON_MONSTER) { mprf("Something prevents you from swapping places " "with %s.", mon->name(DESC_NOCAP_THE).c_str()); return (do_resist); } int swap_mon = mgrd(newpos); // Pick the monster up. mgrd(newpos) = NON_MONSTER; mon->moveto(oldpos); // Plunk it down. mgrd(mon->pos()) = swap_mon; mprf("You swap places with %s.", mon->name(DESC_NOCAP_THE).c_str()); } move_player_to_grid(newpos, true, true, true); if (swapping) mon->apply_location_effects(newpos); } } } return (do_resist); } //--------------------------------------------------------------- // // handle_special_ability // //--------------------------------------------------------------- static bool _handle_special_ability(monsters *monster, bolt & beem) { bool used = false; const monster_type mclass = (mons_genus( monster->type ) == MONS_DRACONIAN) ? draco_subspecies( monster ) : static_cast( monster->type ); if (!mons_near(monster) || mons_is_sleeping(monster) || mons_is_submerged(monster)) { return (false); } const msg_channel_type spl = (mons_friendly(monster) ? MSGCH_FRIEND_SPELL : MSGCH_MONSTER_SPELL); spell_type spell = SPELL_NO_SPELL; switch (mclass) { case MONS_ORC_KNIGHT: case MONS_ORC_WARLORD: case MONS_SAINT_ROKA: if (is_sanctuary(monster->pos())) break; used = orc_battle_cry(monster); break; case MONS_ORANGE_STATUE: if (_is_player_or_mon_sanct(monster)) break; used = orange_statue_effects(monster); break; case MONS_SILVER_STATUE: if (_is_player_or_mon_sanct(monster)) { break; } used = silver_statue_effects(monster); break; case MONS_BALL_LIGHTNING: if (is_sanctuary(monster->pos())) break; if (monster->attitude == ATT_HOSTILE && distance(you.pos(), monster->pos()) <= 5) { monster->hit_points = -1; used = true; break; } for (int i = 0; i < MAX_MONSTERS; i++) { monsters *targ = &menv[i]; if (targ->type == -1 || targ->type == MONS_PROGRAM_BUG) continue; if (distance(monster->pos(), targ->pos()) >= 5) continue; if (mons_atts_aligned(monster->attitude, targ->attitude)) continue; // Faking LOS by checking the neighbouring square. coord_def diff = targ->pos() - monster->pos(); coord_def sg(sgn(diff.x), sgn(diff.y)); coord_def t = monster->pos() + sg; if (!inside_level_bounds(t)) continue; if (!grid_is_solid(grd(t))) { monster->hit_points = -1; used = true; break; } } break; case MONS_LAVA_SNAKE: if (monster->has_ench(ENCH_CONFUSION)) break; if (!mons_player_visible(monster)) break; if (coinflip()) break; // Setup tracer. beem.name = "glob of lava"; beem.aux_source = "glob of lava"; beem.range = 6; beem.damage = dice_def(3, 10); beem.hit = 20; beem.colour = RED; beem.type = dchar_glyph(DCHAR_FIRED_ZAP); beem.flavour = BEAM_LAVA; beem.beam_source = monster_index(monster); beem.thrower = KILL_MON; // Fire tracer. fire_tracer(monster, beem); // Good idea? if (mons_should_fire(beem)) { _make_mons_stop_fleeing(monster); simple_monster_message(monster, " spits lava!"); beem.fire(); used = true; } break; case MONS_ELECTRICAL_EEL: if (monster->has_ench(ENCH_CONFUSION)) break; if (!mons_player_visible( monster )) break; if (coinflip()) break; // Setup tracer. beem.name = "bolt of electricity"; beem.aux_source = "bolt of electricity"; beem.range = 8; beem.damage = dice_def( 3, 6 ); beem.hit = 50; beem.colour = LIGHTCYAN; beem.type = dchar_glyph(DCHAR_FIRED_ZAP); beem.flavour = BEAM_ELECTRICITY; beem.beam_source = monster_index(monster); beem.thrower = KILL_MON; beem.is_beam = true; // Fire tracer. fire_tracer(monster, beem); // Good idea? if (mons_should_fire(beem)) { _make_mons_stop_fleeing(monster); simple_monster_message(monster, " shoots out a bolt of electricity!"); beem.fire(); used = true; } break; case MONS_ACID_BLOB: case MONS_OKLOB_PLANT: case MONS_YELLOW_DRACONIAN: if (monster->has_ench(ENCH_CONFUSION)) break; if (_is_player_or_mon_sanct(monster)) break; if (one_chance_in(3)) { spell = SPELL_ACID_SPLASH; setup_mons_cast(monster, beem, spell); // Fire tracer. fire_tracer(monster, beem); // Good idea? if (mons_should_fire(beem)) { _make_mons_stop_fleeing(monster); mons_cast(monster, beem, spell); mmov.reset(); used = true; } } break; case MONS_MOTH_OF_WRATH: if (one_chance_in(3)) used = moth_incite_monsters(monster); break; case MONS_SNORG: if (monster->has_ench(ENCH_CONFUSION)) break; if (monster->foe == MHITNOT || monster->foe == MHITYOU && mons_friendly(monster)) { break; } // There's a 5% chance of Snorg spontaneously going berserk that // increases to 20% once he is wounded. if (monster->hit_points == monster->max_hit_points && !one_chance_in(4)) break; if (one_chance_in(5)) monster->go_berserk(true); break; case MONS_PIT_FIEND: if (one_chance_in(3)) break; // deliberate fall through case MONS_FIEND: if (monster->has_ench(ENCH_CONFUSION)) break; if (_is_player_or_mon_sanct(monster)) break; // Friendly fiends won't use torment, preferring hellfire // (right now there is no way a monster can predict how // badly they'll damage the player with torment) -- GDL // Well, I guess you could allow it if the player is torment // resistant, but there's a very good reason torment resistant // players can't cast Torment themselves, and allowing your // allies to cast it would just introduce harmless Torment // through the backdoor. Thus, shouldn't happen. (jpeg) if (one_chance_in(4)) { spell_type spell_cast = SPELL_NO_SPELL; switch (random2(4)) { case 0: if (!mons_friendly(monster)) { _make_mons_stop_fleeing(monster); spell_cast = SPELL_SYMBOL_OF_TORMENT; mons_cast(monster, beem, spell_cast); used = true; break; } // deliberate fallthrough -- see above case 1: case 2: case 3: spell_cast = SPELL_HELLFIRE; setup_mons_cast(monster, beem, spell_cast); // Fire tracer. fire_tracer(monster, beem); // Good idea? if (mons_should_fire(beem)) { _make_mons_stop_fleeing(monster); mons_cast(monster, beem, spell_cast); used = true; } break; } mmov.reset(); } break; case MONS_IMP: case MONS_PHANTOM: case MONS_INSUBSTANTIAL_WISP: case MONS_BLINK_FROG: case MONS_KILLER_KLOWN: case MONS_PRINCE_RIBBIT: if (one_chance_in(7) || mons_is_caught(monster) && one_chance_in(3)) used = monster_blink(monster); break; case MONS_MANTICORE: if (monster->has_ench(ENCH_CONFUSION)) break; if (!mons_player_visible( monster )) break; // The fewer spikes the manticore has left, the less // likely it will use them. if (random2(16) >= static_cast(monster->number)) break; // Do the throwing right here, since the beam is so // easy to set up and doesn't involve inventory. // Set up the beam. beem.name = "volley of spikes"; beem.aux_source = "volley of spikes"; beem.range = 6; beem.hit = 14; beem.damage = dice_def( 2, 10 ); beem.beam_source = monster_index(monster); beem.type = dchar_glyph(DCHAR_FIRED_MISSILE); beem.colour = LIGHTGREY; beem.flavour = BEAM_MISSILE; beem.thrower = KILL_MON; beem.is_beam = false; // Fire tracer. fire_tracer(monster, beem); // Good idea? if (mons_should_fire(beem)) { _make_mons_stop_fleeing(monster); simple_monster_message(monster, " flicks its tail!"); beem.fire(); used = true; // Decrement # of volleys left. monster->number--; } break; case MONS_PLAYER_GHOST: { const ghost_demon &ghost = *(monster->ghost); if (ghost.species < SP_RED_DRACONIAN || ghost.species == SP_GREY_DRACONIAN || ghost.species >= SP_BASE_DRACONIAN || ghost.xl < 7 || one_chance_in(ghost.xl - 5)) { break; } } // Intentional fallthrough case MONS_WHITE_DRACONIAN: case MONS_RED_DRACONIAN: spell = SPELL_DRACONIAN_BREATH; // Intentional fallthrough case MONS_ICE_DRAGON: if (spell == SPELL_NO_SPELL) spell = SPELL_COLD_BREATH; // Intentional fallthrough // Dragon breath weapons: case MONS_DRAGON: case MONS_HELL_HOUND: case MONS_LINDWURM: case MONS_FIREDRAKE: case MONS_XTAHUA: if (spell == SPELL_NO_SPELL) spell = SPELL_FIRE_BREATH; if (monster->has_ench(ENCH_CONFUSION)) break; if (!mons_player_visible( monster )) break; if (monster->type != MONS_HELL_HOUND && x_chance_in_y(3, 13) || one_chance_in(10)) { setup_mons_cast(monster, beem, spell); // Fire tracer. fire_tracer(monster, beem); // Good idea? if (mons_should_fire(beem)) { _make_mons_stop_fleeing(monster); mons_cast(monster, beem, spell); mmov.reset(); used = true; } } break; case MONS_MERMAID: case MONS_SIREN: { // Don't behold observer in the arena. if (crawl_state.arena) break; // Don't behold player already half down or up the stairs. if (!you.delay_queue.empty()) { delay_queue_item delay = you.delay_queue.front(); if (delay.type == DELAY_ASCENDING_STAIRS || delay.type == DELAY_DESCENDING_STAIRS) { #ifdef DEBUG_DIAGNOSTICS mpr("Taking stairs, don't mesmerise.", MSGCH_DIAGNOSTICS); #endif break; } } // Won't sing if either of you silenced, or it's friendly, // confused, fleeing, or leaving the level. if (monster->has_ench(ENCH_CONFUSION) || mons_is_fleeing(monster) || mons_is_pacified(monster) || mons_friendly(monster) || !player_can_hear(monster->pos())) { break; } // Don't even try on berserkers. Mermaids know their limits. if (you.duration[DUR_BERSERKER]) break; // Reduce probability because of spamminess. if (you.species == SP_MERFOLK && !one_chance_in(4)) break; // A wounded invisible mermaid is less likely to give away her position. if (monster->invisible() && monster->hit_points <= monster->max_hit_points / 2 && !one_chance_in(3)) { break; } bool already_mesmerised = player_mesmerised_by(monster); if (one_chance_in(5) || monster->foe == MHITYOU && !already_mesmerised && coinflip()) { noisy(12, monster->pos(), NULL, true); bool did_resist = false; if (you.can_see(monster)) { simple_monster_message(monster, make_stringf(" chants %s song.", already_mesmerised ? "her luring" : "a haunting").c_str(), spl); if (monster->type == MONS_SIREN) { if (_siren_movement_effect(monster)) { canned_msg(MSG_YOU_RESIST); // flavour only did_resist = true; } } } else { // If you're already mesmerised by an invisible mermaid she // can still prolong the enchantment; otherwise you "resist". if (already_mesmerised) mpr("You hear a luring song.", MSGCH_SOUND); else { if (one_chance_in(4)) // reduce spamminess { if (coinflip()) mpr("You hear a haunting song.", MSGCH_SOUND); else mpr("You hear an eerie melody.", MSGCH_SOUND); canned_msg(MSG_YOU_RESIST); // flavour only } break; } } // Once mesmerised by a particular monster, you cannot resist // anymore. if (!already_mesmerised && (you.species == SP_MERFOLK || you_resist_magic(100))) { if (!did_resist) canned_msg(MSG_YOU_RESIST); break; } if (!you.duration[DUR_MESMERISED]) { you.duration[DUR_MESMERISED] = 7; you.mesmerised_by.push_back(monster_index(monster)); mprf(MSGCH_WARN, "You are mesmerised by %s!", monster->name(DESC_NOCAP_THE).c_str()); } else { you.duration[DUR_MESMERISED] += 5; if (!already_mesmerised) you.mesmerised_by.push_back(monster_index(monster)); } used = true; if (you.duration[DUR_MESMERISED] > 12) you.duration[DUR_MESMERISED] = 12; } break; } default: break; } if (used) monster->lose_energy(EUT_SPECIAL); return (used); } //--------------------------------------------------------------- // // _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 (mons_is_sleeping(monster) || 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 (mons_is_submerged(monster)) 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) { // This check isn't redundant -- player may be invisible. if (monster->target == you.pos() && see_grid_no_trans(monster->pos()) && grid_distance(monster->pos(), you.pos()) == 2) { 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_grid(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 (mons_is_sleeping(monster) || mons_is_confused(monster) || mons_is_submerged(monster) || monster->inv[MSLOT_SCROLL] == NON_ITEM || !one_chance_in(3)) { 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) || mons_is_sleeping(monster) || 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) || player_see_invis(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); } // Returns a suitable breath weapon for the draconian; does not handle all // draconians, does fire a tracer. static spell_type _get_draconian_breath_spell( monsters *monster ) { spell_type draco_breath = SPELL_NO_SPELL; if (mons_genus( monster->type ) == MONS_DRACONIAN) { switch (draco_subspecies( monster )) { case MONS_DRACONIAN: case MONS_YELLOW_DRACONIAN: // already handled as ability break; default: draco_breath = SPELL_DRACONIAN_BREATH; break; } } if (draco_breath != SPELL_NO_SPELL) { // [ds] Check line-of-fire here. It won't happen elsewhere. bolt beem; setup_mons_cast(monster, beem, draco_breath); fire_tracer(monster, beem); if (!mons_should_fire(beem)) draco_breath = SPELL_NO_SPELL; } return (draco_breath); } static bool _is_emergency_spell(const monster_spells &msp, int spell) { // If the emergency spell appears early, it's probably not a dedicated // escape spell. for (int i = 0; i < 5; ++i) if (msp[i] == spell) return (false); return (msp[5] == spell); } static bool _mon_has_spells(monsters *monster) { for (int i = 0; i < NUM_MONSTER_SPELL_SLOTS; ++i) if (monster->spells[i] != SPELL_NO_SPELL) return (true); return (false); } //--------------------------------------------------------------- // // handle_spell // // Give the monster a chance to cast a spell. Returns true if // a spell was cast. // //--------------------------------------------------------------- static bool _handle_spell(monsters *monster, bolt &beem) { bool monsterNearby = mons_near(monster); bool finalAnswer = false; // as in: "Is that your...?" {dlb} const spell_type draco_breath = _get_draconian_breath_spell(monster); // A polymorphed unique will retain his or her spells even in another // form. If the new form has the SPELLCASTER flag, casting happens as // normally, otherwise we need to enforce it, but it only happens with // a 50% chance. const bool spellcasting_poly = !mons_class_flag(monster->type, M_SPELLCASTER) && mons_class_flag(monster->type, M_SPEAKS) && _mon_has_spells(monster); if (is_sanctuary(monster->pos()) && !mons_wont_attack(monster)) return (false); // Yes, there is a logic to this ordering {dlb}: if (mons_is_sleeping(monster) || mons_is_submerged(monster) || !mons_class_flag(monster->type, M_SPELLCASTER) && !spellcasting_poly && draco_breath == SPELL_NO_SPELL) { return (false); } // If the monster's a priest, assume summons come from priestly // abilities, in which case they'll have the same god. If the // monster is neither a priest nor a wizard, assume summons come // from intrinsic abilities, in which case they'll also have the // same god. const bool priest = mons_class_flag(monster->type, M_PRIEST); const bool wizard = mons_class_flag(monster->type, M_ACTUAL_SPELLS); god_type god = (priest || !(priest || wizard)) ? monster->god : GOD_NO_GOD; if (silenced(monster->pos()) && (priest || wizard || spellcasting_poly || mons_class_flag(monster->type, M_SPELL_NO_SILENT))) { return (false); } // Shapeshifters don't get spells. if (mons_is_shapeshifter(monster) && (priest || wizard)) return (false); else if (monster->has_ench(ENCH_CONFUSION) && !mons_class_flag(monster->type, M_CONFUSED)) { return (false); } else if (monster->type == MONS_PANDEMONIUM_DEMON && !monster->ghost->spellcaster) { return (false); } else if (random2(200) > monster->hit_dice + 50 || monster->type == MONS_BALL_LIGHTNING && coinflip()) { return (false); } else if (spellcasting_poly && coinflip()) // 50% chance of not casting return (false); else { spell_type spell_cast = SPELL_NO_SPELL; monster_spells hspell_pass(monster->spells); if (!mon_enemies_around(monster)) { // Force the casting of dig when the player is not visible - // this is EVIL! if (monster->has_spell(SPELL_DIG) && mons_is_seeking(monster)) { spell_cast = SPELL_DIG; finalAnswer = true; } else if ((monster->has_spell(SPELL_MINOR_HEALING) || monster->has_spell(SPELL_MAJOR_HEALING)) && monster->hit_points < monster->max_hit_points) { // The player's out of sight! // Quick, let's take a turn to heal ourselves. -- bwr spell_cast = monster->has_spell(SPELL_MAJOR_HEALING) ? SPELL_MAJOR_HEALING : SPELL_MINOR_HEALING; finalAnswer = true; } else if (mons_is_fleeing(monster) || mons_is_pacified(monster)) { // Since the player isn't around, we'll extend the monster's // normal choices to include the self-enchant slot. int foundcount = 0; for (int i = NUM_MONSTER_SPELL_SLOTS - 1; i >= 0; --i) { if (ms_useful_fleeing_out_of_sight(monster, hspell_pass[i]) && one_chance_in(++foundcount)) { spell_cast = hspell_pass[i]; finalAnswer = true; } } } else if (monster->foe == MHITYOU && !monsterNearby) return (false); } // Monsters caught in a net try to get away. // This is only urgent if enemies are around. if (!finalAnswer && mon_enemies_around(monster) && mons_is_caught(monster) && one_chance_in(4)) { for (int i = 0; i < NUM_MONSTER_SPELL_SLOTS; ++i) { if (ms_quick_get_away(monster, hspell_pass[i])) { spell_cast = hspell_pass[i]; finalAnswer = true; break; } } } // Promote the casting of useful spells for low-HP monsters. if (!finalAnswer && monster->hit_points < monster->max_hit_points / 4 && !one_chance_in(4)) { // Note: There should always be at least some chance we don't // get here... even if the monster is on its last HP. That // way we don't have to worry about monsters infinitely casting // Healing on themselves (e.g. orc high priests). if ((mons_is_fleeing(monster) || mons_is_pacified(monster)) && ms_low_hitpoint_cast(monster, hspell_pass[5])) { spell_cast = hspell_pass[5]; finalAnswer = true; } if (!finalAnswer) { int found_spell = 0; for (int i = 0; i < NUM_MONSTER_SPELL_SLOTS; ++i) { if (ms_low_hitpoint_cast(monster, hspell_pass[i]) && one_chance_in(++found_spell)) { spell_cast = hspell_pass[i]; finalAnswer = true; } } } } if (!finalAnswer) { // If nothing found by now, safe friendlies and good // neutrals will rarely cast. if (mons_wont_attack(monster) && !mon_enemies_around(monster) && !one_chance_in(10)) { return (false); } // Remove healing/invis/haste if we don't need them. int num_no_spell = 0; for (int i = 0; i < NUM_MONSTER_SPELL_SLOTS; ++i) { if (hspell_pass[i] == SPELL_NO_SPELL) num_no_spell++; else if (ms_waste_of_time(monster, hspell_pass[i]) || hspell_pass[i] == SPELL_DIG) { // Should monster not have selected dig by now, // it never will. hspell_pass[i] = SPELL_NO_SPELL; num_no_spell++; } } // If no useful spells... cast no spell. if (num_no_spell == NUM_MONSTER_SPELL_SLOTS && draco_breath == SPELL_NO_SPELL) { return (false); } const bolt orig_beem = beem; // Up to four tries to pick a spell. for (int loopy = 0; loopy < 4; ++loopy) { beem = orig_beem; bool spellOK = false; // Setup spell - monsters that are fleeing or pacified // and leaving the level will always try to choose their // emergency spell. if (mons_is_fleeing(monster) || mons_is_pacified(monster)) { spell_cast = (one_chance_in(5) ? SPELL_NO_SPELL : hspell_pass[5]); // Pacified monsters leaving the level won't choose // emergency spells harmful to the area. if (spell_cast != SPELL_NO_SPELL && mons_is_pacified(monster) && spell_harms_area(spell_cast)) { spell_cast = SPELL_NO_SPELL; } } else { // Randomly picking one of the non-emergency spells: spell_cast = hspell_pass[random2(5)]; } if (spell_cast == SPELL_NO_SPELL) continue; // Setup the spell. setup_mons_cast(monster, beem, spell_cast); // beam-type spells requiring tracers if (spell_needs_tracer(spell_cast)) { const bool explode = spell_is_direct_explosion(spell_cast); fire_tracer(monster, beem, explode); // Good idea? if (mons_should_fire(beem)) spellOK = true; } else { // All direct-effect/summoning/self-enchantments/etc. spellOK = true; if (ms_direct_nasty(spell_cast) && mons_aligned(monster_index(monster), monster->foe)) { spellOK = false; } else if (monster->foe == MHITYOU || monster->foe == MHITNOT) { // XXX: Note the crude hack so that monsters can // use ME_ALERT to target (we should really have // a measure of time instead of peeking to see // if the player is still there). -- bwr if (!mons_player_visible(monster) && (monster->target != you.pos() || coinflip())) { spellOK = false; } } else if (!mon_can_see_monster(monster, &menv[monster->foe])) { spellOK = false; } else if (monster->type == MONS_DAEVA && monster->god == GOD_SHINING_ONE) { const monsters *mon = &menv[monster->foe]; // Don't allow TSO-worshipping daevas to make // unchivalric magic attacks, except against // appropriate monsters. if (is_unchivalric_attack(monster, mon) && !tso_unchivalric_attack_safe_monster(mon)) { spellOK = false; } } } // If not okay, then maybe we'll cast a defensive spell. if (!spellOK) { spell_cast = (coinflip() ? hspell_pass[2] : SPELL_NO_SPELL); } if (spell_cast != SPELL_NO_SPELL) break; } } // If there's otherwise no ranged attack use the breath weapon. // The breath weapon is also occasionally used. if (draco_breath != SPELL_NO_SPELL && (spell_cast == SPELL_NO_SPELL || !_is_emergency_spell(hspell_pass, spell_cast) && one_chance_in(4)) && !_is_player_or_mon_sanct(monster)) { spell_cast = draco_breath; finalAnswer = true; } // Should the monster *still* not have a spell, well, too bad {dlb}: if (spell_cast == SPELL_NO_SPELL) return (false); // Friendly monsters don't use polymorph other, for fear of harming // the player. if (spell_cast == SPELL_POLYMORPH_OTHER && mons_friendly(monster)) return (false); // Try to animate dead: if nothing rises, pretend we didn't cast it. if (spell_cast == SPELL_ANIMATE_DEAD && !animate_dead(monster, 100, SAME_ATTITUDE(monster), monster->foe, god, false)) { return (false); } if (monster->type == MONS_BALL_LIGHTNING) monster->hit_points = -1; // FINALLY! determine primary spell effects {dlb}: if (spell_cast == SPELL_BLINK || spell_cast == SPELL_CONTROLLED_BLINK) { // Why only cast blink if nearby? {dlb} if (monsterNearby) { mons_cast_noise(monster, beem, spell_cast); monster_blink(monster); monster->lose_energy(EUT_SPELL); } else return (false); } else { if (spell_needs_foe(spell_cast)) _make_mons_stop_fleeing(monster); mons_cast(monster, beem, spell_cast); mmov.reset(); monster->lose_energy(EUT_SPELL); } } // end "if mons_class_flag(monster->type, M_SPELLCASTER) ... return (true); } // Returns a rough estimate of damage from throwing the wielded weapon. int mons_thrown_weapon_damage(const item_def *weap) { if (!weap || get_weapon_brand(*weap) != SPWPN_RETURNING) return (0); return std::max(0, (property(*weap, PWPN_DAMAGE) + weap->plus2 / 2)); } int mons_weapon_damage_rating(const item_def &launcher) { return (property(launcher, PWPN_DAMAGE) + launcher.plus2); } // Returns a rough estimate of damage from firing/throwing missile. int mons_missile_damage(monsters *mons, const item_def *launch, const item_def *missile) { if (!missile || (!launch && !is_throwable(mons, *missile))) return (0); const int missile_damage = property(*missile, PWPN_DAMAGE) / 2 + 1; const int launch_damage = launch? property(*launch, PWPN_DAMAGE) : 0; return std::max(0, launch_damage + missile_damage); } // Given the monster's current weapon and alt weapon (either or both of // which may be NULL), works out whether using missiles or throwing the // main weapon (with returning brand) is better. If using missiles that // need a launcher, sets *launcher to the launcher. // // If the monster has no ranged weapon attack, returns NON_ITEM. // int mons_pick_best_missile(monsters *mons, item_def **launcher, bool ignore_melee) { *launcher = NULL; item_def *melee = NULL, *launch = NULL; for (int i = MSLOT_WEAPON; i <= MSLOT_ALT_WEAPON; ++i) { if (item_def *item = mons->mslot_item(static_cast(i))) { if (is_range_weapon(*item)) launch = item; else if (!ignore_melee) melee = item; } } const item_def *missiles = mons->missiles(); if (launch && missiles && !missiles->launched_by(*launch)) launch = NULL; const int tdam = mons_thrown_weapon_damage(melee); const int fdam = mons_missile_damage(mons, launch, missiles); if (!tdam && !fdam) return (NON_ITEM); else if (tdam >= fdam) return (melee->index()); else { *launcher = launch; return (missiles->index()); } } //--------------------------------------------------------------- // // 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 (_is_player_or_mon_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); } static bool _handle_monster_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_spell(monster, beem)) return (true); } 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; } } int mons_natural_regen_rate(monsters *monster) { // A HD divider ranging from 3 (at 1 HD) to 1 (at 8 HD). int divider = std::max(div_rand_round(15 - monster->hit_dice, 4), 1); // The undead have a harder time regenerating. Golems have it worse. switch (monster->holiness()) { case MH_UNDEAD: divider *= (mons_enslaved_soul(monster)) ? 2 : 4; break; // And golems have it worse. case MH_NONLIVING: divider *= 5; break; default: break; } return (std::max(div_rand_round(monster->hit_dice, divider), 1)); } 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 && grid_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 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 ); } #if 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 && !mons_is_submerged(monster)) { _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 == -1) 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) { #if 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 (mons_is_submerged(monster)) { monster->speed_increment -= entry->energy_usage.swim; break; } if (monster->type == -1) { monster->speed_increment -= entry->energy_usage.move; break; // problem with vortices } } _mons_in_cloud(monster); if (monster->type == -1) { 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; } // Harpies may eat food/corpses on the ground. if (monster->type == MONS_HARPY && !mons_is_fleeing(monster) && (mons_wont_attack(monster) || (grid_distance(monster->pos(), you.pos()) > 1)) && (mons_is_wandering(monster) && one_chance_in(3) || one_chance_in(5)) && expose_items_to_element(BEAM_STEAL_FOOD, monster->pos(), 10)) { simple_monster_message(monster, " eats something on the ground."); monster->speed_increment -= non_move_energy; continue; } if (igrd(monster->pos()) != NON_ITEM && (mons_itemuse(monster) == MONUSE_WEAPONS_ARMOUR || mons_itemuse(monster) == MONUSE_EATS_ITEMS)) { // 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) && (!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 (mons_is_submerged(monster)) { // 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 && mons_is_submerged(monster)) { 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; } } } } _handle_nearby_ability( monster ); if (!mons_is_sleeping(monster) && !mons_is_wandering(monster) // Berserking monsters are limited to running up and // hitting their foes. && !monster->has_ench(ENCH_BERSERK)) { bolt beem; beem.source = monster->pos(); beem.target = monster->target; beem.beam_source = monster->mindex(); const bool friendly_or_near = mons_friendly(monster) || mons_near(monster, monster->foe); // Prevents unfriendlies from nuking you from offscreen. // How nice! if (friendly_or_near || monster->type == MONS_TEST_SPAWNER) { // [ds] Special abilities shouldn't overwhelm spellcasting // in monsters that have both. This aims to give them both // roughly the same weight. if (coinflip() ? _handle_special_ability(monster, beem) || _handle_monster_spell(monster, beem) : _handle_monster_spell(monster, beem) || _handle_special_ability(monster, beem)) { DEBUG_ENERGY_USE("spell or special"); 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)) { // 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())) { // 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 (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; } } 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; else if (monster->pos() == old_pos) // There was nowhere the monster could move to, so it did // nothing. 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 != -1 && 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 _is_item_jelly_edible(const item_def &item) { // Don't eat artefacts. if (is_artefact(item)) return (false); // Shouldn't eat stone things // - but what about wands and rings? if (item.base_type == OBJ_MISSILES && (item.sub_type == MI_STONE || item.sub_type == MI_LARGE_ROCK)) { return (false); } // Don't eat special game items. if (item.base_type == OBJ_ORBS || (item.base_type == OBJ_MISCELLANY && (item.sub_type == MISC_RUNE_OF_ZOT || item.sub_type == MISC_HORN_OF_GERYON))) { return (false); } return (true); } //--------------------------------------------------------------- // // handle_pickup // // Returns false if monster doesn't spend any time picking something up. // //--------------------------------------------------------------- static bool _handle_pickup(monsters *monster) { if (mons_is_sleeping(monster) || mons_is_submerged(monster)) return (false); const bool monster_nearby = mons_near(monster); if (mons_itemuse(monster) == MONUSE_EATS_ITEMS) { // Friendly jellies won't eat. if (monster->attitude != ATT_HOSTILE) 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 (quant >= si->quantity) item_was_destroyed(*si, monster->mindex()); dec_mitm_item_quantity(si.link(), quant); } if (eaten) { 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; if (monster->max_hit_points < monster->hit_points) monster->max_hit_points = monster->hit_points; if (player_can_hear(monster->pos())) { mprf(MSGCH_SOUND, "You hear a%s slurping noise.", monster_nearby ? "" : " distant"); } if (eaten_net) simple_monster_message(monster, " devours the net!"); _jelly_divide(monster); } return (false); } // Note: Monsters only look at stuff near the top of stacks. // XXX: Need to put in something so that monster picks up multiple items // (eg ammunition) identical to those it's carrying. // Monsters may now pick up up to two items in the same turn. (jpeg) int count_pickup = 0; for (stack_iterator si(monster->pos()); si; ++si) { if (monster->pickup_item(*si, monster_nearby)) count_pickup++; if (count_pickup > 1 || coinflip()) break; } return (count_pickup > 0); } 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 _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) || mons_is_sleeping(mpusher)) { 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); } 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 (mons_is_sleeping(m2)) { 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_grid(f)) simple_monster_message(monster, " moves out of view."); else if (Options.tutorial_left && (monster->flags & MF_WAS_IN_VIEW) && !see_grid(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); } void mons_check_pool(monsters *monster, const coord_def &oldpos, killer_type killer, int killnum) { // Levitating/flying monsters don't make contact with the terrain. if (monster->airborne()) return; dungeon_feature_type grid = grd(monster->pos()); if ((grid == DNGN_LAVA || grid == DNGN_DEEP_WATER) && !monster_habitable_grid(monster, grid)) { const bool message = mons_near(monster); // Don't worry about invisibility. You should be able to see if // something has fallen into the lava. if (message && (oldpos == monster->pos() || grd(oldpos) != grid)) { mprf("%s falls into the %s!", monster->name(DESC_CAP_THE).c_str(), grid == DNGN_LAVA ? "lava" : "water"); } if (grid == DNGN_LAVA && mons_res_fire(monster) >= 2) grid = DNGN_DEEP_WATER; // Even fire resistant monsters perish in lava, but inanimate // monsters can survive deep water. if (grid == DNGN_LAVA || monster->can_drown()) { if (message) { if (grid == DNGN_LAVA) { simple_monster_message(monster, " is incinerated.", MSGCH_MONSTER_DAMAGE, MDAM_DEAD); } else if (mons_genus(monster->type) == MONS_MUMMY) { simple_monster_message(monster, " falls apart.", MSGCH_MONSTER_DAMAGE, MDAM_DEAD); } else { simple_monster_message(monster, " drowns.", MSGCH_MONSTER_DAMAGE, MDAM_DEAD); } } if (killer == KILL_NONE) { // Self-kill. killer = KILL_MON; killnum = monster_index(monster); } // Yredelemnul special, redux: It's the only one that can // work on drowned monsters. if (!_yred_enslave_soul(monster, killer)) monster_die(monster, killer, killnum, true); } } } // 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_grid(dc)) { grid = grid_secret_door_appearance(dc); was_secret = true; } if (see_grid(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 _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); } // 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); // 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); // 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 && grid_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_grid(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_STEAM)) { return (false); } // Fire elementals avoid water and cold. if (monster->type == MONS_FIRE_ELEMENTAL && (grid_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 randomize 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 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 (mons_is_submerged(monster)) 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()); } else if (one_chance_in(5)) handle_monster_shouts(monster, true); else { // Just be noisy without messaging the player. noisy(noise_level, monster->pos()); } } } 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); } // 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 // (they _live_ in the dungeon!) if (grd(newpos) == DNGN_CLOSED_DOOR || grd(newpos) == DNGN_SECRET_DOOR && 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 // Jellies eat doors. Yum! if ((grd(newpos) == DNGN_CLOSED_DOOR || grd(newpos) == DNGN_OPEN_DOOR) && mons_itemuse(monster) == MONUSE_EATS_ITEMS // Doors with permarock marker cannot be eaten. && !feature_marker_at(newpos, DNGN_PERMAROCK_WALL)) { grd(newpos) = DNGN_FLOOR; _jelly_grows(monster); if (see_grid(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: 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 (mons_res_poison(monster) > 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 (mons_res_poison(monster) > 0) return; poison_monster(monster, cloud.whose); // If the monster got poisoned, wake it up. wake = true; hurted += (random2(8) * 10) / speed; if (mons_res_poison(monster) < 0) hurted += (random2(4) * 10) / speed; break; case CLOUD_STEAM: { // 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 (mons_res_miasma(monster) > 0) return; poison_monster(monster, cloud.whose); if (monster->max_hit_points > 4 && coinflip()) monster->max_hit_points--; beam.flavour = BEAM_SLOW; if (one_chance_in(3)) beam.apply_enchantment_to_monster(monster); hurted += (10 * random2avg(12, 3)) / speed; // 3 break; default: // 'harmless' clouds -- colored smoke, etc {dlb}. return; } // A sleeping monster that sustains damage will wake up. if ((wake || hurted > 0) && mons_is_sleeping(monster)) { // 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()); } } } bool monster_descriptor(int which_class, mon_desc_type which_descriptor) { if (which_descriptor == MDSC_LEAVES_HIDE) { switch (which_class) { case MONS_DRAGON: case MONS_TROLL: case MONS_ICE_DRAGON: case MONS_STEAM_DRAGON: case MONS_MOTTLED_DRAGON: case MONS_STORM_DRAGON: case MONS_GOLDEN_DRAGON: case MONS_SWAMP_DRAGON: case MONS_YAK: case MONS_SHEEP: return (true); default: return (false); } } if (which_descriptor == MDSC_REGENERATES) { switch (which_class) { case MONS_CACODEMON: case MONS_DEEP_TROLL: case MONS_HELLWING: case MONS_IMP: case MONS_IRON_TROLL: case MONS_LEMURE: case MONS_ROCK_TROLL: case MONS_SLIME_CREATURE: case MONS_SNORG: case MONS_TROLL: case MONS_HYDRA: case MONS_KILLER_KLOWN: case MONS_LERNAEAN_HYDRA: case MONS_DISSOLUTION: return (true); default: return (false); } } if (which_descriptor == MDSC_NOMSG_WOUNDS) { // Zombified monsters other than spectral things don't show // wounds. if (mons_class_is_zombified(which_class) && which_class != MONS_SPECTRAL_THING) { return (true); } switch (which_class) { case MONS_RAKSHASA: case MONS_RAKSHASA_FAKE: return (true); default: return (false); } } return (false); } bool message_current_target() { if (crawl_state.is_replaying_keys()) { if (you.prev_targ == MHITNOT || you.prev_targ == MHITYOU) return (false); return (you.can_see(&menv[you.prev_targ])); } if (you.prev_targ != MHITNOT && you.prev_targ != MHITYOU) { const monsters *montarget = &menv[you.prev_targ]; if (you.can_see(montarget)) { mprf(MSGCH_PROMPT, "Current target: %s " "(use p or f to fire at it again.)", montarget->name(DESC_PLAIN).c_str()); return (true); } mpr("You have no current target."); } return (false); } // aaah, the simple joys of pointer arithmetic! {dlb}: unsigned int monster_index(const monsters *monster) { return (monster - menv.buffer()); } bool heal_monster(monsters * patient, int health_boost, bool permit_growth) { if (mons_is_statue(patient->type)) return (false); if (health_boost < 1) return (false); else if (!permit_growth && patient->hit_points == patient->max_hit_points) return (false); patient->hit_points += health_boost; bool success = true; if (patient->hit_points > patient->max_hit_points) { if (permit_growth) { const monsterentry* m = get_monster_data(patient->type); const int maxhp = m->hpdice[0] * (m->hpdice[1] + m->hpdice[2]) + m->hpdice[3]; // Limit HP growth. if (random2(3 * maxhp) > 2 * patient->max_hit_points) patient->max_hit_points++; else success = false; } patient->hit_points = patient->max_hit_points; } return (success); } 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; } } void seen_monster(monsters *monster) { // If the monster is in the auto_exclude list, automatically // set an exclusion. set_auto_exclude(monster); // Monster was viewed this turn monster->flags |= MF_WAS_IN_VIEW; if (monster->flags & MF_SEEN) return; // First time we've seen this particular monster. monster->flags |= MF_SEEN; if (!mons_is_mimic(monster->type)) { if (Options.tutorial_left) tutorial_first_monster(*monster); if (MONST_INTERESTING(monster)) { take_note( Note(NOTE_SEEN_MONSTER, monster->type, 0, monster->name(DESC_NOCAP_A, true).c_str())); } } } //--------------------------------------------------------------- // // shift_monster // // Moves a monster to approximately p and returns true if // the monster was moved. // //--------------------------------------------------------------- bool shift_monster(monsters *mon, coord_def p) { coord_def result; int count = 0; if (p.origin()) p = mon->pos(); for (adjacent_iterator ai(p); ai; ++ai) { // Don't drop on anything but vanilla floor right now. if (grd(*ai) != DNGN_FLOOR) continue; if (actor_at(*ai)) continue; if (one_chance_in(++count)) result = *ai; } if (count > 0) { mgrd(mon->pos()) = NON_MONSTER; mon->moveto(result); mgrd(result) = mon->mindex(); } return (count > 0); }