From 204607ad3e8aecb32e01f303b1e86545d3d57f62 Mon Sep 17 00:00:00 2001 From: Matthew Cline Date: Sun, 1 Nov 2009 01:55:04 -0700 Subject: Split up monstuff.cc A lot of monstuff.cc was moved into mon-abil.cc (monster abilities), mon-act.cc (the main monster loop), mon-behv.cc (monster behaviour) and mon-cast.cc (monster spells). mstuff2.cc was completely merged into other files. --- crawl-ref/source/acr.cc | 1 + crawl-ref/source/arena.cc | 4 +- crawl-ref/source/beam.cc | 2 +- crawl-ref/source/delay.cc | 2 +- crawl-ref/source/effects.cc | 3 +- crawl-ref/source/fight.cc | 3 +- crawl-ref/source/files.cc | 2 +- crawl-ref/source/godabil.cc | 5 +- crawl-ref/source/item_use.cc | 2 +- crawl-ref/source/makefile.obj | 5 +- crawl-ref/source/mon-abil.cc | 1378 ++++++++ crawl-ref/source/mon-abil.h | 19 + crawl-ref/source/mon-act.cc | 3617 ++++++++++++++++++++ crawl-ref/source/mon-act.h | 14 + crawl-ref/source/mon-behv.cc | 1928 +++++++++++ crawl-ref/source/mon-behv.h | 52 + crawl-ref/source/mon-cast.cc | 2357 +++++++++++++ crawl-ref/source/mon-cast.h | 25 + crawl-ref/source/mon-util.cc | 7 + crawl-ref/source/mon-util.h | 13 +- crawl-ref/source/monplace.cc | 3 +- crawl-ref/source/monster.cc | 3 +- crawl-ref/source/monstuff.cc | 7385 ++++------------------------------------- crawl-ref/source/monstuff.h | 36 +- crawl-ref/source/mstuff2.cc | 3225 ------------------ crawl-ref/source/mstuff2.h | 29 - crawl-ref/source/religion.cc | 2 +- crawl-ref/source/spells2.cc | 1 + crawl-ref/source/spells3.cc | 1 + crawl-ref/source/spells4.cc | 2 +- crawl-ref/source/spl-cast.cc | 2 +- crawl-ref/source/spl-util.cc | 1 + crawl-ref/source/terrain.cc | 2 +- crawl-ref/source/tutorial.cc | 2 +- crawl-ref/source/view.cc | 1 + crawl-ref/source/xom.cc | 1 + 36 files changed, 10170 insertions(+), 9965 deletions(-) create mode 100644 crawl-ref/source/mon-abil.cc create mode 100644 crawl-ref/source/mon-abil.h create mode 100644 crawl-ref/source/mon-act.cc create mode 100644 crawl-ref/source/mon-act.h create mode 100644 crawl-ref/source/mon-behv.cc create mode 100644 crawl-ref/source/mon-behv.h create mode 100644 crawl-ref/source/mon-cast.cc create mode 100644 crawl-ref/source/mon-cast.h (limited to 'crawl-ref/source') diff --git a/crawl-ref/source/acr.cc b/crawl-ref/source/acr.cc index 75a813d937..7928d56200 100644 --- a/crawl-ref/source/acr.cc +++ b/crawl-ref/source/acr.cc @@ -82,6 +82,7 @@ #include "maps.h" #include "message.h" #include "misc.h" +#include "mon-act.h" #include "monplace.h" #include "monstuff.h" #include "mon-util.h" diff --git a/crawl-ref/source/arena.cc b/crawl-ref/source/arena.cc index bf6149f697..f4395c5336 100644 --- a/crawl-ref/source/arena.cc +++ b/crawl-ref/source/arena.cc @@ -20,11 +20,11 @@ #include "macro.h" #include "maps.h" #include "message.h" +#include "mon-behv.h" #include "mon-pick.h" #include "mon-util.h" -#include "monstuff.h" #include "monplace.h" -#include "mstuff2.h" +#include "monstuff.h" #include "spl-mis.h" #include "spl-util.h" #include "state.h" diff --git a/crawl-ref/source/beam.cc b/crawl-ref/source/beam.cc index e9e258b396..3291b82804 100644 --- a/crawl-ref/source/beam.cc +++ b/crawl-ref/source/beam.cc @@ -40,10 +40,10 @@ #include "los.h" #include "message.h" #include "misc.h" +#include "mon-behv.h" #include "monplace.h" #include "monstuff.h" #include "mon-util.h" -#include "mstuff2.h" #include "mutation.h" #include "ouch.h" #include "player.h" diff --git a/crawl-ref/source/delay.cc b/crawl-ref/source/delay.cc index 4de151f7a7..cbc7993a3f 100644 --- a/crawl-ref/source/delay.cc +++ b/crawl-ref/source/delay.cc @@ -30,9 +30,9 @@ #include "it_use2.h" #include "message.h" #include "misc.h" +#include "mon-behv.h" #include "monstuff.h" #include "mon-util.h" -#include "mstuff2.h" #include "notes.h" #include "ouch.h" #include "output.h" diff --git a/crawl-ref/source/effects.cc b/crawl-ref/source/effects.cc index 82de3a69f9..ed513fb2f6 100644 --- a/crawl-ref/source/effects.cc +++ b/crawl-ref/source/effects.cc @@ -38,10 +38,11 @@ #include "makeitem.h" #include "message.h" #include "misc.h" +#include "mon-behv.h" +#include "mon-cast.h" #include "monplace.h" #include "monstuff.h" #include "mon-util.h" -#include "mstuff2.h" #include "mutation.h" #include "notes.h" #include "ouch.h" diff --git a/crawl-ref/source/fight.cc b/crawl-ref/source/fight.cc index 058faf0cd2..33a86ca592 100644 --- a/crawl-ref/source/fight.cc +++ b/crawl-ref/source/fight.cc @@ -38,10 +38,11 @@ #include "macro.h" #include "message.h" #include "misc.h" +#include "mon-behv.h" +#include "mon-cast.h" #include "monplace.h" #include "monstuff.h" #include "mon-util.h" -#include "mstuff2.h" #include "mutation.h" #include "ouch.h" #include "player.h" diff --git a/crawl-ref/source/files.cc b/crawl-ref/source/files.cc index 30c4fa6e8f..82c308de6d 100644 --- a/crawl-ref/source/files.cc +++ b/crawl-ref/source/files.cc @@ -58,9 +58,9 @@ #include "mapmark.h" #include "message.h" #include "misc.h" +#include "mon-act.h" #include "monstuff.h" #include "mon-util.h" -#include "mstuff2.h" #include "mtransit.h" #include "newgame.h" #include "notes.h" diff --git a/crawl-ref/source/godabil.cc b/crawl-ref/source/godabil.cc index 2adfc984a6..1647678869 100644 --- a/crawl-ref/source/godabil.cc +++ b/crawl-ref/source/godabil.cc @@ -18,9 +18,10 @@ #include "kills.h" #include "message.h" #include "misc.h" -#include "mon-util.h" +#include "mon-act.h" +#include "mon-behv.h" #include "monstuff.h" -#include "mstuff2.h" +#include "mon-util.h" #include "mutation.h" #include "random.h" #include "religion.h" diff --git a/crawl-ref/source/item_use.cc b/crawl-ref/source/item_use.cc index b5393c488d..9ab4dd67cc 100644 --- a/crawl-ref/source/item_use.cc +++ b/crawl-ref/source/item_use.cc @@ -41,9 +41,9 @@ #include "macro.h" #include "message.h" #include "misc.h" +#include "mon-behv.h" #include "monplace.h" #include "monstuff.h" -#include "mstuff2.h" #include "mon-util.h" #include "notes.h" #include "ouch.h" diff --git a/crawl-ref/source/makefile.obj b/crawl-ref/source/makefile.obj index d9ffb8bd5a..3d6c68b246 100644 --- a/crawl-ref/source/makefile.obj +++ b/crawl-ref/source/makefile.obj @@ -84,6 +84,10 @@ menu.o \ message.o \ mgrow.o \ misc.o \ +mon-abil.o \ +mon-act.o \ +mon-behv.o \ +mon-cast.o \ mon-info.o \ mon-pick.o \ mon-util.o \ @@ -91,7 +95,6 @@ monplace.o \ monspeak.o \ monster.o \ monstuff.o \ -mstuff2.o \ mt19937ar.o \ mtransit.o \ mutation.o \ diff --git a/crawl-ref/source/mon-abil.cc b/crawl-ref/source/mon-abil.cc new file mode 100644 index 0000000000..ed8b22d55c --- /dev/null +++ b/crawl-ref/source/mon-abil.cc @@ -0,0 +1,1378 @@ +/* + * File: mon-abil.cc + * Summary: Monster abilities. + * Written by: Linley Henzell + */ + +#include "AppHdr.h" +#include "mon-abil.h" + +#include "externs.h" + +#ifdef TARGET_OS_DOS +#include +#endif + +#include "beam.h" +#include "colour.h" +#include "directn.h" +#include "ghost.h" +#include "misc.h" +#include "mon-act.h" +#include "mon-behv.h" +#include "mon-cast.h" +#include "monplace.h" +#include "monspeak.h" +#include "monstuff.h" +#include "random.h" +#include "spl-mis.h" +#include "spl-util.h" +#include "state.h" +#include "stuff.h" +#include "view.h" + +bool ugly_thing_mutate(monsters *ugly, bool proximity) +{ + bool success = false; + + std::string src = ""; + + unsigned char mon_colour = BLACK; + + if (!proximity) + success = true; + else if (one_chance_in(8)) + { + int you_mutate_chance = 0; + int mon_mutate_chance = 0; + + for (adjacent_iterator ri(ugly->pos()); ri; ++ri) + { + if (you.pos() == *ri) + you_mutate_chance = get_contamination_level(); + else + { + monsters *ugly_near = monster_at(*ri); + + if (!ugly_near + || ugly_near->type != MONS_UGLY_THING + && ugly_near->type != MONS_VERY_UGLY_THING) + { + continue; + } + + for (int i = 0; i < 2; ++i) + { + if (coinflip()) + { + mon_mutate_chance++; + + if (coinflip()) + { + const int ugly_colour = + make_low_colour(ugly->colour); + const int ugly_near_colour = + make_low_colour(ugly_near->colour); + + if (ugly_colour != ugly_near_colour) + mon_colour = ugly_near_colour; + } + } + + if (ugly_near->type != MONS_VERY_UGLY_THING) + break; + } + } + } + + you_mutate_chance = std::min(16, you_mutate_chance); + mon_mutate_chance = std::min(16, mon_mutate_chance); + + if (!one_chance_in(you_mutate_chance + mon_mutate_chance + 1)) + { + const bool proximity_you = + (you_mutate_chance > mon_mutate_chance) ? true : + (you_mutate_chance == mon_mutate_chance) ? coinflip() + : false; + + src = proximity_you ? " from you" : " from its kin"; + + success = true; + } + } + + if (success) + { + simple_monster_message(ugly, + make_stringf(" basks in the mutagenic energy%s and changes!", + src.c_str()).c_str()); + + ugly->uglything_mutate(mon_colour); + + return (true); + } + + return (false); +} + +// Inflict any enchantments the parent slime has on its offspring, +// leaving durations unchanged, I guess. -cao +static void _split_ench_durations(monsters *initial_slime, monsters *split_off) +{ + mon_enchant_list::iterator i; + + for (i = initial_slime->enchantments.begin(); + i != initial_slime->enchantments.end(); ++i) + { + split_off->add_ench(i->second); + } + +} + +// What to do about any enchantments these two slimes may have? For +// now, we are averaging the durations. -cao +static void _merge_ench_durations(monsters *initial_slime, monsters *merge_to) +{ + mon_enchant_list::iterator i; + + int initial_count = initial_slime->number; + int merge_to_count = merge_to->number; + int total_count = initial_count + merge_to_count; + + for (i = initial_slime->enchantments.begin(); + i != initial_slime->enchantments.end(); ++i) + { + // Does the other slime have this enchantment as well? + mon_enchant temp = merge_to->get_ench(i->first); + // If not, use duration 0 for their part of the average. + int duration = temp.ench == ENCH_NONE ? 0 : temp.duration; + + i->second.duration = (i->second.duration * initial_count + + duration * merge_to_count)/total_count; + + if (!i->second.duration) + i->second.duration = 1; + + merge_to->add_ench(i->second); + } + + for (i = merge_to->enchantments.begin(); + i != merge_to->enchantments.end(); ++i) + { + if (initial_slime->enchantments.find(i->first) + != initial_slime->enchantments.end() + && i->second.duration > 1) + { + i->second.duration = (merge_to_count * i->second.duration) + / total_count; + + merge_to->update_ench(i->second); + } + } +} + +// Calculate slime creature hp based on how many are merged. +static void _stats_from_blob_count(monsters *slime, float hp_per_blob) +{ + slime->max_hit_points = (int)(slime->number * hp_per_blob); + slime->hit_points = slime->max_hit_points; +} + +// Create a new slime creature at 'target', and split 'thing''s hp and +// merge count with the new monster. +static bool _do_split(monsters *thing, coord_def & target) +{ + // Create a new slime. + int slime_idx = create_monster(mgen_data(MONS_SLIME_CREATURE, + thing->behaviour, + 0, + 0, + target, + thing->foe, + MG_FORCE_PLACE)); + + if (slime_idx == -1) + return (false); + + monsters *new_slime = &env.mons[slime_idx]; + + // Inflict the new slime with any enchantments on the parent. + _split_ench_durations(thing, new_slime); + new_slime->attitude = thing->attitude; + new_slime->flags = thing->flags; + + if (!new_slime) + return (false); + + if (you.can_see(thing)) + mprf("%s splits.", thing->name(DESC_CAP_A).c_str()); + + int split_off = thing->number / 2; + float hp_per_blob = thing->max_hit_points / float(thing->number); + + thing->number -= split_off; + new_slime->number = split_off; + + new_slime->hit_dice = thing->hit_dice; + + _stats_from_blob_count(thing, hp_per_blob); + _stats_from_blob_count(new_slime, hp_per_blob); + + return (true); +} + +// Actually merge two slime creature, pooling their hp, etc. +// initial_slime is the one that gets killed off by this process. +static bool _do_merge(monsters *initial_slime, monsters *merge_to) +{ + // Combine enchantment durations. + _merge_ench_durations(initial_slime, merge_to); + + merge_to->number += initial_slime->number; + merge_to->max_hit_points += initial_slime->max_hit_points; + merge_to->hit_points += initial_slime->max_hit_points; + + // Merge monster flags (mostly so that MF_CREATED_NEUTRAL, etc. are + // passed on if the merged slime subsequently splits. Hopefully + // this won't do anything weird. + merge_to->flags |= initial_slime->flags; + + // Merging costs the combined slime some energy. + monsterentry* entry = get_monster_data(merge_to->type); + merge_to->speed_increment -= entry->energy_usage.move; + + // This is dumb. With that said, the idea is that if 2 slimes merge + // you can gain a space by moving away the turn after (maybe this is + // too nice but there will probably be a lot of complaints about the + // damage on higher level slimes). So we subtracted some energy + // above, but if merge_to hasn't moved yet this turn, that will just + // cancel its turn in this round of world_reacts(). So we are going + // to see if merge_to has gone already by checking its mindex (this + // works because handle_monsters just iterates over env.mons in + // ascending order). + if (initial_slime->mindex() < merge_to->mindex()) + merge_to->speed_increment -= entry->energy_usage.move; + + // Overwrite the state of the slime getting merged into, because it + // might have been resting or something. + merge_to->behaviour = initial_slime->behaviour; + merge_to->foe = initial_slime->foe; + + behaviour_event(merge_to, ME_EVAL); + + // Messaging. + if (you.can_see(merge_to)) + { + if (you.can_see(initial_slime)) + { + mprf("Two slime creatures merge to form %s.", + merge_to->name(DESC_NOCAP_A).c_str()); + } + else + { + mprf("A slime creature suddenly becomes %s.", + merge_to->name(DESC_NOCAP_A).c_str()); + } + + you.flash_colour = LIGHTGREEN; + viewwindow(true, false); + + int flash_delay = 150; + // Scale delay to match change in arena_delay. + if (crawl_state.arena) + { + flash_delay *= Options.arena_delay; + flash_delay /= 600; + } + + delay(flash_delay); + } + else if (you.can_see(initial_slime)) + mpr("A slime creature suddenly disappears!"); + + // Have to 'kill' the slime doing the merging. + monster_die(initial_slime, KILL_MISC, NON_MONSTER, true); + + return (true); +} + +// Slime creatures can split but not merge under these conditions. +static bool _unoccupied_slime(monsters *thing) +{ + return (thing->asleep() + || mons_is_wandering(thing) + || thing->foe == MHITNOT); +} + +// Slime creatures cannot split or merge under these conditions. +static bool _disabled_slime(monsters *thing) +{ + return (!thing + || mons_is_fleeing(thing) + || mons_is_confused(thing) + || mons_is_paralysed(thing)); +} + +// See if there are any appropriate adjacent slime creatures for 'thing' +// to merge with. If so, carry out the merge. +static bool _slime_merge(monsters *thing) +{ + if (!thing || _disabled_slime(thing) || _unoccupied_slime(thing)) + return (false); + + int max_slime_merge = 5; + int compass_idx[8] = {0, 1, 2, 3, 4, 5, 6, 7}; + std::random_shuffle(compass_idx, compass_idx + 8); + coord_def origin = thing->pos(); + + // Check for adjacent slime creatures. + for (int i = 0; i < 8; ++i) + { + coord_def target = origin + Compass[compass_idx[i]]; + monsters *other_thing = monster_at(target); + + // We found an adjacent monster. Is it another slime creature + // we can consider merging with? + if (other_thing + && other_thing->mons_species() == MONS_SLIME_CREATURE + && other_thing->attitude == thing->attitude + && other_thing->is_summoned() == thing->is_summoned() + && !mons_is_shapeshifter(other_thing) + && !_disabled_slime(other_thing)) + { + // We can actually merge if doing so won't take us over the + // merge cap and the 'movement' would bring us closer to our + // target. + int new_blob_count = other_thing->number + thing->number; + if (new_blob_count <= max_slime_merge + && grid_distance(thing->target, thing->pos()) > + grid_distance(thing->target, target)) + { + return (_do_merge(thing, other_thing)); + } + } + } + + // No adjacent slime creatures we could merge with. + return (false); +} + +// See if slime creature 'thing' can split, and carry out the split if +// we can find a square to place the new slime creature on. +static bool _slime_split(monsters *thing) +{ + if (!thing + || !_unoccupied_slime(thing) + || _disabled_slime(thing) + || thing->number <= 1) + { + return (false); + } + + int compass_idx[] = {0, 1, 2, 3, 4, 5, 6, 7}; + std::random_shuffle(compass_idx, compass_idx + 8); + coord_def origin = thing->pos(); + + // Anywhere we can place an offspring? + for (int i = 0; i < 8; ++i) + { + coord_def target = origin + Compass[compass_idx[i]]; + + if (mons_class_can_pass(MONS_SLIME_CREATURE, env.grid(target)) + && !actor_at(target)) + { + // This can fail if placing a new monster fails. That + // probably means we have too many monsters on the level, + // so just return in that case. + return (_do_split(thing, target)); + } + } + + // No free squares. + return (false); +} + +// See if a given slime creature can split or merge. +bool slime_split_merge(monsters *thing) +{ + // No merging/splitting shapeshifters. + if (!thing + || mons_is_shapeshifter(thing) + || thing->mons_species() != MONS_SLIME_CREATURE) + { + return (false); + } + + if (_slime_split(thing)) + return (true); + + return (_slime_merge(thing)); +} + +// 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_feat_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) + && !mon->asleep() + && swap_check(mon, you.pos(), true)) + { + swapping = true; + } + else if (!mon->submerged()) + 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); +} + +static bool _silver_statue_effects(monsters *mons) +{ + actor *foe = mons->get_foe(); + if (foe && mons->can_see(foe) && !one_chance_in(3)) + { + const std::string msg = + "'s eyes glow " + weird_glowing_colour() + '.'; + simple_monster_message(mons, msg.c_str(), MSGCH_WARN); + + create_monster( + mgen_data( + summon_any_demon((coinflip() ? DEMON_COMMON + : DEMON_LESSER)), + SAME_ATTITUDE(mons), 5, 0, foe->pos(), mons->foe)); + return (true); + } + return (false); +} + +static bool _orange_statue_effects(monsters *mons) +{ + actor *foe = mons->get_foe(); + if (foe && mons->can_see(foe) && !one_chance_in(3)) + { + if (you.can_see(foe)) + { + if (foe == &you) + mprf(MSGCH_WARN, "A hostile presence attacks your mind!"); + else if (you.can_see(mons)) + mprf(MSGCH_WARN, "%s fixes %s piercing gaze on %s.", + mons->name(DESC_CAP_THE).c_str(), + mons->pronoun(PRONOUN_NOCAP_POSSESSIVE).c_str(), + foe->name(DESC_NOCAP_THE).c_str()); + } + + MiscastEffect(foe, monster_index(mons), SPTYP_DIVINATION, + random2(15), random2(150), + "an orange crystal statue"); + return (true); + } + + return (false); +} + +static bool _orc_battle_cry(monsters *chief) +{ + const actor *foe = chief->get_foe(); + int affected = 0; + + if (foe + && (foe != &you || !mons_friendly(chief)) + && !silenced(chief->pos()) + && chief->can_see(foe) + && coinflip()) + { + const int boss_index = monster_index(chief); + const int level = chief->hit_dice > 12? 2 : 1; + std::vector seen_affected; + for (int i = 0; i < MAX_MONSTERS; ++i) + { + monsters *mon = &menv[i]; + if (mon != chief + && mon->alive() + && mons_species(mon->type) == MONS_ORC + && mons_aligned(boss_index, i) + && mon->hit_dice < chief->hit_dice + && !mon->has_ench(ENCH_BERSERK) + && !mon->has_ench(ENCH_MIGHT) + && !mon->cannot_move() + && !mon->confused() + && chief->can_see(mon)) + { + mon_enchant ench = mon->get_ench(ENCH_BATTLE_FRENZY); + if (ench.ench == ENCH_NONE || ench.degree < level) + { + const int dur = + random_range(12, 20) * speed_to_duration(mon->speed); + + if (ench.ench != ENCH_NONE) + { + ench.degree = level; + ench.duration = std::max(ench.duration, dur); + mon->update_ench(ench); + } + else + { + mon->add_ench(mon_enchant(ENCH_BATTLE_FRENZY, level, + KC_OTHER, dur)); + } + + affected++; + if (you.can_see(mon)) + seen_affected.push_back(mon); + + if (mon->asleep()) + behaviour_event(mon, ME_DISTURB, MHITNOT, chief->pos()); + } + } + } + + if (affected) + { + if (you.can_see(chief) && player_can_hear(chief->pos())) + { + mprf(MSGCH_SOUND, "%s roars a battle-cry!", + chief->name(DESC_CAP_THE).c_str()); + } + + // The yell happens whether you happen to see it or not. + noisy(15, chief->pos(), chief->mindex()); + + // Disabling detailed frenzy announcement because it's so spammy. + const msg_channel_type channel = + mons_friendly_real(chief) ? MSGCH_MONSTER_ENCHANT + : MSGCH_FRIEND_ENCHANT; + + if (!seen_affected.empty()) + { + std::string who; + if (seen_affected.size() == 1) + { + who = seen_affected[0]->name(DESC_CAP_THE); + mprf(channel, "%s goes into a battle-frenzy!", who.c_str()); + } + else + { + int type = seen_affected[0]->type; + for (unsigned int i = 0; i < seen_affected.size(); i++) + { + if (seen_affected[i]->type != type) + { + // just mention plain orcs + type = MONS_ORC; + break; + } + } + who = get_monster_data(type)->name; + + mprf(channel, "%s %s go into a battle-frenzy!", + mons_friendly(chief) ? "Your" : "The", + pluralise(who).c_str()); + } + } + } + } + // Orc battle cry doesn't cost the monster an action. + return (false); +} + +static bool _make_monster_angry(const monsters *mon, monsters *targ) +{ + if (mons_friendly_real(mon) != mons_friendly_real(targ)) + return (false); + + // targ is guaranteed to have a foe (needs_berserk checks this). + // Now targ needs to be closer to *its* foe than mon is (otherwise + // mon might be in the way). + + coord_def victim; + if (targ->foe == MHITYOU) + victim = you.pos(); + else if (targ->foe != MHITNOT) + { + const monsters *vmons = &menv[targ->foe]; + if (!vmons->alive()) + return (false); + victim = vmons->pos(); + } + else + { + // Should be impossible. needs_berserk should find this case. + ASSERT(false); + return (false); + } + + // If mon may be blocking targ from its victim, don't try. + if (victim.distance_from(targ->pos()) > victim.distance_from(mon->pos())) + return (false); + + if (you.can_see(mon)) + { + mprf("%s goads %s on!", mon->name(DESC_CAP_THE).c_str(), + targ->name(DESC_NOCAP_THE).c_str()); + } + + targ->go_berserk(false); + + return (true); +} + +static bool _moth_incite_monsters(const monsters *mon) +{ + if (is_sanctuary(you.pos()) || is_sanctuary(mon->pos())) + return false; + + int goaded = 0; + for (int i = 0; i < MAX_MONSTERS; ++i) + { + monsters *targ = &menv[i]; + if (targ == mon || !targ->alive() || !targ->needs_berserk()) + continue; + + if (mon->pos().distance_from(targ->pos()) > 3) + continue; + + if (is_sanctuary(targ->pos())) + continue; + + // Cannot goad other moths of wrath! + if (targ->type == MONS_MOTH_OF_WRATH) + continue; + + if (_make_monster_angry(mon, targ) && !one_chance_in(3 * ++goaded)) + return (true); + } + + return (false); +} + +//--------------------------------------------------------------- +// +// mon_special_ability +// +//--------------------------------------------------------------- +bool mon_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 ); + + // Slime creatures can split while out of sight. + if ((!mons_near(monster) + || monster->asleep() + || monster->submerged()) + && monster->mons_species() != MONS_SLIME_CREATURE) + { + 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_UGLY_THING: + case MONS_VERY_UGLY_THING: + // A (very) ugly thing's proximity to you if you're glowing, or + // to others of its kind, can mutate it into a different (very) + // ugly thing. + used = ugly_thing_mutate(monster, true); + break; + + case MONS_SLIME_CREATURE: + // Slime creatures may split or merge depending on the + // situation. + used = slime_split_merge(monster); + if (!monster->alive()) + return (true); + break; + + 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 (player_or_mon_in_sanct(monster)) + break; + + used = _orange_statue_effects(monster); + break; + + case MONS_SILVER_STATUE: + if (player_or_mon_in_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 == MONS_NO_MONSTER) + 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 (!feat_is_solid(grd(t))) + { + monster->hit_points = -1; + used = true; + break; + } + } + break; + + case MONS_LAVA_SNAKE: + if (monster->has_ench(ENCH_CONFUSION)) + break; + + if (!you.visible_to(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_ELECTRIC_EEL: + if (monster->has_ench(ENCH_CONFUSION)) + break; + + if (!you.visible_to(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 (player_or_mon_in_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); + 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 (player_or_mon_in_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; + } + } + 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 (!you.visible_to(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 (!you.visible_to(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); + 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(), monster->mindex(), 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); +} + +//--------------------------------------------------------------- +// +// mon_nearby_ability +// +// Gives monsters a chance to use a special ability when they're +// next to the player. +// +//--------------------------------------------------------------- +void mon_nearby_ability(monsters *monster) +{ + actor *foe = monster->get_foe(); + if (!foe + || !monster->can_see(foe) + || monster->asleep() + || monster->submerged()) + { + 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) + && !player_or_mon_in_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) + && !player_or_mon_in_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; + } +} + + diff --git a/crawl-ref/source/mon-abil.h b/crawl-ref/source/mon-abil.h new file mode 100644 index 0000000000..527f71dc3e --- /dev/null +++ b/crawl-ref/source/mon-abil.h @@ -0,0 +1,19 @@ +/* + * File: mon-abil.h + * Summary: Monster abilities. + * Written by: Linley Henzell + */ + +#ifndef MONABIL_H +#define MONABIL_H + +class monsters; +class bolt; + +bool mon_special_ability(monsters *monster, bolt & beem); +void mon_nearby_ability(monsters *monster); + +bool ugly_thing_mutate(monsters *ugly, bool proximity = false); +bool slime_split_merge(monsters *thing); + +#endif diff --git a/crawl-ref/source/mon-act.cc b/crawl-ref/source/mon-act.cc new file mode 100644 index 0000000000..42ead2a820 --- /dev/null +++ b/crawl-ref/source/mon-act.cc @@ -0,0 +1,3617 @@ +/* + * File: mon-act.cc + * Summary: Monsters doing stuff (monsters acting). + * Written by: Linley Henzell + */ + +#include "AppHdr.h" +#include "mon-act.h" + +#ifdef TARGET_OS_DOS +#include +#endif + +#include "arena.h" +#include "beam.h" +#include "cloud.h" +#include "delay.h" +#include "directn.h" +#include "fight.h" +#include "itemname.h" +#include "itemprop.h" +#include "items.h" +#include "item_use.h" +#include "mapmark.h" +#include "message.h" +#include "misc.h" +#include "mon-abil.h" +#include "mon-behv.h" +#include "mon-cast.h" +#include "monplace.h" +#include "monstuff.h" +#include "mutation.h" +#include "notes.h" +#include "player.h" +#include "random.h" +#include "religion.h" +#include "shopping.h" // for item values +#include "state.h" +#include "terrain.h" +#include "traps.h" +#include "tutorial.h" +#include "view.h" + +static bool _handle_pickup(monsters *monster); +static void _mons_in_cloud(monsters *monster); +static bool _mon_can_move_to_pos(const monsters *monster, + const coord_def& delta, + bool just_check = false); +static bool _is_trap_safe(const monsters *monster, const coord_def& where, + bool just_check = false); +static bool _monster_move(monsters *monster); +static spell_type _map_wand_to_mspell(int wand_type); + +// [dshaligram] Doesn't need to be extern. +static coord_def mmov; + +static const coord_def mon_compass[8] = { + coord_def(-1,-1), coord_def(0,-1), coord_def(1,-1), coord_def(1,0), + coord_def( 1, 1), coord_def(0, 1), coord_def(-1,1), coord_def(-1,0) +}; + +static bool immobile_monster[MAX_MONSTERS]; + +// A probably needless optimization: convert the C string "just seen" to +// a C++ string just once, instead of twice every time a monster moves. +static const std::string _just_seen("just seen"); + +static inline bool _mons_natural_regen_roll(monsters *monster) +{ + const int regen_rate = mons_natural_regen_rate(monster); + return (x_chance_in_y(regen_rate, 25)); +} + +// Do natural regeneration for monster. +static void _monster_regenerate(monsters *monster) +{ + if (monster->has_ench(ENCH_SICK) || !mons_can_regenerate(monster)) + return; + + // Non-land creatures out of their element cannot regenerate. + if (mons_primary_habitat(monster) != HT_LAND + && !monster_habitable_grid(monster, grd(monster->pos()))) + { + return; + } + + if (monster_descriptor(monster->type, MDSC_REGENERATES) + || (monster->type == MONS_FIRE_ELEMENTAL + && (grd(monster->pos()) == DNGN_LAVA + || cloud_type_at(monster->pos()) == CLOUD_FIRE)) + + || (monster->type == MONS_WATER_ELEMENTAL + && feat_is_watery(grd(monster->pos()))) + + || (monster->type == MONS_AIR_ELEMENTAL + && env.cgrid(monster->pos()) == EMPTY_CLOUD + && one_chance_in(3)) + + || _mons_natural_regen_roll(monster)) + { + heal_monster(monster, 1, false); + } +} + +static bool _swap_monsters(monsters* mover, monsters* moved) +{ + // Can't swap with a stationary monster. + if (mons_is_stationary(moved)) + return (false); + + // Swapping is a purposeful action. + if (mover->confused()) + return (false); + + // Right now just happens in sanctuary. + if (!is_sanctuary(mover->pos()) || !is_sanctuary(moved->pos())) + return (false); + + // A friendly or good-neutral monster moving past a fleeing hostile + // or neutral monster, or vice versa. + if (mons_wont_attack_real(mover) == mons_wont_attack_real(moved) + || mons_is_fleeing(mover) == mons_is_fleeing(moved)) + { + return (false); + } + + // Don't swap places if the player explicitly ordered their pet to + // attack monsters. + if ((mons_friendly(mover) || mons_friendly(moved)) + && you.pet_target != MHITYOU && you.pet_target != MHITNOT) + { + return (false); + } + + if (!mover->can_pass_through(moved->pos()) + || !moved->can_pass_through(mover->pos())) + { + return (false); + } + + if (!monster_habitable_grid(mover, grd(moved->pos())) + || !monster_habitable_grid(moved, grd(mover->pos()))) + { + return (false); + } + + // Okay, we can do the swap. + const coord_def mover_pos = mover->pos(); + const coord_def moved_pos = moved->pos(); + + mover->pos() = moved_pos; + moved->pos() = mover_pos; + + mgrd(mover->pos()) = mover->mindex(); + mgrd(moved->pos()) = moved->mindex(); + + if (you.can_see(mover) && you.can_see(moved)) + { + mprf("%s and %s swap places.", mover->name(DESC_CAP_THE).c_str(), + moved->name(DESC_NOCAP_THE).c_str()); + } + + return (true); +} + +static bool _do_mon_spell(monsters *monster, bolt &beem) +{ + // Shapeshifters don't get spells. + if (!mons_is_shapeshifter(monster) + || !mons_class_flag(monster->type, M_ACTUAL_SPELLS)) + { + if (handle_mon_spell(monster, beem)) + { + mmov.reset(); + return (true); + } + } + + return (false); +} + +static void _swim_or_move_energy(monsters *mon) +{ + const dungeon_feature_type feat = grd(mon->pos()); + + // FIXME: Replace check with mons_is_swimming()? + mon->lose_energy( (feat >= DNGN_LAVA && feat <= DNGN_SHALLOW_WATER + && !mon->airborne()) ? EUT_SWIM + : EUT_MOVE ); +} + +// Check up to eight grids in the given direction for whether there's a +// monster of the same alignment as the given monster that happens to +// have a ranged attack. If this is true for the first monster encountered, +// returns true. Otherwise returns false. +static bool _ranged_allied_monster_in_dir(monsters *mon, coord_def p) +{ + coord_def pos = mon->pos(); + + for (int i = 1; i <= LOS_RADIUS; i++) + { + pos += p; + if (!in_bounds(pos)) + break; + + const monsters* ally = monster_at(pos); + if (ally == NULL) + continue; + + if (mons_aligned(mon->mindex(), ally->mindex())) + { + // Hostile monsters of normal intelligence only move aside for + // monsters of the same type. + if (mons_intel(mon) <= I_NORMAL && !mons_wont_attack(mon) + && mons_genus(mon->type) != mons_genus(ally->type)) + { + return (false); + } + + if (mons_has_ranged_attack(ally) + || mons_has_ranged_spell(ally, true)) + { + return (true); + } + } + break; + } + return (false); +} + +// Check whether there's a monster of the same type and alignment adjacent +// to the given monster in at least one of three given directions (relative to +// the monster position). +static bool _allied_monster_at(monsters *mon, coord_def a, coord_def b, + coord_def c) +{ + std::vector pos; + pos.push_back(mon->pos() + a); + pos.push_back(mon->pos() + b); + pos.push_back(mon->pos() + c); + + for (unsigned int i = 0; i < pos.size(); i++) + { + if (!in_bounds(pos[i])) + continue; + + const monsters *ally = monster_at(pos[i]); + if (ally == NULL) + continue; + + if (mons_is_stationary(ally)) + continue; + + // Hostile monsters of normal intelligence only move aside for + // monsters of the same genus. + if (mons_intel(mon) <= I_NORMAL && !mons_wont_attack_real(mon) + && mons_genus(mon->type) != mons_genus(ally->type)) + { + continue; + } + + if (mons_aligned(mon->mindex(), ally->mindex())) + return (true); + } + + return (false); +} + +// Altars as well as branch entrances are considered interesting for +// some monster types. +static bool _mon_on_interesting_grid(monsters *mon) +{ + // Patrolling shouldn't happen all the time. + if (one_chance_in(4)) + return (false); + + const dungeon_feature_type feat = grd(mon->pos()); + + switch (feat) + { + // Holy beings will tend to patrol around altars to the good gods. + case DNGN_ALTAR_ELYVILON: + if (!one_chance_in(3)) + return (false); + // else fall through + case DNGN_ALTAR_ZIN: + case DNGN_ALTAR_SHINING_ONE: + return (mons_is_holy(mon)); + + // Orcs will tend to patrol around altars to Beogh, and guard the + // stairway from and to the Orcish Mines. + case DNGN_ALTAR_BEOGH: + case DNGN_ENTER_ORCISH_MINES: + case DNGN_RETURN_FROM_ORCISH_MINES: + return (mons_is_native_in_branch(mon, BRANCH_ORCISH_MINES)); + + // Same for elves and the Elven Halls. + case DNGN_ENTER_ELVEN_HALLS: + case DNGN_RETURN_FROM_ELVEN_HALLS: + return (mons_is_native_in_branch(mon, BRANCH_ELVEN_HALLS)); + + // Killer bees always return to their hive. + case DNGN_ENTER_HIVE: + return (mons_is_native_in_branch(mon, BRANCH_HIVE)); + + default: + return (false); + } +} + +// If a hostile monster finds itself on a grid of an "interesting" feature, +// while unoccupied, it will remain in that area, and try to return to it +// if it left it for fighting, seeking etc. +static void _maybe_set_patrol_route(monsters *monster) +{ + if (mons_is_wandering(monster) + && !mons_friendly(monster) + && !monster->is_patrolling() + && _mon_on_interesting_grid(monster)) + { + monster->patrol_point = monster->pos(); + } +} + +//--------------------------------------------------------------- +// +// handle_movement +// +// Move the monster closer to its target square. +// +//--------------------------------------------------------------- +static void _handle_movement(monsters *monster) +{ + coord_def delta; + + _maybe_set_patrol_route(monster); + + // Monsters will try to flee out of a sanctuary. + if (is_sanctuary(monster->pos()) + && mons_is_influenced_by_sanctuary(monster) + && !mons_is_fleeing_sanctuary(monster)) + { + mons_start_fleeing_from_sanctuary(monster); + } + else if (mons_is_fleeing_sanctuary(monster) + && !is_sanctuary(monster->pos())) + { + // Once outside there's a chance they'll regain their courage. + // Nonliving and berserking monsters always stop immediately, + // since they're only being forced out rather than actually + // scared. + if (monster->holiness() == MH_NONLIVING + || monster->has_ench(ENCH_BERSERK) + || x_chance_in_y(2, 5)) + { + mons_stop_fleeing_from_sanctuary(monster); + } + } + + // Some calculations. + if (mons_class_flag(monster->type, M_BURROWS) && monster->foe == MHITYOU) + { + // Boring beetles always move in a straight line in your + // direction. + delta = you.pos() - monster->pos(); + } + else + { + delta = monster->target - monster->pos(); + + if (crawl_state.arena && Options.arena_force_ai + && !mons_is_stationary(monster)) + { + const bool ranged = (mons_has_ranged_attack(monster) + || mons_has_ranged_spell(monster)); + + // Smiters are happy if they have clear visibility through + // glass, but other monsters must go around. + const bool glass_ok = mons_has_smite_attack(monster); + + // Monsters in the arena are smarter than the norm and + // always pathfind to their targets. + if (delta.abs() > 2 + && (!ranged + || !monster->mon_see_cell(monster->target, !glass_ok))) + { + monster_pathfind mp; + if (mp.init_pathfind(monster, monster->target)) + delta = mp.next_pos(monster->pos()) - monster->pos(); + } + } + } + + // Move the monster. + mmov.x = (delta.x > 0) ? 1 : ((delta.x < 0) ? -1 : 0); + mmov.y = (delta.y > 0) ? 1 : ((delta.y < 0) ? -1 : 0); + + if (mons_is_fleeing(monster) && monster->travel_target != MTRAV_WALL + && (!mons_friendly(monster) + || monster->target != you.pos())) + { + mmov *= -1; + } + + // Don't allow monsters to enter a sanctuary or attack you inside a + // sanctuary, even if you're right next to them. + if (is_sanctuary(monster->pos() + mmov) + && (!is_sanctuary(monster->pos()) + || monster->pos() + mmov == you.pos())) + { + mmov.reset(); + } + + // Bounds check: don't let fleeing monsters try to run off the grid. + const coord_def s = monster->pos() + mmov; + if (!in_bounds_x(s.x)) + mmov.x = 0; + if (!in_bounds_y(s.y)) + mmov.y = 0; + + // Now quit if we can't move. + if (mmov.origin()) + return; + + if (delta.rdist() > 3) + { + // Reproduced here is some semi-legacy code that makes monsters + // move somewhat randomly along oblique paths. It is an + // exceedingly good idea, given crawl's unique line of sight + // properties. + // + // Added a check so that oblique movement paths aren't used when + // close to the target square. -- bwr + + // Sometimes we'll just move parallel the x axis. + if (abs(delta.x) > abs(delta.y) && coinflip()) + mmov.y = 0; + + // Sometimes we'll just move parallel the y axis. + if (abs(delta.y) > abs(delta.x) && coinflip()) + mmov.x = 0; + } + + const coord_def newpos(monster->pos() + mmov); + FixedArray < bool, 3, 3 > good_move; + + for (int count_x = 0; count_x < 3; count_x++) + for (int count_y = 0; count_y < 3; count_y++) + { + const int targ_x = monster->pos().x + count_x - 1; + const int targ_y = monster->pos().y + count_y - 1; + + // Bounds check: don't consider moving out of grid! + if (!in_bounds(targ_x, targ_y)) + { + good_move[count_x][count_y] = false; + continue; + } + + good_move[count_x][count_y] = + _mon_can_move_to_pos(monster, coord_def(count_x-1, count_y-1)); + } + + if (mons_wall_shielded(monster)) + { + // The rock worm will try to move along through rock for as long as + // possible. If the player is walking through a corridor, for example, + // moving along in the wall beside him is much preferable to actually + // leaving the wall. + // This might cause the rock worm to take detours but it still + // comes off as smarter than otherwise. + if (mmov.x != 0 && mmov.y != 0) // diagonal movement + { + bool updown = false; + bool leftright = false; + + coord_def t = monster->pos() + coord_def(mmov.x, 0); + if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t))) + updown = true; + + t = monster->pos() + coord_def(0, mmov.y); + if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t))) + leftright = true; + + if (updown && (!leftright || coinflip())) + mmov.y = 0; + else if (leftright) + mmov.x = 0; + } + else if (mmov.x == 0 && monster->target.x == monster->pos().x) + { + bool left = false; + bool right = false; + coord_def t = monster->pos() + coord_def(-1, mmov.y); + if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t))) + left = true; + + t = monster->pos() + coord_def(1, mmov.y); + if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t))) + right = true; + + if (left && (!right || coinflip())) + mmov.x = -1; + else if (right) + mmov.x = 1; + } + else if (mmov.y == 0 && monster->target.y == monster->pos().y) + { + bool up = false; + bool down = false; + coord_def t = monster->pos() + coord_def(mmov.x, -1); + if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t))) + up = true; + + t = monster->pos() + coord_def(mmov.x, 1); + if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t))) + down = true; + + if (up && (!down || coinflip())) + mmov.y = -1; + else if (down) + mmov.y = 1; + } + } + + // If the monster is moving in your direction, whether to attack or + // protect you, or towards a monster it intends to attack, check + // whether we first need to take a step to the side to make sure the + // reinforcement can follow through. Only do this with 50% chance, + // though, so it's not completely predictable. + + // First, check whether the monster is smart enough to even consider + // this. + if ((newpos == you.pos() + || mgrd(newpos) != NON_MONSTER && monster->foe == mgrd(newpos)) + && mons_intel(monster) >= I_ANIMAL + && coinflip() + && !mons_is_confused(monster) && !mons_is_caught(monster) + && !monster->has_ench(ENCH_BERSERK)) + { + // If the monster is moving parallel to the x or y axis, check + // whether + // + // a) the neighbouring grids are blocked + // b) there are other unblocked grids adjacent to the target + // c) there's at least one allied monster waiting behind us. + // + // (For really smart monsters, also check whether there's a + // monster farther back in the corridor that has some kind of + // ranged attack.) + if (mmov.y == 0) + { + if (!good_move[1][0] && !good_move[1][2] + && (good_move[mmov.x+1][0] || good_move[mmov.x+1][2]) + && (_allied_monster_at(monster, coord_def(-mmov.x, -1), + coord_def(-mmov.x, 0), + coord_def(-mmov.x, 1)) + || mons_intel(monster) >= I_NORMAL + && !mons_wont_attack_real(monster) + && _ranged_allied_monster_in_dir(monster, + coord_def(-mmov.x, 0)))) + { + if (good_move[mmov.x+1][0]) + mmov.y = -1; + if (good_move[mmov.x+1][2] && (mmov.y == 0 || coinflip())) + mmov.y = 1; + } + } + else if (mmov.x == 0) + { + if (!good_move[0][1] && !good_move[2][1] + && (good_move[0][mmov.y+1] || good_move[2][mmov.y+1]) + && (_allied_monster_at(monster, coord_def(-1, -mmov.y), + coord_def(0, -mmov.y), + coord_def(1, -mmov.y)) + || mons_intel(monster) >= I_NORMAL + && !mons_wont_attack_real(monster) + && _ranged_allied_monster_in_dir(monster, + coord_def(0, -mmov.y)))) + { + if (good_move[0][mmov.y+1]) + mmov.x = -1; + if (good_move[2][mmov.y+1] && (mmov.x == 0 || coinflip())) + mmov.x = 1; + } + } + else // We're moving diagonally. + { + if (good_move[mmov.x+1][1]) + { + if (!good_move[1][mmov.y+1] + && (_allied_monster_at(monster, coord_def(-mmov.x, -1), + coord_def(-mmov.x, 0), + coord_def(-mmov.x, 1)) + || mons_intel(monster) >= I_NORMAL + && !mons_wont_attack_real(monster) + && _ranged_allied_monster_in_dir(monster, + coord_def(-mmov.x, -mmov.y)))) + { + mmov.y = 0; + } + } + else if (good_move[1][mmov.y+1] + && (_allied_monster_at(monster, coord_def(-1, -mmov.y), + coord_def(0, -mmov.y), + coord_def(1, -mmov.y)) + || mons_intel(monster) >= I_NORMAL + && !mons_wont_attack_real(monster) + && _ranged_allied_monster_in_dir(monster, + coord_def(-mmov.x, -mmov.y)))) + { + mmov.x = 0; + } + } + } + + // Now quit if we can't move. + if (mmov.origin()) + return; + + // Try to stay in sight of the player if we're moving towards + // him/her, in order to avoid the monster coming into view, + // shouting, and then taking a step in a path to the player which + // temporarily takes it out of view, which can lead to the player + // getting "comes into view" and shout messages with no monster in + // view. + + // Doesn't matter for arena mode. + if (crawl_state.arena) + return; + + // Did we just come into view? + if (monster->seen_context != _just_seen) + return; + + monster->seen_context.clear(); + + // If the player can't see us, it doesn't matter. + if (!(monster->flags & MF_WAS_IN_VIEW)) + return; + + const coord_def old_pos = monster->pos(); + const int old_dist = grid_distance(you.pos(), old_pos); + + // We're not moving towards the player. + if (grid_distance(you.pos(), old_pos + mmov) >= old_dist) + { + // Give a message if we move back out of view. + monster->seen_context = _just_seen; + return; + } + + // We're already staying in the player's LOS. + if (see_cell(old_pos + mmov)) + return; + + // Try to find a move that brings us closer to the player while + // keeping us in view. + int matches = 0; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + { + if (i == 0 && j == 0) + continue; + + if (!good_move[i][j]) + continue; + + delta.set(i - 1, j - 1); + coord_def tmp = old_pos + delta; + + if (grid_distance(you.pos(), tmp) < old_dist && see_cell(tmp)) + { + if (one_chance_in(++matches)) + mmov = delta; + break; + } + } + + // The only way to get closer to the player is to step out of view; + // give a message so they player isn't confused about its being + // announced as coming into view but not being seen. + monster->seen_context = _just_seen; +} + +//--------------------------------------------------------------- +// +// _handle_potion +// +// Give the monster a chance to quaff a potion. Returns true if +// the monster imbibed. +// +//--------------------------------------------------------------- +static bool _handle_potion(monsters *monster, bolt & beem) +{ + if (monster->asleep() + || monster->inv[MSLOT_POTION] == NON_ITEM + || !one_chance_in(3)) + { + return (false); + } + + bool rc = false; + + const int potion_idx = monster->inv[MSLOT_POTION]; + item_def& potion = mitm[potion_idx]; + const potion_type ptype = static_cast(potion.sub_type); + + if (monster->can_drink_potion(ptype) && monster->should_drink_potion(ptype)) + { + const bool was_visible = you.can_see(monster); + + // Drink the potion. + const item_type_id_state_type id = monster->drink_potion_effect(ptype); + + // Give ID if necessary. + if (was_visible && id != ID_UNKNOWN_TYPE) + set_ident_type(OBJ_POTIONS, ptype, id); + + // Remove it from inventory. + if (dec_mitm_item_quantity(potion_idx, 1)) + monster->inv[MSLOT_POTION] = NON_ITEM; + else if (is_blood_potion(potion)) + remove_oldest_blood_potion(potion); + + monster->lose_energy(EUT_ITEM); + rc = true; + } + + return (rc); +} + +static bool _handle_reaching(monsters *monster) +{ + bool ret = false; + const int wpn = monster->inv[MSLOT_WEAPON]; + + if (monster->submerged()) + return (false); + + if (mons_aligned(monster_index(monster), monster->foe)) + return (false); + + if (wpn != NON_ITEM && get_weapon_brand(mitm[wpn]) == SPWPN_REACHING) + { + if (monster->foe == MHITYOU) + { + const coord_def delta = monster->pos() - you.pos(); + const int x_middle = std::max(monster->pos().x, you.pos().x) + - (abs(delta.x) / 2); + const int y_middle = std::max(monster->pos().y, you.pos().y) + - (abs(delta.y) / 2); + const coord_def middle(x_middle, y_middle); + + // This check isn't redundant -- player may be invisible. + if (monster->target == you.pos() + && grid_distance(monster->pos(), you.pos()) == 2 + && (see_cell_no_trans(monster->pos()) + || grd(middle) > DNGN_MAX_NONREACH)) + { + ret = true; + monster_attack(monster, false); + } + } + else if (monster->foe != MHITNOT) + { + monsters& mfoe = menv[monster->foe]; + coord_def foepos = mfoe.pos(); + // Same comments as to invisibility as above. + if (monster->target == foepos + && monster->mon_see_cell(foepos, true) + && grid_distance(monster->pos(), foepos) == 2) + { + ret = true; + monsters_fight(monster, &mfoe, false); + } + } + } + + // Player saw the item reach. + if (ret && !is_artefact(mitm[wpn]) && you.can_see(monster)) + set_ident_flags(mitm[wpn], ISFLAG_KNOW_TYPE); + + return (ret); +} + +//--------------------------------------------------------------- +// +// handle_scroll +// +// Give the monster a chance to read a scroll. Returns true if +// the monster read something. +// +//--------------------------------------------------------------- +static bool _handle_scroll(monsters *monster) +{ + // Yes, there is a logic to this ordering {dlb}: + if (monster->asleep() + || mons_is_confused(monster) + || monster->submerged() + || monster->inv[MSLOT_SCROLL] == NON_ITEM + || !one_chance_in(3)) + { + return (false); + } + + // Make sure the item actually is a scroll. + if (mitm[monster->inv[MSLOT_SCROLL]].base_type != OBJ_SCROLLS) + return (false); + + bool read = false; + item_type_id_state_type ident = ID_UNKNOWN_TYPE; + bool was_visible = you.can_see(monster); + + // Notice how few cases are actually accounted for here {dlb}: + const int scroll_type = mitm[monster->inv[MSLOT_SCROLL]].sub_type; + switch (scroll_type) + { + case SCR_TELEPORTATION: + if (!monster->has_ench(ENCH_TP)) + { + if (mons_is_caught(monster) || mons_is_fleeing(monster) + || mons_is_pacified(monster)) + { + simple_monster_message(monster, " reads a scroll."); + monster_teleport(monster, false); + read = true; + ident = ID_KNOWN_TYPE; + } + } + break; + + case SCR_BLINKING: + if (mons_is_caught(monster) || mons_is_fleeing(monster) + || mons_is_pacified(monster)) + { + if (mons_near(monster)) + { + simple_monster_message(monster, " reads a scroll."); + monster_blink(monster); + read = true; + ident = ID_KNOWN_TYPE; + } + } + break; + + case SCR_SUMMONING: + if (mons_near(monster)) + { + simple_monster_message(monster, " reads a scroll."); + const int mon = create_monster( + mgen_data(MONS_ABOMINATION_SMALL, SAME_ATTITUDE(monster), + 0, 0, monster->pos(), monster->foe, MG_FORCE_BEH)); + + read = true; + if (mon != -1) + { + if (you.can_see(&menv[mon])) + { + mprf("%s appears!", menv[mon].name(DESC_CAP_A).c_str()); + ident = ID_KNOWN_TYPE; + } + player_angers_monster(&menv[mon]); + } + else if (you.can_see(monster)) + canned_msg(MSG_NOTHING_HAPPENS); + } + break; + } + + if (read) + { + if (dec_mitm_item_quantity(monster->inv[MSLOT_SCROLL], 1)) + monster->inv[MSLOT_SCROLL] = NON_ITEM; + + if (ident != ID_UNKNOWN_TYPE && was_visible) + set_ident_type(OBJ_SCROLLS, scroll_type, ident); + + monster->lose_energy(EUT_ITEM); + } + + return read; +} + +//--------------------------------------------------------------- +// +// handle_wand +// +// Give the monster a chance to zap a wand. Returns true if the +// monster zapped. +// +//--------------------------------------------------------------- +static bool _handle_wand(monsters *monster, bolt &beem) +{ + // Yes, there is a logic to this ordering {dlb}: + if (!mons_near(monster) + || monster->asleep() + || monster->has_ench(ENCH_SUBMERGED) + || monster->inv[MSLOT_WAND] == NON_ITEM + || mitm[monster->inv[MSLOT_WAND]].plus <= 0 + || coinflip()) + { + return (false); + } + + bool niceWand = false; + bool zap = false; + bool was_visible = you.can_see(monster); + + item_def &wand(mitm[monster->inv[MSLOT_WAND]]); + + // map wand type to monster spell type + const spell_type mzap = _map_wand_to_mspell(wand.sub_type); + if (mzap == SPELL_NO_SPELL) + return (false); + + // set up the beam + int power = 30 + monster->hit_dice; + bolt theBeam = mons_spells(monster, mzap, power); + + beem.name = theBeam.name; + beem.beam_source = monster_index(monster); + beem.source = monster->pos(); + beem.colour = theBeam.colour; + beem.range = theBeam.range; + beem.damage = theBeam.damage; + beem.ench_power = theBeam.ench_power; + beem.hit = theBeam.hit; + beem.type = theBeam.type; + beem.flavour = theBeam.flavour; + beem.thrower = theBeam.thrower; + beem.is_beam = theBeam.is_beam; + beem.is_explosion = theBeam.is_explosion; + +#if HISCORE_WEAPON_DETAIL + beem.aux_source = + wand.name(DESC_QUALNAME, false, true, false, false); +#else + beem.aux_source = + wand.name(DESC_QUALNAME, false, true, false, false, + ISFLAG_KNOW_CURSE | ISFLAG_KNOW_PLUSES); +#endif + + const int wand_type = wand.sub_type; + switch (wand_type) + { + case WAND_DISINTEGRATION: + // Dial down damage from wands of disintegration, since + // disintegration beams can do large amounts of damage. + beem.damage.size = beem.damage.size * 2 / 3; + break; + + case WAND_ENSLAVEMENT: + case WAND_DIGGING: + case WAND_RANDOM_EFFECTS: + // These have been deemed "too tricky" at this time {dlb}: + return (false); + + case WAND_POLYMORPH_OTHER: + // Monsters can be very trigger happy with wands, reduce this + // for polymorph. + if (!one_chance_in(5)) + return (false); + break; + + // These are wands that monsters will aim at themselves {dlb}: + case WAND_HASTING: + if (!monster->has_ench(ENCH_HASTE)) + { + beem.target = monster->pos(); + niceWand = true; + break; + } + return (false); + + case WAND_HEALING: + if (monster->hit_points <= monster->max_hit_points / 2) + { + beem.target = monster->pos(); + niceWand = true; + break; + } + return (false); + + case WAND_INVISIBILITY: + if (!monster->has_ench(ENCH_INVIS) + && !monster->has_ench(ENCH_SUBMERGED) + && (!mons_friendly(monster) || you.can_see_invisible(false))) + { + beem.target = monster->pos(); + niceWand = true; + break; + } + return (false); + + case WAND_TELEPORTATION: + if (monster->hit_points <= monster->max_hit_points / 2 + || mons_is_caught(monster)) + { + if (!monster->has_ench(ENCH_TP) + && !one_chance_in(20)) + { + beem.target = monster->pos(); + niceWand = true; + break; + } + // This break causes the wand to be tried on the player. + break; + } + return (false); + } + + // Fire tracer, if necessary. + if (!niceWand) + { + fire_tracer( monster, beem ); + + // Good idea? + zap = mons_should_fire(beem); + } + + if (niceWand || zap) + { + if (!niceWand) + make_mons_stop_fleeing(monster); + + if (!simple_monster_message(monster, " zaps a wand.")) + { + if (!silenced(you.pos())) + mpr("You hear a zap.", MSGCH_SOUND); + } + + // charge expenditure {dlb} + wand.plus--; + beem.is_tracer = false; + beem.fire(); + + if (was_visible) + { + if (niceWand || !beem.is_enchantment() || beem.obvious_effect) + set_ident_type(OBJ_WANDS, wand_type, ID_KNOWN_TYPE); + else + set_ident_type(OBJ_WANDS, wand_type, ID_MON_TRIED_TYPE); + + // Increment zap count. + if (wand.plus2 >= 0) + wand.plus2++; + } + + monster->lose_energy(EUT_ITEM); + + return (true); + } + + return (false); +} + +static void _setup_generic_throw(struct monsters *monster, struct bolt &pbolt) +{ + // FIXME we should use a sensible range here + pbolt.range = LOS_RADIUS; + pbolt.beam_source = monster_index(monster); + + pbolt.type = dchar_glyph(DCHAR_FIRED_MISSILE); + pbolt.flavour = BEAM_MISSILE; + pbolt.thrower = KILL_MON_MISSILE; + pbolt.aux_source.clear(); + pbolt.is_beam = false; +} + +static bool _mons_throw(struct monsters *monster, struct bolt &pbolt, + int hand_used) +{ + std::string ammo_name; + + bool returning = false; + + int baseHit = 0, baseDam = 0; // from thrown or ammo + int ammoHitBonus = 0, ammoDamBonus = 0; // from thrown or ammo + int lnchHitBonus = 0, lnchDamBonus = 0; // special add from launcher + int exHitBonus = 0, exDamBonus = 0; // 'extra' bonus from skill/dex/str + int lnchBaseDam = 0; + + int hitMult = 0; + int damMult = 0; + int diceMult = 100; + + // Some initial convenience & initializations. + int wepClass = mitm[hand_used].base_type; + int wepType = mitm[hand_used].sub_type; + + int weapon = monster->inv[MSLOT_WEAPON]; + int lnchType = (weapon != NON_ITEM) ? mitm[weapon].sub_type : 0; + + mon_inv_type slot = get_mon_equip_slot(monster, mitm[hand_used]); + ASSERT(slot != NUM_MONSTER_SLOTS); + + const bool skilled = mons_class_flag(monster->type, M_FIGHTER); + + monster->lose_energy(EUT_MISSILE); + const int throw_energy = monster->action_energy(EUT_MISSILE); + + // Dropping item copy, since the launched item might be different. + item_def item = mitm[hand_used]; + item.quantity = 1; + if (mons_friendly(monster)) + item.flags |= ISFLAG_DROPPED_BY_ALLY; + + // FIXME we should actually determine a sensible range here + pbolt.range = LOS_RADIUS; + + if (setup_missile_beam(monster, pbolt, item, ammo_name, returning)) + return (false); + + pbolt.aimed_at_spot = returning; + + const launch_retval projected = + is_launched(monster, monster->mslot_item(MSLOT_WEAPON), + mitm[hand_used]); + + // extract launcher bonuses due to magic + if (projected == LRET_LAUNCHED) + { + lnchHitBonus = mitm[weapon].plus; + lnchDamBonus = mitm[weapon].plus2; + lnchBaseDam = property(mitm[weapon], PWPN_DAMAGE); + } + + // extract weapon/ammo bonuses due to magic + ammoHitBonus = item.plus; + ammoDamBonus = item.plus2; + + // Archers get a boost from their melee attack. + if (mons_class_flag(monster->type, M_ARCHER)) + { + const mon_attack_def attk = mons_attack_spec(monster, 0); + if (attk.type == AT_SHOOT) + ammoDamBonus += random2avg(attk.damage, 2); + } + + if (projected == LRET_THROWN) + { + // Darts are easy. + if (wepClass == OBJ_MISSILES && wepType == MI_DART) + { + baseHit = 11; + hitMult = 40; + damMult = 25; + } + else + { + baseHit = 6; + hitMult = 30; + damMult = 25; + } + + baseDam = property(item, PWPN_DAMAGE); + + if (wepClass == OBJ_MISSILES) // throw missile + { + // ammo damage needs adjusting here - OBJ_MISSILES + // don't get separate tohit/damage bonuses! + ammoDamBonus = ammoHitBonus; + + // [dshaligram] Thrown stones/darts do only half the damage of + // launched stones/darts. This matches 4.0 behaviour. + if (wepType == MI_DART || wepType == MI_STONE + || wepType == MI_SLING_BULLET) + { + baseDam = div_rand_round(baseDam, 2); + } + } + + // give monster "skill" bonuses based on HD + exHitBonus = (hitMult * monster->hit_dice) / 10 + 1; + exDamBonus = (damMult * monster->hit_dice) / 10 + 1; + } + + // Monsters no longer gain unfair advantages with weapons of + // fire/ice and incorrect ammo. They now have the same restrictions + // as players. + + int bow_brand = SPWPN_NORMAL; + const int ammo_brand = get_ammo_brand(item); + + if (projected == LRET_LAUNCHED) + { + bow_brand = get_weapon_brand(mitm[monster->inv[MSLOT_WEAPON]]); + + switch (lnchType) + { + case WPN_BLOWGUN: + baseHit = 12; + hitMult = 60; + damMult = 0; + lnchDamBonus = 0; + break; + case WPN_BOW: + case WPN_LONGBOW: + baseHit = 0; + hitMult = 60; + damMult = 35; + // monsters get half the launcher damage bonus, + // which is about as fair as I can figure it. + lnchDamBonus = (lnchDamBonus + 1) / 2; + break; + case WPN_CROSSBOW: + baseHit = 4; + hitMult = 70; + damMult = 30; + break; + case WPN_HAND_CROSSBOW: + baseHit = 2; + hitMult = 50; + damMult = 20; + break; + case WPN_SLING: + baseHit = 10; + hitMult = 40; + damMult = 20; + // monsters get half the launcher damage bonus, + // which is about as fair as I can figure it. + lnchDamBonus /= 2; + break; + } + + // Launcher is now more important than ammo for base damage. + baseDam = property(item, PWPN_DAMAGE); + if (lnchBaseDam) + baseDam = lnchBaseDam + random2(1 + baseDam); + + // missiles don't have pluses2; use hit bonus + ammoDamBonus = ammoHitBonus; + + exHitBonus = (hitMult * monster->hit_dice) / 10 + 1; + exDamBonus = (damMult * monster->hit_dice) / 10 + 1; + + if (!baseDam && elemental_missile_beam(bow_brand, ammo_brand)) + baseDam = 4; + + // [dshaligram] This is a horrible hack - we force beam.cc to + // consider this beam "needle-like". + if (wepClass == OBJ_MISSILES && wepType == MI_NEEDLE) + pbolt.ench_power = AUTOMATIC_HIT; + + // elven bow w/ elven arrow, also orcish + if (get_equip_race(mitm[monster->inv[MSLOT_WEAPON]]) + == get_equip_race(mitm[monster->inv[MSLOT_MISSILE]])) + { + baseHit++; + baseDam++; + + if (get_equip_race(mitm[monster->inv[MSLOT_WEAPON]]) == ISFLAG_ELVEN) + pbolt.hit++; + } + + // POISON brand launchers poison ammo + if (bow_brand == SPWPN_VENOM && ammo_brand == SPMSL_NORMAL) + set_item_ego_type(item, OBJ_MISSILES, SPMSL_POISONED); + + // Vorpal brand increases damage dice size. + if (bow_brand == SPWPN_VORPAL) + diceMult = diceMult * 130 / 100; + + // As do steel ammo. + if (ammo_brand == SPMSL_STEEL) + diceMult = diceMult * 150 / 100; + + // Note: we already have throw_energy taken off. -- bwr + int speed_delta = 0; + if (lnchType == WPN_CROSSBOW) + { + if (bow_brand == SPWPN_SPEED) + { + // Speed crossbows take 50% less time to use than + // ordinary crossbows. + speed_delta = div_rand_round(throw_energy * 2, 5); + } + else + { + // Ordinary crossbows take 20% more time to use + // than ordinary bows. + speed_delta = -div_rand_round(throw_energy, 5); + } + } + else if (bow_brand == SPWPN_SPEED) + { + // Speed bows take 50% less time to use than + // ordinary bows. + speed_delta = div_rand_round(throw_energy, 2); + } + + monster->speed_increment += speed_delta; + } + + // Chaos overides flame and frost + if (pbolt.flavour != BEAM_MISSILE) + { + baseHit += 2; + exDamBonus += 6; + } + + // monster intelligence bonus + if (mons_intel(monster) == I_HIGH) + exHitBonus += 10; + + // Now, if a monster is, for some reason, throwing something really + // stupid, it will have baseHit of 0 and damage of 0. Ah well. + std::string msg = monster->name(DESC_CAP_THE); + msg += ((projected == LRET_LAUNCHED) ? " shoots " : " throws "); + + if (!pbolt.name.empty() && projected == LRET_LAUNCHED) + msg += article_a(pbolt.name); + else + { + // build shoot message + msg += item.name(DESC_NOCAP_A); + + // build beam name + pbolt.name = item.name(DESC_PLAIN, false, false, false); + } + msg += "."; + + if (monster->observable()) + { + mpr(msg.c_str()); + + if (projected == LRET_LAUNCHED + && item_type_known(mitm[monster->inv[MSLOT_WEAPON]]) + || projected == LRET_THROWN + && mitm[hand_used].base_type == OBJ_MISSILES) + { + set_ident_flags(mitm[hand_used], ISFLAG_KNOW_TYPE); + } + } + + // [dshaligram] When changing bolt names here, you must edit + // hiscores.cc (scorefile_entry::terse_missile_cause()) to match. + char throw_buff[ITEMNAME_SIZE]; + if (projected == LRET_LAUNCHED) + { + snprintf(throw_buff, sizeof(throw_buff), "Shot with a%s %s by %s", + (is_vowel(pbolt.name[0]) ? "n" : ""), pbolt.name.c_str(), + monster->name(DESC_NOCAP_A).c_str()); + } + else + { + snprintf(throw_buff, sizeof(throw_buff), "Hit by a%s %s thrown by %s", + (is_vowel(pbolt.name[0]) ? "n" : ""), pbolt.name.c_str(), + monster->name(DESC_NOCAP_A).c_str()); + } + + pbolt.aux_source = throw_buff; + + // Add everything up. + pbolt.hit = baseHit + random2avg(exHitBonus, 2) + ammoHitBonus; + pbolt.damage = + dice_def(1, baseDam + random2avg(exDamBonus, 2) + ammoDamBonus); + + if (projected == LRET_LAUNCHED) + { + pbolt.damage.size += lnchDamBonus; + pbolt.hit += lnchHitBonus; + } + pbolt.damage.size = diceMult * pbolt.damage.size / 100; + + if (monster->has_ench(ENCH_BATTLE_FRENZY)) + { + const mon_enchant ench = monster->get_ench(ENCH_BATTLE_FRENZY); + +#ifdef DEBUG_DIAGNOSTICS + const dice_def orig_damage = pbolt.damage; +#endif + + pbolt.damage.size = pbolt.damage.size * (115 + ench.degree * 15) / 100; + +#ifdef DEBUG_DIAGNOSTICS + mprf(MSGCH_DIAGNOSTICS, "%s frenzy damage: %dd%d -> %dd%d", + monster->name(DESC_PLAIN).c_str(), + orig_damage.num, orig_damage.size, + pbolt.damage.num, pbolt.damage.size); +#endif + } + + // Skilled archers get better to-hit and damage. + if (skilled) + { + pbolt.hit = pbolt.hit * 120 / 100; + pbolt.damage.size = pbolt.damage.size * 120 / 100; + } + + scale_dice(pbolt.damage); + + // decrease inventory + bool really_returns; + if (returning && !one_chance_in(mons_power(monster->type) + 3)) + really_returns = true; + else + really_returns = false; + + pbolt.drop_item = !really_returns; + + // Redraw the screen before firing, in case the monster just + // came into view and the screen hasn't been updated yet. + viewwindow(true, false); + pbolt.fire(); + + // The item can be destroyed before returning. + if (really_returns && thrown_object_destroyed(&item, pbolt.target, true)) + { + really_returns = false; + } + + if (really_returns) + { + // Fire beam in reverse. + pbolt.setup_retrace(); + viewwindow(true, false); + pbolt.fire(); + msg::stream << "The weapon returns " + << (you.can_see(monster)? + ("to " + monster->name(DESC_NOCAP_THE)) + : "whence it came from") + << "!" << std::endl; + + // Player saw the item return. + if (!is_artefact(item)) + { + // Since this only happens for non-artefacts, also mark properties + // as known. + set_ident_flags(mitm[hand_used], + ISFLAG_KNOW_TYPE | ISFLAG_KNOW_PROPERTIES); + } + } + else if (dec_mitm_item_quantity(hand_used, 1)) + monster->inv[returning ? slot : MSLOT_MISSILE] = NON_ITEM; + + if (pbolt.special_explosion != NULL) + delete pbolt.special_explosion; + + return (true); +} + +//--------------------------------------------------------------- +// +// handle_throw +// +// Give the monster a chance to throw something. Returns true if +// the monster hurled. +// +//--------------------------------------------------------------- +static bool _handle_throw(monsters *monster, bolt & beem) +{ + // Yes, there is a logic to this ordering {dlb}: + if (monster->incapacitated() + || monster->asleep() + || monster->submerged()) + { + return (false); + } + + if (mons_itemuse(monster) < MONUSE_STARTING_EQUIPMENT) + return (false); + + const bool archer = mons_class_flag(monster->type, M_ARCHER); + // Highly-specialised archers are more likely to shoot than talk. + if (one_chance_in(archer? 9 : 5)) + return (false); + + // Don't allow offscreen throwing for now. + if (monster->foe == MHITYOU && !mons_near(monster)) + return (false); + + // Monsters won't shoot in melee range, largely for balance reasons. + // Specialist archers are an exception to this rule. + if (!archer && adjacent(beem.target, monster->pos())) + return (false); + + // Greatly lowered chances if the monster is fleeing or pacified and + // leaving the level. + if ((mons_is_fleeing(monster) || mons_is_pacified(monster)) + && !one_chance_in(8)) + { + return (false); + } + + item_def *launcher = NULL; + const item_def *weapon = NULL; + const int mon_item = mons_pick_best_missile(monster, &launcher); + + if (mon_item == NON_ITEM || !is_valid_item(mitm[mon_item])) + return (false); + + if (player_or_mon_in_sanct(monster)) + return (false); + + item_def *missile = &mitm[mon_item]; + + // Throwing a net at a target that is already caught would be + // completely useless, so bail out. + const actor *act = actor_at(beem.target); + if (missile->base_type == OBJ_MISSILES + && missile->sub_type == MI_THROWING_NET + && act && act->caught()) + { + return (false); + } + + // If the attack needs a launcher that we can't wield, bail out. + if (launcher) + { + weapon = monster->mslot_item(MSLOT_WEAPON); + if (weapon && weapon != launcher && weapon->cursed()) + return (false); + } + + // Ok, we'll try it. + _setup_generic_throw( monster, beem ); + + // Set fake damage for the tracer. + beem.damage = dice_def(10, 10); + + // Set item for tracer, even though it probably won't be used + beem.item = missile; + + // Fire tracer. + fire_tracer( monster, beem ); + + // Clear fake damage (will be set correctly in mons_throw). + beem.damage = 0; + + // Good idea? + if (mons_should_fire( beem )) + { + // Monsters shouldn't shoot if fleeing, so let them "turn to attack". + make_mons_stop_fleeing(monster); + + if (launcher && launcher != weapon) + monster->swap_weapons(); + + beem.name.clear(); + return (_mons_throw( monster, beem, mon_item )); + } + + return (false); +} + +// Give the monster its action energy (aka speed_increment). +static void _monster_add_energy(monsters *monster) +{ + if (monster->speed > 0) + { + // Randomise to make counting off monster moves harder: + const int energy_gained = + std::max(1, div_rand_round(monster->speed * you.time_taken, 10)); + monster->speed_increment += energy_gained; + } +} + +static void _khufu_drop_tomb(monsters *monster) +{ + int count = 0; + + monster->behaviour = BEH_SEEK; // don't wander on duty! + for (adjacent_iterator ai(monster->pos()); ai; ++ai) + { + if (grd(*ai) == DNGN_ROCK_WALL) + { + grd(*ai) = DNGN_FLOOR; + count++; + } + } + if (count) + if (mons_near(monster)) + mpr("The walls disappear!"); + else + mpr("You hear a deep rumble."); + monster->number = 0; + monster->lose_energy(EUT_SPELL); +} + +#ifdef DEBUG +# define DEBUG_ENERGY_USE(problem) \ + if (monster->speed_increment == old_energy && monster->alive()) \ + mprf(MSGCH_DIAGNOSTICS, \ + problem " for monster '%s' consumed no energy", \ + monster->name(DESC_PLAIN).c_str(), true); +#else +# define DEBUG_ENERGY_USE(problem) ((void) 0) +#endif + +static void _handle_monster_move(monsters *monster) +{ + monster->hit_points = std::min(monster->max_hit_points, + monster->hit_points); + + // Monster just summoned (or just took stairs), skip this action. + if (testbits( monster->flags, MF_JUST_SUMMONED )) + { + monster->flags &= ~MF_JUST_SUMMONED; + return; + } + + mon_acting mact(monster); + + _monster_add_energy(monster); + + // Handle clouds on nonmoving monsters. + if (monster->speed == 0 + && env.cgrid(monster->pos()) != EMPTY_CLOUD + && !monster->submerged()) + { + _mons_in_cloud( monster ); + } + + // Apply monster enchantments once for every normal-speed + // player turn. + monster->ench_countdown -= you.time_taken; + while (monster->ench_countdown < 0) + { + monster->ench_countdown += 10; + monster->apply_enchantments(); + + // If the monster *merely* died just break from the loop + // rather than quit altogether, since we have to deal with + // giant spores and ball lightning exploding at the end of the + // function, but do return if the monster's data has been + // reset, since then the monster type is invalid. + if (monster->type == MONS_NO_MONSTER) + return; + else if (monster->hit_points < 1) + break; + } + + // Memory is decremented here for a reason -- we only want it + // decrementing once per monster "move". + if (monster->foe_memory > 0) + monster->foe_memory--; + + // Otherwise there are potential problems with summonings. + if (monster->type == MONS_GLOWING_SHAPESHIFTER) + monster->add_ench(ENCH_GLOWING_SHAPESHIFTER); + + if (monster->type == MONS_SHAPESHIFTER) + monster->add_ench(ENCH_SHAPESHIFTER); + + // We reset batty monsters from wander to seek here, instead + // of in handle_behaviour() since that will be called with + // every single movement, and we want these monsters to + // hit and run. -- bwr + if (monster->foe != MHITNOT && mons_is_wandering(monster) + && mons_is_batty(monster)) + { + monster->behaviour = BEH_SEEK; + } + + monster->check_speed(); + + monsterentry* entry = get_monster_data(monster->type); + if (!entry) + return; + + int old_energy = INT_MAX; + int non_move_energy = std::min(entry->energy_usage.move, + entry->energy_usage.swim); + +#if DEBUG_MONS_SCAN + bool monster_was_floating = mgrd(monster->pos()) != monster->mindex(); +#endif + + while (monster->has_action_energy()) + { + // The continues & breaks are WRT this. + if (!monster->alive()) + break; + + const coord_def old_pos = monster->pos(); + +#if DEBUG_MONS_SCAN + if (!monster_was_floating + && mgrd(monster->pos()) != monster->mindex()) + { + mprf(MSGCH_ERROR, "Monster %s became detached from mgrd " + "in _handle_monster_move() loop", + monster->name(DESC_PLAIN, true).c_str()); + mpr("[[[[[[[[[[[[[[[[[[", MSGCH_WARN); + debug_mons_scan(); + mpr("]]]]]]]]]]]]]]]]]]", MSGCH_WARN); + monster_was_floating = true; + } + else if (monster_was_floating + && mgrd(monster->pos()) == monster->mindex()) + { + mprf(MSGCH_DIAGNOSTICS, "Monster %s re-attached itself to mgrd " + "in _handle_monster_move() loop", + monster->name(DESC_PLAIN, true).c_str()); + monster_was_floating = false; + } +#endif + + if (monster->speed_increment >= old_energy) + { +#ifdef DEBUG + if (monster->speed_increment == old_energy) + { + mprf(MSGCH_DIAGNOSTICS, "'%s' has same energy as last loop", + monster->name(DESC_PLAIN, true).c_str()); + } + else + { + mprf(MSGCH_DIAGNOSTICS, "'%s' has MORE energy than last loop", + monster->name(DESC_PLAIN, true).c_str()); + } +#endif + monster->speed_increment = old_energy - 10; + old_energy = monster->speed_increment; + continue; + } + old_energy = monster->speed_increment; + + monster->shield_blocks = 0; + + cloud_type cl_type; + const int cloud_num = env.cgrid(monster->pos()); + const bool avoid_cloud = mons_avoids_cloud(monster, cloud_num, + &cl_type); + if (cl_type != CLOUD_NONE) + { + if (avoid_cloud) + { + if (monster->submerged()) + { + monster->speed_increment -= entry->energy_usage.swim; + break; + } + + if (monster->type == MONS_NO_MONSTER) + { + monster->speed_increment -= entry->energy_usage.move; + break; // problem with vortices + } + } + + _mons_in_cloud(monster); + + if (monster->type == MONS_NO_MONSTER) + { + monster->speed_increment = 1; + break; + } + } + + if (monster->type == MONS_TIAMAT && one_chance_in(3)) + { + const int cols[] = { RED, WHITE, DARKGREY, GREEN, MAGENTA }; + monster->colour = RANDOM_ELEMENT(cols); + } + + _monster_regenerate(monster); + + if (mons_cannot_act(monster)) + { + monster->speed_increment -= non_move_energy; + continue; + } + + handle_behaviour(monster); + + // handle_behaviour() could make the monster leave the level. + if (!monster->alive()) + break; + + ASSERT(!crawl_state.arena || monster->foe != MHITYOU); + ASSERT(in_bounds(monster->target) || monster->target.origin()); + + // Submerging monsters will hide from clouds. + if (avoid_cloud + && monster_can_submerge(monster, grd(monster->pos())) + && !monster->caught() + && !monster->submerged()) + { + monster->add_ench(ENCH_SUBMERGED); + monster->speed_increment -= ENERGY_SUBMERGE(entry); + continue; + } + + if (monster->speed >= 100) + { + monster->speed_increment -= non_move_energy; + continue; + } + + if (igrd(monster->pos()) != NON_ITEM + && (mons_itemuse(monster) >= MONUSE_WEAPONS_ARMOUR + || mons_itemeat(monster) != MONEAT_NOTHING)) + { + // Keep neutral and charmed monsters from picking up stuff. + // Same for friendlies if friendly_pickup is set to "none". + if (!mons_neutral(monster) && !monster->has_ench(ENCH_CHARM) + || (you.religion == GOD_JIYVA && mons_is_slime(monster)) + && (!mons_friendly(monster) + || you.friendly_pickup != FRIENDLY_PICKUP_NONE)) + { + if (_handle_pickup(monster)) + { + DEBUG_ENERGY_USE("handle_pickup()"); + continue; + } + } + } + + // Lurking monsters only stop lurking if their target is right + // next to them, otherwise they just sit there. + // However, if the monster is involuntarily submerged but + // still alive (e.g., nonbreathing which had water poured + // on top of it), this doesn't apply. + if (mons_is_lurking(monster) || monster->has_ench(ENCH_SUBMERGED)) + { + if (monster->foe != MHITNOT + && grid_distance(monster->target, monster->pos()) <= 1) + { + if (monster->submerged()) + { + // Don't unsubmerge if the monster is too damaged or + // if the monster is afraid, or if it's avoiding the + // cloud on top of the water. + if (monster->hit_points <= monster->max_hit_points / 2 + || monster->has_ench(ENCH_FEAR) + || avoid_cloud) + { + monster->speed_increment -= non_move_energy; + continue; + } + + if (!monster->del_ench(ENCH_SUBMERGED)) + { + // Couldn't unsubmerge. + monster->speed_increment -= non_move_energy; + continue; + } + } + monster->behaviour = BEH_SEEK; + } + else + { + monster->speed_increment -= non_move_energy; + continue; + } + } + + if (mons_is_caught(monster)) + { + // Struggling against the net takes time. + _swim_or_move_energy(monster); + } + else if (!mons_is_petrified(monster)) + { + // Calculates mmov based on monster target. + _handle_movement(monster); + + if (mons_is_confused(monster) + || monster->type == MONS_AIR_ELEMENTAL + && monster->submerged()) + { + mmov.reset(); + int pfound = 0; + for (adjacent_iterator ai(monster->pos(), false); ai; ++ai) + if (monster->can_pass_through(*ai)) + if (one_chance_in(++pfound)) + mmov = *ai - monster->pos(); + + // OK, mmov determined. + const coord_def newcell = mmov + monster->pos(); + monsters* enemy = monster_at(newcell); + if (enemy + && newcell != monster->pos() + && !is_sanctuary(monster->pos())) + { + if (monsters_fight(monster, enemy)) + { + mmov.reset(); + DEBUG_ENERGY_USE("monsters_fight()"); + continue; + } + else + { + // FIXME: None of these work! + // Instead run away! + if (monster->add_ench(mon_enchant(ENCH_FEAR))) + { + behaviour_event(monster, ME_SCARE, + MHITNOT, newcell); + } + break; + } + } + } + } + mon_nearby_ability(monster); + + if (monster->type == MONS_KHUFU && monster->number + && monster->hit_points==monster->max_hit_points) + _khufu_drop_tomb(monster); + + if (!monster->asleep() && !mons_is_wandering(monster) + // Berserking monsters are limited to running up and + // hitting their foes. + && !monster->has_ench(ENCH_BERSERK) + // Slime creatures can split while wandering or resting. + || monster->type == MONS_SLIME_CREATURE) + { + bolt beem; + + beem.source = monster->pos(); + beem.target = monster->target; + beem.beam_source = monster->mindex(); + + // Prevents unfriendlies from nuking you from offscreen. + // How nice! + const bool friendly_or_near = + mons_friendly(monster) || monster->near_foe(); + if (friendly_or_near + || monster->type == MONS_TEST_SPAWNER + // Slime creatures can split when offscreen. + || monster->type == MONS_SLIME_CREATURE) + { + // [ds] Special abilities shouldn't overwhelm + // spellcasting in monsters that have both. This aims + // to give them both roughly the same weight. + if (coinflip() ? mon_special_ability(monster, beem) + || _do_mon_spell(monster, beem) + : _do_mon_spell(monster, beem) + || mon_special_ability(monster, beem)) + { + DEBUG_ENERGY_USE("spell or special"); + mmov.reset(); + continue; + } + } + + if (friendly_or_near) + { + if (_handle_potion(monster, beem)) + { + DEBUG_ENERGY_USE("_handle_potion()"); + continue; + } + + if (_handle_scroll(monster)) + { + DEBUG_ENERGY_USE("_handle_scroll()"); + continue; + } + + if (_handle_wand(monster, beem)) + { + DEBUG_ENERGY_USE("_handle_wand()"); + continue; + } + + if (_handle_reaching(monster)) + { + DEBUG_ENERGY_USE("_handle_reaching()"); + continue; + } + } + + if (_handle_throw(monster, beem)) + { + DEBUG_ENERGY_USE("_handle_throw()"); + continue; + } + } + + if (!mons_is_caught(monster)) + { + if (monster->pos() + mmov == you.pos()) + { + ASSERT(!crawl_state.arena); + + if (!mons_friendly(monster)) + { + // If it steps into you, cancel other targets. + monster->foe = MHITYOU; + monster->target = you.pos(); + + monster_attack(monster); + + if (mons_is_batty(monster)) + { + monster->behaviour = BEH_WANDER; + set_random_target(monster); + } + DEBUG_ENERGY_USE("monster_attack()"); + mmov.reset(); + continue; + } + } + + // See if we move into (and fight) an unfriendly monster. + monsters* targ = monster_at(monster->pos() + mmov); + if (targ + && targ != monster + && !mons_aligned(monster->mindex(), targ->mindex()) + && monster_can_hit_monster(monster, targ)) + { + // Maybe they can swap places? + if (_swap_monsters(monster, targ)) + { + _swim_or_move_energy(monster); + continue; + } + // Figure out if they fight. + else if (monsters_fight(monster, targ)) + { + if (mons_is_batty(monster)) + { + monster->behaviour = BEH_WANDER; + set_random_target(monster); + // monster->speed_increment -= monster->speed; + } + + mmov.reset(); + DEBUG_ENERGY_USE("monsters_fight()"); + continue; + } + } + + if (invalid_monster(monster) || mons_is_stationary(monster)) + { + if (monster->speed_increment == old_energy) + monster->speed_increment -= non_move_energy; + continue; + } + + if (mons_cannot_move(monster) || !_monster_move(monster)) + monster->speed_increment -= non_move_energy; + } + update_beholders(monster); + + // Reevaluate behaviour, since the monster's surroundings have + // changed (it may have moved, or died for that matter). Don't + // bother for dead monsters. :) + if (monster->alive()) + { + handle_behaviour(monster); + ASSERT(in_bounds(monster->target) || monster->target.origin()); + } + } + + if (monster->type != MONS_NO_MONSTER && monster->hit_points < 1) + monster_die(monster, KILL_MISC, NON_MONSTER); +} + +//--------------------------------------------------------------- +// +// handle_monsters +// +// This is the routine that controls monster AI. +// +//--------------------------------------------------------------- +void handle_monsters() +{ + // Keep track of monsters that have already moved and don't allow + // them to move again. + memset(immobile_monster, 0, sizeof immobile_monster); + + for (int i = 0; i < MAX_MONSTERS; ++i) + { + monsters *monster = &menv[i]; + + if (!monster->alive() || immobile_monster[i]) + continue; + + const coord_def oldpos = monster->pos(); + + _handle_monster_move(monster); + + if (!invalid_monster(monster) && monster->pos() != oldpos) + immobile_monster[i] = true; + + // If the player got banished, discard pending monster actions. + if (you.banished) + { + // Clear list of mesmerising monsters. + if (you.duration[DUR_MESMERISED]) + { + you.mesmerised_by.clear(); + you.duration[DUR_MESMERISED] = 0; + } + break; + } + } + + // Clear any summoning flags so that lower indiced + // monsters get their actions in the next round. + for (int i = 0; i < MAX_MONSTERS; i++) + menv[i].flags &= ~MF_JUST_SUMMONED; +} + +static bool _jelly_divide(monsters *parent) +{ + if (!mons_class_flag(parent->type, M_SPLITS)) + return (false); + + const int reqd = std::max(parent->hit_dice * 8, 50); + if (parent->hit_points < reqd) + return (false); + + monsters *child = NULL; + coord_def child_spot; + int num_spots = 0; + + // First, find a suitable spot for the child {dlb}: + for (adjacent_iterator ai(parent->pos()); ai; ++ai) + if (actor_at(*ai) == NULL && parent->can_pass_through(*ai)) + if ( one_chance_in(++num_spots) ) + child_spot = *ai; + + if ( num_spots == 0 ) + return (false); + + int k = 0; + + // Now that we have a spot, find a monster slot {dlb}: + for (k = 0; k < MAX_MONSTERS; k++) + { + child = &menv[k]; + + if (child->type == -1) + break; + else if (k == MAX_MONSTERS - 1) + return (false); + } + + // Handle impact of split on parent {dlb}: + parent->max_hit_points /= 2; + + if (parent->hit_points > parent->max_hit_points) + parent->hit_points = parent->max_hit_points; + + parent->init_experience(); + parent->experience = parent->experience * 3 / 5 + 1; + + // Create child {dlb}: + // This is terribly partial and really requires + // more thought as to generation ... {dlb} + *child = *parent; + child->max_hit_points = child->hit_points; + child->speed_increment = 70 + random2(5); + child->moveto(child_spot); + + mgrd(child->pos()) = k; + + if (!simple_monster_message(parent, " splits in two!")) + if (player_can_hear(parent->pos()) || player_can_hear(child->pos())) + mpr("You hear a squelching noise.", MSGCH_SOUND); + + if (crawl_state.arena) + arena_placed_monster(child); + + return (true); +} + +// XXX: This function assumes that only jellies eat items. +static bool _monster_eat_item(monsters *monster, bool nearby) +{ + if (!mons_eats_items(monster)) + return (false); + + // Friendly jellies won't eat (unless worshipping Jiyva). + if (mons_friendly(monster) && you.religion != GOD_JIYVA) + return (false); + + int hps_gained = 0; + int max_eat = roll_dice(1, 10); + int eaten = 0; + bool eaten_net = false; + + for (stack_iterator si(monster->pos()); + si && eaten < max_eat && hps_gained < 50; ++si) + { + if (!is_item_jelly_edible(*si)) + continue; + +#if DEBUG_DIAGNOSTICS || DEBUG_EATERS + mprf(MSGCH_DIAGNOSTICS, + "%s eating %s", monster->name(DESC_PLAIN, true).c_str(), + si->name(DESC_PLAIN).c_str()); +#endif + + int quant = si->quantity; + + if (si->base_type != OBJ_GOLD) + { + quant = std::min(quant, max_eat - eaten); + + hps_gained += (quant * item_mass(*si)) / 20 + quant; + eaten += quant; + + if (mons_is_caught(monster) + && si->base_type == OBJ_MISSILES + && si->sub_type == MI_THROWING_NET + && item_is_stationary(*si)) + { + monster->del_ench(ENCH_HELD, true); + eaten_net = true; + } + } + else + { + // Shouldn't be much trouble to digest a huge pile of gold! + if (quant > 500) + quant = 500 + roll_dice(2, (quant - 500) / 2); + + hps_gained += quant / 10 + 1; + eaten++; + } + + if (you.religion == GOD_JIYVA) + { + const int quantity = si->quantity; + const int value = item_value(*si) / quantity; + int pg = 0; + int timeout = 0; + + for (int m = 0; m < quantity; ++m) + { + if (x_chance_in_y(value / 2 + 1, 30 + you.piety / 4)) + { + if (timeout <= 0) + pg += random2(item_value(*si) / 6); + else + timeout -= value / 5; + } + } + + if (pg > 0) + { + simple_god_message(" appreciates your sacrifice."); + gain_piety(pg); + } + + if (you.piety > 80 && random2(you.piety) > 50 && one_chance_in(4)) + { + if (you.can_safely_mutate()) + { + simple_god_message(" alters your body."); + + bool success = false; + const int rand = random2(100); + + if (rand < 40) + success = mutate(RANDOM_MUTATION, true, false, true); + else if (rand < 60) + { + success = delete_mutation(RANDOM_MUTATION, true, false, + true); + } + else + { + success = mutate(RANDOM_GOOD_MUTATION, true, false, + true); + } + + if (success) + { + timeout = (100 + roll_dice(2, 4)); + you.num_gifts[you.religion]++; + take_note(Note(NOTE_GOD_GIFT, you.religion)); + } + else + mpr("You feel as though nothing has changed."); + } + } + } + + if (quant >= si->quantity) + item_was_destroyed(*si, monster->mindex()); + + dec_mitm_item_quantity(si.link(), quant); + } + + if (eaten > 0) + { + hps_gained = std::max(hps_gained, 1); + hps_gained = std::min(hps_gained, 50); + + // This is done manually instead of using heal_monster(), + // because that function doesn't work quite this way. -- bwr + monster->hit_points += hps_gained; + monster->max_hit_points = std::max(monster->hit_points, + monster->max_hit_points); + + if (player_can_hear(monster->pos())) + { + mprf(MSGCH_SOUND, "You hear a%s slurping noise.", + nearby ? "" : " distant"); + } + + if (eaten_net) + simple_monster_message(monster, " devours the net!"); + + _jelly_divide(monster); + } + + return (eaten > 0); +} + +static bool _monster_eat_single_corpse(monsters *monster, item_def& item, + bool do_heal, bool nearby) +{ + if (item.base_type != OBJ_CORPSES || item.sub_type != CORPSE_BODY) + return (false); + + monster_type mt = static_cast(item.plus); + if (do_heal) + { + monster->hit_points += 1 + random2(mons_weight(mt)) / 100; + + // Limited growth factor here - should 77 really be the cap? {dlb}: + monster->hit_points = std::min(100, monster->hit_points); + monster->max_hit_points = std::max(monster->hit_points, + monster->max_hit_points); + } + + if (nearby) + { + mprf("%s eats %s.", monster->name(DESC_CAP_THE).c_str(), + item.name(DESC_NOCAP_THE).c_str()); + } + + // Assume that eating a corpse requires butchering it. Use logic + // from misc.cc:turn_corpse_into_chunks() and the butchery-related + // delays in delay.cc:stop_delay(). + + const int max_chunks = mons_weight(mt) / 150; + + // Only fresh corpses bleed enough to colour the ground. + if (!food_is_rotten(item)) + bleed_onto_floor(monster->pos(), mt, max_chunks, true); + + if (mons_skeleton(mt) && one_chance_in(3)) + turn_corpse_into_skeleton(item); + else + destroy_item(item.index()); + + return (true); +} + +static bool _monster_eat_corpse(monsters *monster, bool do_heal, bool nearby) +{ + if (!mons_eats_corpses(monster)) + return (false); + + int eaten = 0; + + for (stack_iterator si(monster->pos()); si; ++si) + { + if (_monster_eat_single_corpse(monster, *si, do_heal, nearby)) + { + eaten++; + break; + } + } + + return (eaten > 0); +} + +static bool _monster_eat_food(monsters *monster, bool nearby) +{ + if (!mons_eats_food(monster)) + return (false); + + if (mons_is_fleeing(monster)) + return (false); + + int eaten = 0; + + for (stack_iterator si(monster->pos()); si; ++si) + { + const bool is_food = (si->base_type == OBJ_FOOD); + const bool is_corpse = (si->base_type == OBJ_CORPSES + && si->sub_type == CORPSE_BODY); + + if (!is_food && !is_corpse) + continue; + + if ((mons_wont_attack(monster) + || grid_distance(monster->pos(), you.pos()) > 1) + && coinflip()) + { + if (is_food) + { + if (nearby) + { + mprf("%s eats %s.", monster->name(DESC_CAP_THE).c_str(), + quant_name(*si, 1, DESC_NOCAP_THE).c_str()); + } + + dec_mitm_item_quantity(si.link(), 1); + + eaten++; + break; + } + else + { + // Assume that only undead can heal from eating corpses. + if (_monster_eat_single_corpse(monster, *si, + monster->holiness() == MH_UNDEAD, + nearby)) + { + eaten++; + break; + } + } + } + } + + return (eaten > 0); +} + +//--------------------------------------------------------------- +// +// handle_pickup +// +// Returns false if monster doesn't spend any time picking something up. +// +//--------------------------------------------------------------- +static bool _handle_pickup(monsters *monster) +{ + if (monster->asleep() || monster->submerged()) + return (false); + + const bool nearby = mons_near(monster); + int count_pickup = 0; + + if (mons_itemeat(monster) != MONEAT_NOTHING) + { + if (mons_eats_items(monster)) + { + if (_monster_eat_item(monster, nearby)) + return (false); + } + else if (mons_eats_corpses(monster)) + { + // Assume that only undead can heal from eating corpses. + if (_monster_eat_corpse(monster, monster->holiness() == MH_UNDEAD, + nearby)) + { + return (false); + } + } + else if (mons_eats_food(monster)) + { + if (_monster_eat_food(monster, nearby)) + return (false); + } + } + + if (mons_itemuse(monster) >= MONUSE_WEAPONS_ARMOUR) + { + // Note: Monsters only look at stuff near the top of stacks. + // + // XXX: Need to put in something so that monster picks up + // multiple items (e.g. ammunition) identical to those it's + // carrying. + // + // Monsters may now pick up up to two items in the same turn. + // (jpeg) + for (stack_iterator si(monster->pos()); si; ++si) + { + if (monster->pickup_item(*si, nearby)) + count_pickup++; + + if (count_pickup > 1 || coinflip()) + break; + } + } + + return (count_pickup > 0); +} + +// Randomise potential damage. +static int _estimated_trap_damage(trap_type trap) +{ + switch (trap) + { + case TRAP_BLADE: return (10 + random2(30)); + case TRAP_DART: return (random2(4)); + case TRAP_ARROW: return (random2(7)); + case TRAP_SPEAR: return (random2(10)); + case TRAP_BOLT: return (random2(13)); + case TRAP_AXE: return (random2(15)); + default: return (0); + } +} + +// Check whether a given trap (described by trap position) can be +// regarded as safe. Takes into account monster intelligence and +// allegiance. +// (just_check is used for intelligent monsters trying to avoid traps.) +static bool _is_trap_safe(const monsters *monster, const coord_def& where, + bool just_check) +{ + const int intel = mons_intel(monster); + + const trap_def *ptrap = find_trap(where); + if (!ptrap) + return (true); + const trap_def& trap = *ptrap; + + const bool player_knows_trap = (trap.is_known(&you)); + + // No friendly monsters will ever enter a Zot trap you know. + if (player_knows_trap && mons_friendly(monster) && trap.type == TRAP_ZOT) + return (false); + + // Dumb monsters don't care at all. + if (intel == I_PLANT) + return (true); + + if (trap.type == TRAP_SHAFT && monster->will_trigger_shaft()) + { + if (mons_is_fleeing(monster) && intel >= I_NORMAL + || mons_is_pacified(monster)) + { + return (true); + } + return (false); + } + + // Hostile monsters are not afraid of non-mechanical traps. + // Allies will try to avoid teleportation and zot traps. + const bool mechanical = (trap.category() == DNGN_TRAP_MECHANICAL); + + if (trap.is_known(monster)) + { + if (just_check) + return (false); // Square is blocked. + else + { + // Test for corridor-like environment. + const int x = where.x - monster->pos().x; + const int y = where.y - monster->pos().y; + + // The question is whether the monster (m) can easily reach its + // presumable destination (x) without stepping on the trap. Traps + // in corridors do not allow this. See e.g + // #x# ## + // #^# or m^x + // m ## + // + // The same problem occurs if paths are blocked by monsters, + // hostile terrain or other traps rather than walls. + // What we do is check whether the squares with the relative + // positions (-1,0)/(+1,0) or (0,-1)/(0,+1) form a "corridor" + // (relative to the _trap_ position rather than the monster one). + // If they don't, the trap square is marked as "unsafe" (because + // there's a good alternative move for the monster to take), + // otherwise the decision will be made according to later tests + // (monster hp, trap type, ...) + // If a monster still gets stuck in a corridor it will usually be + // because it has less than half its maximum hp. + + if ((_mon_can_move_to_pos(monster, coord_def(x-1, y), true) + || _mon_can_move_to_pos(monster, coord_def(x+1,y), true)) + && (_mon_can_move_to_pos(monster, coord_def(x,y-1), true) + || _mon_can_move_to_pos(monster, coord_def(x,y+1), true))) + { + return (false); + } + } + } + + // Friendlies will try not to be parted from you. + if (intelligent_ally(monster) && trap.type == TRAP_TELEPORT + && player_knows_trap && mons_near(monster)) + { + return (false); + } + + // Healthy monsters don't mind a little pain. + if (mechanical && monster->hit_points >= monster->max_hit_points / 2 + && (intel == I_ANIMAL + || monster->hit_points > _estimated_trap_damage(trap.type))) + { + return (true); + } + + // Friendly and good neutral monsters don't enjoy Zot trap perks; + // handle accordingly. In the arena Zot traps affect all monsters. + if (mons_wont_attack(monster) || crawl_state.arena) + { + return (mechanical ? mons_flies(monster) + : !trap.is_known(monster) || trap.type != TRAP_ZOT); + } + else + return (!mechanical || mons_flies(monster)); +} + +static void _mons_open_door(monsters* monster, const coord_def &pos) +{ + dungeon_feature_type grid = grd(pos); + const char *adj = "", *noun = "door"; + + bool was_secret = false; + bool was_seen = false; + + std::set all_door; + find_connected_range(pos, DNGN_CLOSED_DOOR, DNGN_SECRET_DOOR, all_door); + get_door_description(all_door.size(), &adj, &noun); + + for (std::set::iterator i = all_door.begin(); + i != all_door.end(); ++i) + { + const coord_def& dc = *i; + if (grd(dc) == DNGN_SECRET_DOOR && see_cell(dc)) + { + grid = grid_secret_door_appearance(dc); + was_secret = true; + } + + if (see_cell(dc)) + was_seen = true; + else + set_terrain_changed(dc); + + grd[dc.x][dc.y] = DNGN_OPEN_DOOR; + } + + if (was_seen) + { + viewwindow(true, false); + + if (was_secret) + { + mprf("%s was actually a secret door!", + feature_description(grid, NUM_TRAPS, false, + DESC_CAP_THE, false).c_str()); + learned_something_new(TUT_SEEN_SECRET_DOOR, pos); + } + + std::string open_str = "opens the "; + open_str += adj; + open_str += noun; + open_str += "."; + + monster->seen_context = open_str; + + if (!you.can_see(monster)) + { + mprf("Something unseen %s", open_str.c_str()); + interrupt_activity(AI_FORCE_INTERRUPT); + } + else if (!you_are_delayed()) + { + mprf("%s %s", monster->name(DESC_CAP_A).c_str(), + open_str.c_str()); + } + } + + monster->lose_energy(EUT_MOVE); +} + +static bool _habitat_okay( const monsters *monster, dungeon_feature_type targ ) +{ + return (monster_habitable_grid(monster, targ)); +} + +static bool _no_habitable_adjacent_grids(const monsters *mon) +{ + for (adjacent_iterator ai(mon->pos()); ai; ++ai) + if (_habitat_okay(mon, grd(*ai))) + return (false); + + return (true); +} + +static bool _mons_can_displace(const monsters *mpusher, + const monsters *mpushee) +{ + if (invalid_monster(mpusher) || invalid_monster(mpushee)) + return (false); + + const int ipushee = monster_index(mpushee); + if (invalid_monster_index(ipushee)) + return (false); + + if (immobile_monster[ipushee]) + return (false); + + // Confused monsters can't be pushed past, sleeping monsters + // can't push. Note that sleeping monsters can't be pushed + // past, either, but they may be woken up by a crowd trying to + // elbow past them, and the wake-up check happens downstream. + if (mons_is_confused(mpusher) || mons_is_confused(mpushee) + || mons_cannot_move(mpusher) || mons_cannot_move(mpushee) + || mons_is_stationary(mpusher) || mons_is_stationary(mpushee) + || mpusher->asleep()) + { + return (false); + } + + // Batty monsters are unpushable. + if (mons_is_batty(mpusher) || mons_is_batty(mpushee)) + return (false); + + if (!monster_shover(mpusher)) + return (false); + + // Fleeing monsters of the same type may push past higher ranking ones. + if (!monster_senior(mpusher, mpushee, mons_is_fleeing(mpusher))) + return (false); + + return (true); +} + +// Check whether a monster can move to given square (described by its relative +// coordinates to the current monster position). just_check is true only for +// calls from is_trap_safe when checking the surrounding squares of a trap. +static bool _mon_can_move_to_pos(const monsters *monster, + const coord_def& delta, bool just_check) +{ + const coord_def targ = monster->pos() + delta; + + // Bounds check: don't consider moving out of grid! + if (!in_bounds(targ)) + return (false); + + // No monster may enter the open sea. + if (grd(targ) == DNGN_OPEN_SEA) + return (false); + + // Non-friendly and non-good neutral monsters won't enter + // sanctuaries. + if (!mons_wont_attack(monster) + && is_sanctuary(targ) + && !is_sanctuary(monster->pos())) + { + return (false); + } + + // Inside a sanctuary don't attack anything! + if (is_sanctuary(monster->pos()) && actor_at(targ)) + return (false); + + const dungeon_feature_type target_grid = grd(targ); + const habitat_type habitat = mons_primary_habitat(monster); + + // The kraken is so large it cannot enter shallow water. + // Its tentacles can, and will, though. + if (monster->type == MONS_KRAKEN && target_grid == DNGN_SHALLOW_WATER) + return (false); + + // Effectively slows down monster movement across water. + // Fire elementals can't cross at all. + bool no_water = false; + if (monster->type == MONS_FIRE_ELEMENTAL || one_chance_in(5)) + no_water = true; + + cloud_type targ_cloud_type; + const int targ_cloud_num = env.cgrid(targ); + + if (mons_avoids_cloud(monster, targ_cloud_num, &targ_cloud_type)) + return (false); + + if (mons_class_flag(monster->type, M_BURROWS) + && (target_grid == DNGN_ROCK_WALL + || target_grid == DNGN_CLEAR_ROCK_WALL)) + { + // Don't burrow out of bounds. + if (!in_bounds(targ)) + return (false); + + // Don't burrow at an angle (legacy behaviour). + if (delta.x != 0 && delta.y != 0) + return (false); + } + else if (!monster->can_pass_through_feat(target_grid) + || no_water && feat_is_water(target_grid)) + { + return (false); + } + else if (!_habitat_okay(monster, target_grid)) + { + // If the monster somehow ended up in this habitat (and is + // not dead by now), give it a chance to get out again. + if (grd(monster->pos()) == target_grid + && _no_habitable_adjacent_grids(monster)) + { + return (true); + } + + return (false); + } + + // Wandering mushrooms don't move while you are looking. + if (monster->type == MONS_WANDERING_MUSHROOM && see_cell(targ)) + return (false); + + // Water elementals avoid fire and heat. + if (monster->type == MONS_WATER_ELEMENTAL + && (target_grid == DNGN_LAVA + || targ_cloud_type == CLOUD_FIRE + || targ_cloud_type == CLOUD_FOREST_FIRE + || targ_cloud_type == CLOUD_STEAM)) + { + return (false); + } + + // Fire elementals avoid water and cold. + if (monster->type == MONS_FIRE_ELEMENTAL + && (feat_is_watery(target_grid) + || targ_cloud_type == CLOUD_COLD)) + { + return (false); + } + + // Submerged water creatures avoid the shallows where + // they would be forced to surface. -- bwr + // [dshaligram] Monsters now prefer to head for deep water only if + // they're low on hitpoints. No point in hiding if they want a + // fight. + if (habitat == HT_WATER + && targ != you.pos() + && target_grid != DNGN_DEEP_WATER + && grd(monster->pos()) == DNGN_DEEP_WATER + && monster->hit_points < (monster->max_hit_points * 3) / 4) + { + return (false); + } + + // Smacking the player is always a good move if we're + // hostile (even if we're heading somewhere else). + // Also friendlies want to keep close to the player + // so it's okay as well. + + // Smacking another monster is good, if the monsters + // are aligned differently. + if (monsters *targmonster = monster_at(targ)) + { + if (just_check) + { + if (targ == monster->pos()) + return (true); + + return (false); // blocks square + } + + if (mons_aligned(monster->mindex(), targmonster->mindex()) + && !_mons_can_displace(monster, targmonster)) + { + return (false); + } + } + + // Friendlies shouldn't try to move onto the player's + // location, if they are aiming for some other target. + if (mons_wont_attack(monster) + && monster->foe != MHITYOU + && (monster->foe != MHITNOT || monster->is_patrolling()) + && targ == you.pos()) + { + return (false); + } + + // Wandering through a trap is OK if we're pretty healthy, + // really stupid, or immune to the trap. + if (!_is_trap_safe(monster, targ, just_check)) + return (false); + + // If we end up here the monster can safely move. + return (true); +} + +// Uses, and updates the global variable mmov. +static void _find_good_alternate_move(monsters *monster, + const FixedArray& good_move) +{ + const int current_distance = distance(monster->pos(), monster->target); + + int dir = -1; + for (int i = 0; i < 8; i++) + { + if (mon_compass[i] == mmov) + { + dir = i; + break; + } + } + + // Only handle if the original move is to an adjacent square. + if (dir == -1) + return; + + int dist[2]; + + // First 1 away, then 2 (3 is silly). + for (int j = 1; j <= 2; j++) + { + const int FAR_AWAY = 1000000; + + // Try both directions (but randomise which one is first). + const int sdir = coinflip() ? j : -j; + const int inc = -2 * sdir; + + for (int mod = sdir, i = 0; i < 2; mod += inc, i++) + { + const int newdir = (dir + 8 + mod) % 8; + if (good_move[mon_compass[newdir].x+1][mon_compass[newdir].y+1]) + { + dist[i] = distance(monster->pos()+mon_compass[newdir], + monster->target); + } + else + { + dist[i] = (mons_is_fleeing(monster)) ? (-FAR_AWAY) : FAR_AWAY; + } + } + + const int dir0 = ((dir + 8 + sdir) % 8); + const int dir1 = ((dir + 8 - sdir) % 8); + + // Now choose. + if (dist[0] == dist[1] && abs(dist[0]) == FAR_AWAY) + continue; + + // Which one was better? -- depends on FLEEING or not. + if (mons_is_fleeing(monster)) + { + if (dist[0] >= dist[1] && dist[0] >= current_distance) + { + mmov = mon_compass[dir0]; + break; + } + if (dist[1] >= dist[0] && dist[1] >= current_distance) + { + mmov = mon_compass[dir1]; + break; + } + } + else + { + if (dist[0] <= dist[1] && dist[0] <= current_distance) + { + mmov = mon_compass[dir0]; + break; + } + if (dist[1] <= dist[0] && dist[1] <= current_distance) + { + mmov = mon_compass[dir1]; + break; + } + } + } +} + +static void _jelly_grows(monsters *monster) +{ + if (player_can_hear(monster->pos())) + { + mprf(MSGCH_SOUND, "You hear a%s slurping noise.", + mons_near(monster) ? "" : " distant"); + } + + monster->hit_points += 5; + + // note here, that this makes jellies "grow" {dlb}: + if (monster->hit_points > monster->max_hit_points) + monster->max_hit_points = monster->hit_points; + + _jelly_divide(monster); +} + +static bool _monster_swaps_places( monsters *mon, const coord_def& delta ) +{ + if (delta.origin()) + return (false); + + monsters* const m2 = monster_at(mon->pos() + delta); + + if (!m2) + return (false); + + if (!_mons_can_displace(mon, m2)) + return (false); + + if (m2->asleep()) + { + if (coinflip()) + { +#ifdef DEBUG_DIAGNOSTICS + mprf(MSGCH_DIAGNOSTICS, + "Alerting monster %s at (%d,%d)", + m2->name(DESC_PLAIN).c_str(), m2->pos().x, m2->pos().y); +#endif + behaviour_event(m2, ME_ALERT, MHITNOT); + } + return (false); + } + + // Check that both monsters will be happy at their proposed new locations. + const coord_def c = mon->pos(); + const coord_def n = mon->pos() + delta; + + if (!_habitat_okay(mon, grd(n)) || !_habitat_okay(m2, grd(c))) + return (false); + + // Okay, do the swap! + _swim_or_move_energy(mon); + + mon->pos() = n; + mgrd(n) = monster_index(mon); + m2->pos() = c; + const int m2i = monster_index(m2); + ASSERT(m2i >= 0 && m2i < MAX_MONSTERS); + mgrd(c) = m2i; + immobile_monster[m2i] = true; + + mon->check_redraw(c); + mon->apply_location_effects(c); + m2->check_redraw(c); + m2->apply_location_effects(n); + + // The seen context no longer applies if the monster is moving normally. + mon->seen_context.clear(); + m2->seen_context.clear(); + + return (false); +} + +static bool _do_move_monster(monsters *monster, const coord_def& delta) +{ + const coord_def f = monster->pos() + delta; + + if (!in_bounds(f)) + return (false); + + if (f == you.pos()) + { + monster_attack(monster); + return (true); + } + + // This includes the case where the monster attacks itself. + if (monsters* def = monster_at(f)) + { + monsters_fight(monster, def); + return (true); + } + + // The monster gave a "comes into view" message and then immediately + // moved back out of view, leaing the player nothing to see, so give + // this message to avoid confusion. + if (monster->seen_context == _just_seen && !see_cell(f)) + simple_monster_message(monster, " moves out of view."); + else if (Options.tutorial_left && (monster->flags & MF_WAS_IN_VIEW) + && !see_cell(f)) + { + learned_something_new(TUT_MONSTER_LEFT_LOS, monster->pos()); + } + + // The seen context no longer applies if the monster is moving normally. + monster->seen_context.clear(); + + // This appears to be the real one, ie where the movement occurs: + _swim_or_move_energy(monster); + + if (grd(monster->pos()) == DNGN_DEEP_WATER && grd(f) != DNGN_DEEP_WATER + && !monster_habitable_grid(monster, DNGN_DEEP_WATER)) + { + monster->seen_context = "emerges from the water"; + } + mgrd(monster->pos()) = NON_MONSTER; + + monster->pos() = f; + + mgrd(monster->pos()) = monster_index(monster); + + monster->check_redraw(monster->pos() - delta); + monster->apply_location_effects(monster->pos() - delta); + + return (true); +} + +static bool _monster_move(monsters *monster) +{ + FixedArray good_move; + + const habitat_type habitat = mons_primary_habitat(monster); + bool deep_water_available = false; + + if (monster->type == MONS_TRAPDOOR_SPIDER) + { + if (monster->submerged()) + return (false); + + // Trapdoor spiders hide if they can't see their foe. + // (Note that friendly trapdoor spiders will thus hide even + // if they can see you.) + const actor *foe = monster->get_foe(); + const bool can_see = foe && monster->can_see(foe); + + if (monster_can_submerge(monster, grd(monster->pos())) + && !can_see && !mons_is_confused(monster) + && !monster->caught() + && !monster->has_ench(ENCH_BERSERK)) + { + monster->add_ench(ENCH_SUBMERGED); + monster->behaviour = BEH_LURK; + return (false); + } + } + + // Berserking monsters make a lot of racket. + if (monster->has_ench(ENCH_BERSERK)) + { + int noise_level = get_shout_noise_level(mons_shouts(monster->type)); + if (noise_level > 0) + { + if (you.can_see(monster)) + { + if (one_chance_in(10)) + { + mprf(MSGCH_TALK_VISUAL, "%s rages.", + monster->name(DESC_CAP_THE).c_str()); + } + noisy(noise_level, monster->pos(), monster->mindex()); + } + else if (one_chance_in(5)) + handle_monster_shouts(monster, true); + else + { + // Just be noisy without messaging the player. + noisy(noise_level, monster->pos(), monster->mindex()); + } + } + } + + if (monster->confused()) + { + if (!mmov.origin() || one_chance_in(15)) + { + const coord_def newpos = monster->pos() + mmov; + if (in_bounds(newpos) + && (habitat == HT_LAND + || monster_habitable_grid(monster, grd(newpos)))) + { + return _do_move_monster(monster, mmov); + } + } + return (false); + } + + // If a water monster is currently flopping around on land, it cannot + // really control where it wants to move, though there's a 50% chance + // of flopping into an adjacent water grid. + if (monster->has_ench(ENCH_AQUATIC_LAND)) + { + std::vector adj_water; + std::vector adj_move; + for (adjacent_iterator ai(monster->pos()); ai; ++ai) + { + if (!cell_is_solid(*ai)) + { + adj_move.push_back(*ai); + if (feat_is_watery(grd(*ai))) + adj_water.push_back(*ai); + } + } + if (adj_move.empty()) + { + simple_monster_message(monster, " flops around on dry land!"); + return (false); + } + + std::vector moves = adj_water; + if (adj_water.empty() || coinflip()) + moves = adj_move; + + coord_def newpos = monster->pos(); + int count = 0; + for (unsigned int i = 0; i < moves.size(); ++i) + if (one_chance_in(++count)) + newpos = moves[i]; + + const monsters *mon2 = monster_at(newpos); + if (newpos == you.pos() && mons_wont_attack(monster) + || (mon2 && mons_wont_attack(monster) == mons_wont_attack(mon2))) + { + + simple_monster_message(monster, " flops around on dry land!"); + return (false); + } + + return _do_move_monster(monster, newpos - monster->pos()); + } + + // Let's not even bother with this if mmov is zero. + if (mmov.origin()) + return (false); + + for (int count_x = 0; count_x < 3; count_x++) + for (int count_y = 0; count_y < 3; count_y++) + { + const int targ_x = monster->pos().x + count_x - 1; + const int targ_y = monster->pos().y + count_y - 1; + + // Bounds check: don't consider moving out of grid! + if (!in_bounds(targ_x, targ_y)) + { + good_move[count_x][count_y] = false; + continue; + } + dungeon_feature_type target_grid = grd[targ_x][targ_y]; + + if (target_grid == DNGN_DEEP_WATER) + deep_water_available = true; + + good_move[count_x][count_y] = + _mon_can_move_to_pos(monster, coord_def(count_x-1, count_y-1)); + } + + // Now we know where we _can_ move. + + const coord_def newpos = monster->pos() + mmov; + // Normal/smart monsters know about secret doors, since they live in + // the dungeon. + if (grd(newpos) == DNGN_CLOSED_DOOR + || feat_is_secret_door(grd(newpos)) && mons_intel(monster) >= I_NORMAL) + { + if (mons_is_zombified(monster)) + { + // For zombies, monster type is kept in mon->base_monster. + if (mons_class_itemuse(monster->base_monster) >= MONUSE_OPEN_DOORS) + { + _mons_open_door(monster, newpos); + return (true); + } + } + else if (mons_itemuse(monster) >= MONUSE_OPEN_DOORS) + { + _mons_open_door(monster, newpos); + return (true); + } + } // endif - secret/closed doors + + // Monsters that eat items (currently only jellies) also eat doors. + // However, they don't realise that secret doors make good eating. + if ((grd(newpos) == DNGN_CLOSED_DOOR || grd(newpos) == DNGN_OPEN_DOOR) + && mons_itemeat(monster) == MONEAT_ITEMS + // Doors with permarock marker cannot be eaten. + && !feature_marker_at(newpos, DNGN_PERMAROCK_WALL)) + { + grd(newpos) = DNGN_FLOOR; + + _jelly_grows(monster); + + if (see_cell(newpos)) + { + viewwindow(true, false); + + if (!you.can_see(monster)) + { + mpr("The door mysteriously vanishes."); + interrupt_activity( AI_FORCE_INTERRUPT ); + } + } + } // done door-eating jellies + + // Water creatures have a preference for water they can hide in -- bwr + // [ds] Weakened the powerful attraction to deep water if the monster + // is in good health. + if (habitat == HT_WATER + && deep_water_available + && grd(monster->pos()) != DNGN_DEEP_WATER + && grd(newpos) != DNGN_DEEP_WATER + && newpos != you.pos() + && (one_chance_in(3) + || monster->hit_points <= (monster->max_hit_points * 3) / 4)) + { + int count = 0; + + for (int cx = 0; cx < 3; cx++) + for (int cy = 0; cy < 3; cy++) + { + if (good_move[cx][cy] + && grd[monster->pos().x + cx - 1][monster->pos().y + cy - 1] + == DNGN_DEEP_WATER) + { + if (one_chance_in(++count)) + { + mmov.x = cx - 1; + mmov.y = cy - 1; + } + } + } + } + + // Now, if a monster can't move in its intended direction, try + // either side. If they're both good, move in whichever dir + // gets it closer (farther for fleeing monsters) to its target. + // If neither does, do nothing. + if (good_move[mmov.x + 1][mmov.y + 1] == false) + _find_good_alternate_move(monster, good_move); + + // ------------------------------------------------------------------ + // If we haven't found a good move by this point, we're not going to. + // ------------------------------------------------------------------ + + // Take care of beetle burrowing. + if (mons_class_flag(monster->type, M_BURROWS)) + { + const dungeon_feature_type feat = grd(monster->pos() + mmov); + if ((feat == DNGN_ROCK_WALL || feat == DNGN_CLEAR_ROCK_WALL) + && good_move[mmov.x + 1][mmov.y + 1] == true) + { + grd(monster->pos() + mmov) = DNGN_FLOOR; + set_terrain_changed(monster->pos() + mmov); + + if (player_can_hear(monster->pos() + mmov)) + { + // Message depends on whether caused by boring beetle or + // acid (Dissolution). + mpr((monster->type == MONS_BORING_BEETLE) ? + "You hear a grinding noise." : + "You hear a sizzling sound.", MSGCH_SOUND); + } + } + } + + bool ret = false; + if (good_move[mmov.x + 1][mmov.y + 1] && !mmov.origin()) + { + // Check for attacking player. + if (monster->pos() + mmov == you.pos()) + { + ret = monster_attack(monster); + mmov.reset(); + } + + // If we're following the player through stairs, the only valid + // movement is towards the player. -- bwr + if (testbits(monster->flags, MF_TAKING_STAIRS)) + { + const delay_type delay = current_delay_action(); + if (delay != DELAY_ASCENDING_STAIRS + && delay != DELAY_DESCENDING_STAIRS) + { + monster->flags &= ~MF_TAKING_STAIRS; + +#ifdef DEBUG_DIAGNOSTICS + mprf(MSGCH_DIAGNOSTICS, + "BUG: %s was marked as follower when not following!", + monster->name(DESC_PLAIN).c_str(), true); +#endif + } + else + { + ret = true; + mmov.reset(); + +#if DEBUG_DIAGNOSTICS + mprf(MSGCH_DIAGNOSTICS, + "%s is skipping movement in order to follow.", + monster->name(DESC_CAP_THE).c_str(), true ); +#endif + } + } + + // Check for attacking another monster. + if (monsters* targ = monster_at(monster->pos() + mmov)) + { + if (mons_aligned(monster->mindex(), targ->mindex())) + ret = _monster_swaps_places(monster, mmov); + else + { + monsters_fight(monster, targ); + ret = true; + } + + // If the monster swapped places, the work's already done. + mmov.reset(); + } + + if (monster->type == MONS_EFREET + || monster->type == MONS_FIRE_ELEMENTAL) + { + place_cloud( CLOUD_FIRE, monster->pos(), + 2 + random2(4), monster->kill_alignment() ); + } + + if (monster->type == MONS_ROTTING_DEVIL + || monster->type == MONS_CURSE_TOE) + { + place_cloud( CLOUD_MIASMA, monster->pos(), + 2 + random2(3), monster->kill_alignment() ); + } + } + else + { + mmov.reset(); + + // Fleeing monsters that can't move will panic and possibly + // turn to face their attacker. + make_mons_stop_fleeing(monster); + } + + if (mmov.x || mmov.y || (monster->confused() && one_chance_in(6))) + return (_do_move_monster(monster, mmov)); + + return (ret); +} + +static void _mons_in_cloud(monsters *monster) +{ + int wc = env.cgrid(monster->pos()); + int hurted = 0; + bolt beam; + + const int speed = ((monster->speed > 0) ? monster->speed : 10); + bool wake = false; + + if (mons_is_mimic( monster->type )) + { + mimic_alert(monster); + return; + } + + const cloud_struct &cloud(env.cloud[wc]); + switch (cloud.type) + { + case CLOUD_DEBUGGING: + mprf(MSGCH_ERROR, + "Monster %s stepped on a nonexistent cloud at (%d,%d)", + monster->name(DESC_PLAIN, true).c_str(), + monster->pos().x, monster->pos().y); + return; + + case CLOUD_FIRE: + case CLOUD_FOREST_FIRE: + if (monster->type == MONS_FIRE_VORTEX + || monster->type == MONS_EFREET + || monster->type == MONS_FIRE_ELEMENTAL) + { + return; + } + + simple_monster_message(monster, " is engulfed in flames!"); + + hurted += + resist_adjust_damage( monster, + BEAM_FIRE, + monster->res_fire(), + ((random2avg(16, 3) + 6) * 10) / speed ); + + hurted -= random2(1 + monster->ac); + break; + + case CLOUD_STINK: + simple_monster_message(monster, " is engulfed in noxious gasses!"); + + if (monster->res_poison() > 0) + return; + + beam.flavour = BEAM_CONFUSION; + beam.thrower = cloud.killer; + + if (cloud.whose == KC_FRIENDLY) + beam.beam_source = ANON_FRIENDLY_MONSTER; + + if (mons_class_is_confusable(monster->type) + && 1 + random2(27) >= monster->hit_dice) + { + beam.apply_enchantment_to_monster(monster); + } + + hurted += (random2(3) * 10) / speed; + break; + + case CLOUD_COLD: + simple_monster_message(monster, " is engulfed in freezing vapours!"); + + hurted += + resist_adjust_damage( monster, + BEAM_COLD, + monster->res_cold(), + ((6 + random2avg(16, 3)) * 10) / speed ); + + hurted -= random2(1 + monster->ac); + break; + + case CLOUD_POISON: + simple_monster_message(monster, " is engulfed in a cloud of poison!"); + + if (monster->res_poison() > 0) + return; + + poison_monster(monster, cloud.whose); + // If the monster got poisoned, wake it up. + wake = true; + + hurted += (random2(8) * 10) / speed; + + if (monster->res_poison() < 0) + hurted += (random2(4) * 10) / speed; + break; + + case CLOUD_STEAM: + { + // FIXME: couldn't be bothered coding for armour of res fire + + simple_monster_message(monster, " is engulfed in steam!"); + + const int steam_base_damage = steam_cloud_damage(cloud); + hurted += + resist_adjust_damage( + monster, + BEAM_STEAM, + monster->res_steam(), + (random2avg(steam_base_damage, 2) * 10) / speed); + + hurted -= random2(1 + monster->ac); + break; + } + + case CLOUD_MIASMA: + simple_monster_message(monster, " is engulfed in a dark miasma!"); + + if (monster->res_rotting()) + return; + + miasma_monster(monster, cloud.whose); + + hurted += (10 * random2avg(12, 3)) / speed; // 3 + break; + + case CLOUD_RAIN: + if (monster->is_fiery()) + { + if (!silenced(monster->pos())) + simple_monster_message(monster, " sizzles in the rain!"); + else + simple_monster_message(monster, " steams in the rain!"); + + hurted += ((4 * random2(3)) - random2(monster->ac)); + wake = true; + } + break; + + case CLOUD_MUTAGENIC: + simple_monster_message(monster, " is engulfed in a mutagenic fog!"); + + // Will only polymorph a monster if they're not magic immune, can + // mutate, aren't res asphyx, and pass the same check as meph cloud. + if (monster->can_mutate() && !mons_immune_magic(monster) + && 1 + random2(27) >= monster->hit_dice + && !monster->res_asphyx()) + { + if (monster->mutate()) + wake = true; + } + break; + + default: // 'harmless' clouds -- colored smoke, etc {dlb}. + return; + } + + // A sleeping monster that sustains damage will wake up. + if ((wake || hurted > 0) && monster->asleep()) + { + // We have no good coords to give the monster as the source of the + // disturbance other than the cloud itself. + behaviour_event(monster, ME_DISTURB, MHITNOT, monster->pos()); + } + + if (hurted > 0) + { +#ifdef DEBUG_DIAGNOSTICS + mprf(MSGCH_DIAGNOSTICS, "%s takes %d damage from cloud.", + monster->name(DESC_CAP_THE).c_str(), hurted); +#endif + monster->hurt(NULL, hurted, BEAM_MISSILE, false); + + if (monster->hit_points < 1) + { + mon_enchant death_ench(ENCH_NONE, 0, cloud.whose); + monster_die(monster, cloud.killer, death_ench.kill_agent()); + } + } +} + +static spell_type _map_wand_to_mspell(int wand_type) +{ + switch (wand_type) + { + case WAND_FLAME: return SPELL_THROW_FLAME; + case WAND_FROST: return SPELL_THROW_FROST; + case WAND_SLOWING: return SPELL_SLOW; + case WAND_HASTING: return SPELL_HASTE; + case WAND_MAGIC_DARTS: return SPELL_MAGIC_DART; + case WAND_HEALING: return SPELL_MINOR_HEALING; + case WAND_PARALYSIS: return SPELL_PARALYSE; + case WAND_FIRE: return SPELL_BOLT_OF_FIRE; + case WAND_COLD: return SPELL_BOLT_OF_COLD; + case WAND_CONFUSION: return SPELL_CONFUSE; + case WAND_INVISIBILITY: return SPELL_INVISIBILITY; + case WAND_TELEPORTATION: return SPELL_TELEPORT_OTHER; + case WAND_LIGHTNING: return SPELL_LIGHTNING_BOLT; + case WAND_DRAINING: return SPELL_BOLT_OF_DRAINING; + case WAND_DISINTEGRATION: return SPELL_DISINTEGRATE; + case WAND_POLYMORPH_OTHER: return SPELL_POLYMORPH_OTHER; + default: return SPELL_NO_SPELL; + } +} + diff --git a/crawl-ref/source/mon-act.h b/crawl-ref/source/mon-act.h new file mode 100644 index 0000000000..026d8a2f4b --- /dev/null +++ b/crawl-ref/source/mon-act.h @@ -0,0 +1,14 @@ +/* + * File: mon-act.h + * Summary: Monsters doing stuff (monsters acting). + * Written by: Linley Henzell + */ + +#ifndef MONACT_H +#define MONACT_H + +void handle_monsters(void); + +#define ENERGY_SUBMERGE(entry) (std::max(entry->energy_usage.swim / 2, 1)) + +#endif diff --git a/crawl-ref/source/mon-behv.cc b/crawl-ref/source/mon-behv.cc new file mode 100644 index 0000000000..1644b57c8a --- /dev/null +++ b/crawl-ref/source/mon-behv.cc @@ -0,0 +1,1928 @@ +/* + * File: mon-behv.h + * Summary: Monster behaviour functions. + * Written by: Linley Henzell + */ + +#include "AppHdr.h" +#include "mon-behv.h" + +#include "externs.h" + +#include "coord.h" +#include "coordit.h" +#include "exclude.h" +#include "los.h" +#include "monplace.h" +#include "monstuff.h" +#include "mon-util.h" +#include "random.h" +#include "state.h" +#include "terrain.h" +#include "traps.h" +#include "tutorial.h" +#include "view.h" + +static void _set_nearest_monster_foe(monsters *monster); + +// 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) + { + // XXX: remove explicit coordinate translation. + const coord_def ep = *ri - you.pos() + coord_def(ENV_SHOW_OFFSET, + ENV_SHOW_OFFSET); + 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_cell(*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 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); +} + +//#define DEBUG_PATHFIND + +// 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 + // realise 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 + || can_go_straight(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 (can_go_straight(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 (can_go_straight(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); +} + +static bool _is_level_exit(const coord_def& pos) +{ + // All types of stairs. + if (feat_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); +} + +// 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); + } + + 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 (!feat_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 (cell_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; + } + + 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 (!cell_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 (!can_go_straight(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 (can_go_straight(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); +} + +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_cell(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); + + los_def patrol(mon->patrol_point, opacity_monmove(*mon), bounds_radius(rad)); + patrol.update(); + los_def lm(mon->pos(), opacity_monmove(*mon)); + if (is_smart || !patrol_seen) + { + // For stupid monsters, don't bother if the patrol point is in sight. + lm.update(); + } + + 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.see_cell(*ri) && + (!is_smart || !lm.see_cell(*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.see_cell(*ri) || + !lm.see_cell(*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); +}// 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); +} + +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; + } +} + +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()); +} + +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); +} + +static void _set_random_slime_target(monsters* mon) +{ + // Strictly neutral slimes will go for the nearest item. + int item_idx; + coord_def orig_target = mon->target; + + for (radius_iterator ri(mon->pos(), LOS_RADIUS, true, false); ri; ++ri) + { + item_idx = igrd(*ri); + if (item_idx != NON_ITEM) + { + for (stack_iterator si(*ri); si; ++si) + { + item_def& item(*si); + + if (is_item_jelly_edible(item)) + { + mon->target = *ri; + break; + } + } + } + } + + if (mon->target == mon->pos() || mon->target == you.pos()) + set_random_target(mon); +} + +//--------------------------------------------------------------- +// +// 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. +//--------------------------------------------------------------- +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); + + // Whether the player is in LOS of the monster and can see + // or has guessed the player's location. + 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 targetting the player in arena mode. + else if (mon->foe == MHITYOU) + mon->foe = MHITNOT; + } + + if (mons_wall_shielded(mon) && cell_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.visible_to(mon)) + { + proxPlayer = false; + + const int intel = mons_intel(mon); + // 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. + 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->mons_species() != MONS_GIANT_SPORE ) + { + mon->foe = you.pet_target; + } + + // Instead, berserkers attack nearest monsters. + if ((mon->has_ench(ENCH_BERSERK) || mon->mons_species() == MONS_GIANT_SPORE) + && (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. + // Others choose the nearest foe. + if (!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) + { + actor* afoe = mon->get_foe(); + proxFoe = afoe && mon->can_see(afoe); + + coord_def foepos = coord_def(0,0); + if (afoe) + foepos = afoe->pos(); + + if (mon->foe == MHITYOU) + proxFoe = proxPlayer; // Take invis into account. + + // 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 (crawl_state.arena || !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 || crawl_state.arena) + { + new_foe = MHITNOT; + new_beh = BEH_WANDER; + } + else + { + new_foe = MHITYOU; + mon->target = foepos; + } + break; + } + + ASSERT(mon->foe != MHITNOT); + if (mon->foe_memory > 0) + { + // 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 = 0; + } + else + { + if (coinflip()) // XXX: cheesy! + mon->target = menv[mon->foe].pos(); + else + mon->foe_memory = 0; + } + } + + // Either keep chasing, or start wandering. + if (mon->foe_memory < 2) + { + mon->foe_memory = 0; + new_beh = BEH_WANDER; + } + break; + } + + ASSERT(mon->foe_memory == 0); + // Hack: smarter monsters will tend to pursue the player longer. + switch (mons_intel(mon)) + { + case I_HIGH: + mon->foe_memory = 100 + random2(200); + break; + case I_NORMAL: + mon->foe_memory = 50 + random2(100); + break; + case I_ANIMAL: + case I_INSECT: + mon->foe_memory = 25 + random2(75); + break; + case I_PLANT: + mon->foe_memory = 10 + random2(50); + break; + } + break; // switch/case BEH_SEEK + } + + ASSERT(proxFoe && mon->foe != MHITNOT); + // 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) + && mon->holiness() != MH_PLANT + && mon->holiness() != 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; + } + + if (mons_strict_neutral(mon) && mons_is_slime(mon) + && you.religion == GOD_JIYVA) + { + _set_random_slime_target(mon); + } + + // 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 and nonliving monsters cannot fight back. + if (mon->holiness() == MH_PLANT + || mon->holiness() == 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 && cell_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_foe(monsters *mon, const coord_def& p, + bool friendly, bool neutral) +{ + if (!inside_level_bounds(p)) + return (false); + + if (!friendly && !neutral && p == you.pos() + && you.visible_to(mon) && !is_sanctuary(p)) + { + return (true); + } + + if (monsters *foe = monster_at(p)) + { + if (foe != mon + && mon->can_see(foe) + && (friendly || !is_sanctuary(p)) + && (mons_friendly(foe) != friendly + || (neutral && !mons_neutral(foe)))) + { + return (true); + } + } + return (false); +} + +// Choose random nearest monster as a foe. +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) + { + std::vector monster_pos; + 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 (_mons_check_foe(mon, p, friendly, neutral)) + monster_pos.push_back(p); + } + if (monster_pos.empty()) + continue; + + const coord_def mpos = monster_pos[random2(monster_pos.size())]; + if (mpos == you.pos()) + mon->foe = MHITYOU; + else + mon->foe = env.mgrid(mpos); + return; + } +} + +///--------------------------------------------------------------- +// +// 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 = mon->asleep(); + + 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 (mon->asleep()) + { + mon->behaviour = BEH_WANDER; + + if (mons_near(mon)) + remove_auto_exclude(mon, true); + } + + // A bit of code to make Projected 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))) + { + // Monster types that you can't gain experience from cannot + // fight back, so don't bother having them do so. If you + // worship Feawn, create a ring of friendly plants, and try + // to break out of the ring by killing a plant, you'll get + // a warning prompt and penance only once. Without the + // hostility check, the plant will remain friendly until it + // dies, and you'll get a warning prompt and penance once + // *per hit*. This may not be the best way to address the + // issue, though. -cao + if (mons_class_flag(mon->type, M_NO_EXP_GAIN) + && mon->attitude != ATT_FRIENDLY + && mon->attitude != ATT_GOOD_NEUTRAL) + { + return; + } + + mon->foe = src; + + if (mon->asleep() && 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() + && !mon->asleep()) + { + break; + } + + if (mon->asleep() && 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_cell(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 (mon->holiness() == MH_PLANT + || mon->holiness() == 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) && !crawl_state.arena) + mon->foe = MHITYOU; + + if (see_cell(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) && !crawl_state.arena) + { + 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 && !mon->asleep() && 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()); +} + +void make_mons_stop_fleeing(monsters *mon) +{ + if (mons_is_fleeing(mon)) + behaviour_event(mon, ME_CORNERED); +} diff --git a/crawl-ref/source/mon-behv.h b/crawl-ref/source/mon-behv.h new file mode 100644 index 0000000000..9f3001657a --- /dev/null +++ b/crawl-ref/source/mon-behv.h @@ -0,0 +1,52 @@ +/* + * File: mon-behv.h + * Summary: Monster behaviour functions. + * Written by: Linley Henzell + */ + +#ifndef MONBEHV_H +#define MONBEHV_H + +#include "enum.h" + +enum mon_event_type +{ + ME_EVAL, // 0, evaluate monster AI state + ME_DISTURB, // noisy + ME_ANNOY, // annoy at range + ME_ALERT, // alert to presence + ME_WHACK, // physical attack + ME_SCARE, // frighten monster + ME_CORNERED // cannot flee +}; + +class monsters; +struct coord_def; + +void behaviour_event(monsters *mon, mon_event_type event_type, + int src = MHITNOT, coord_def src_pos = coord_def(), + bool allow_shout = true); + +// This function is somewhat low level; you should probably use +// behaviour_event(mon, ME_EVAL) instead. +void handle_behaviour(monsters *mon); + +void make_mons_stop_fleeing(monsters *mon); + +void set_random_target(monsters* mon); + +void make_mons_leave_level(monsters *mon); + +bool message_current_target(void); + +bool monster_can_hit_monster(monsters *monster, const monsters *targ); + +bool mons_avoids_cloud(const monsters *monster, cloud_type cl_type, + bool placement = false); + +// Like the above, but allow a monster to move from one damaging cloud +// to another. +bool mons_avoids_cloud(const monsters *monster, int cloud_num, + cloud_type *cl_type = NULL, bool placement = false); + +#endif diff --git a/crawl-ref/source/mon-cast.cc b/crawl-ref/source/mon-cast.cc new file mode 100644 index 0000000000..1524d0cb44 --- /dev/null +++ b/crawl-ref/source/mon-cast.cc @@ -0,0 +1,2357 @@ +/* + * File: mon-cast.cc + * Summary: Monster spell casting. + * Written by: Linley Henzell + */ + +#include "AppHdr.h" +#include "mon-cast.h" + +#ifdef TARGET_OS_DOS +#include +#endif + +#include "beam.h" +#include "colour.h" +#include "database.h" +#include "effects.h" +#include "fight.h" +#include "ghost.h" +#include "los.h" +#include "misc.h" +#include "mon-behv.h" +#include "monplace.h" +#include "monspeak.h" +#include "monstuff.h" +#include "mon-util.h" +#include "random.h" +#include "religion.h" +#include "spl-util.h" +#include "spl-cast.h" +#include "spells1.h" +#include "spells3.h" +#include "stuff.h" +#include "view.h" + +static void _scale_draconian_breath(bolt& beam, int drac_type) +{ + int scaling = 100; + switch (drac_type) + { + case MONS_RED_DRACONIAN: + beam.name = "searing blast"; + beam.aux_source = "blast of searing breath"; + scaling = 65; + break; + + case MONS_WHITE_DRACONIAN: + beam.name = "chilling blast"; + beam.aux_source = "blast of chilling breath"; + beam.short_name = "frost"; + scaling = 65; + break; + + case MONS_PLAYER_GHOST: // draconians only + beam.name = "blast of negative energy"; + beam.aux_source = "blast of draining breath"; + beam.flavour = BEAM_NEG; + beam.colour = DARKGREY; + scaling = 65; + break; + } + beam.damage.size = scaling * beam.damage.size / 100; +} + +static spell_type _draco_type_to_breath(int drac_type) +{ + switch (drac_type) + { + case MONS_BLACK_DRACONIAN: return SPELL_LIGHTNING_BOLT; + case MONS_MOTTLED_DRACONIAN: return SPELL_STICKY_FLAME_SPLASH; + case MONS_YELLOW_DRACONIAN: return SPELL_ACID_SPLASH; + case MONS_GREEN_DRACONIAN: return SPELL_POISONOUS_CLOUD; + case MONS_PURPLE_DRACONIAN: return SPELL_ISKENDERUNS_MYSTIC_BLAST; + case MONS_RED_DRACONIAN: return SPELL_FIRE_BREATH; + case MONS_WHITE_DRACONIAN: return SPELL_COLD_BREATH; + case MONS_PALE_DRACONIAN: return SPELL_STEAM_BALL; + + // Handled later. + case MONS_PLAYER_GHOST: return SPELL_DRACONIAN_BREATH; + + default: + DEBUGSTR("Invalid monster using draconian breath spell"); + break; + } + + return (SPELL_DRACONIAN_BREATH); +} + +bolt mons_spells( monsters *mons, spell_type spell_cast, int power ) +{ + ASSERT(power > 0); + + bolt beam; + + // Initialise to some bogus values so we can catch problems. + beam.name = "****"; + beam.colour = 1000; + beam.hit = -1; + beam.damage = dice_def( 1, 0 ); + beam.ench_power = -1; + beam.type = 0; + beam.flavour = BEAM_NONE; + beam.thrower = KILL_MISC; + beam.is_beam = false; + beam.is_explosion = false; + + // Sandblast is different, and gets range updated later + if (spell_cast != SPELL_SANDBLAST) + beam.range = spell_range(spell_cast, power, true, false); + + const int drac_type = (mons_genus(mons->type) == MONS_DRACONIAN) + ? draco_subspecies(mons) : mons->type; + + spell_type real_spell = spell_cast; + + if (spell_cast == SPELL_DRACONIAN_BREATH) + real_spell = _draco_type_to_breath(drac_type); + + beam.type = dchar_glyph(DCHAR_FIRED_ZAP); // default + beam.thrower = KILL_MON_MISSILE; + + // FIXME: this should use the zap_data[] struct from beam.cc! + switch (real_spell) + { + case SPELL_MAGIC_DART: + beam.colour = LIGHTMAGENTA; + beam.name = "magic dart"; + beam.damage = dice_def( 3, 4 + (power / 100) ); + beam.hit = AUTOMATIC_HIT; + beam.flavour = BEAM_MMISSILE; + break; + + case SPELL_THROW_FLAME: + beam.colour = RED; + beam.name = "puff of flame"; + beam.damage = dice_def( 3, 5 + (power / 40) ); + beam.hit = 25 + power / 40; + beam.flavour = BEAM_FIRE; + break; + + case SPELL_THROW_FROST: + beam.colour = WHITE; + beam.name = "puff of frost"; + beam.damage = dice_def( 3, 5 + (power / 40) ); + beam.hit = 25 + power / 40; + beam.flavour = BEAM_COLD; + break; + + case SPELL_SANDBLAST: + beam.colour = BROWN; + beam.name = "rocky blast"; + beam.damage = dice_def( 3, 5 + (power / 40) ); + beam.hit = 20 + power / 40; + beam.flavour = BEAM_FRAG; + beam.range = 2; // spell_range() is wrong here + break; + + case SPELL_DISPEL_UNDEAD: + beam.flavour = BEAM_DISPEL_UNDEAD; + beam.damage = dice_def( 3, std::min(6 + power / 10, 40) ); + beam.is_beam = true; + break; + + case SPELL_PARALYSE: + beam.flavour = BEAM_PARALYSIS; + beam.is_beam = true; + break; + + case SPELL_SLOW: + beam.flavour = BEAM_SLOW; + beam.is_beam = true; + break; + + case SPELL_HASTE: // (self) + beam.flavour = BEAM_HASTE; + break; + + case SPELL_BACKLIGHT: + beam.flavour = BEAM_BACKLIGHT; + beam.is_beam = true; + break; + + case SPELL_CONFUSE: + beam.flavour = BEAM_CONFUSION; + beam.is_beam = true; + break; + + case SPELL_SLEEP: + beam.flavour = BEAM_SLEEP; + beam.is_beam = true; + break; + + case SPELL_POLYMORPH_OTHER: + beam.flavour = BEAM_POLYMORPH; + beam.is_beam = true; + // Be careful with this one. + // Having allies mutate you is infuriating. + beam.foe_ratio = 1000; + break; + + case SPELL_VENOM_BOLT: + beam.name = "bolt of poison"; + beam.damage = dice_def( 3, 6 + power / 13 ); + beam.colour = LIGHTGREEN; + beam.flavour = BEAM_POISON; + beam.hit = 19 + power / 20; + beam.is_beam = true; + break; + + case SPELL_POISON_ARROW: + beam.name = "poison arrow"; + beam.damage = dice_def( 3, 7 + power / 12 ); + beam.colour = LIGHTGREEN; + beam.type = dchar_glyph(DCHAR_FIRED_MISSILE); + beam.flavour = BEAM_POISON_ARROW; + beam.hit = 20 + power / 25; + break; + + case SPELL_BOLT_OF_MAGMA: + beam.name = "bolt of magma"; + beam.damage = dice_def( 3, 8 + power / 11 ); + beam.colour = RED; + beam.flavour = BEAM_LAVA; + beam.hit = 17 + power / 25; + beam.is_beam = true; + break; + + case SPELL_BOLT_OF_FIRE: + beam.name = "bolt of fire"; + beam.damage = dice_def( 3, 8 + power / 11 ); + beam.colour = RED; + beam.flavour = BEAM_FIRE; + beam.hit = 17 + power / 25; + beam.is_beam = true; + break; + + case SPELL_FLING_ICICLE: + beam.name = "shard of ice"; + beam.damage = dice_def( 3, 8 + power / 11 ); + beam.colour = WHITE; + beam.flavour = BEAM_ICE; + beam.hit = 17 + power / 25; + beam.is_beam = true; + break; + + case SPELL_BOLT_OF_COLD: + beam.name = "bolt of cold"; + beam.damage = dice_def( 3, 8 + power / 11 ); + beam.colour = WHITE; + beam.flavour = BEAM_COLD; + beam.hit = 17 + power / 25; + beam.is_beam = true; + break; + + case SPELL_FREEZING_CLOUD: + beam.name = "freezing blast"; + beam.damage = dice_def( 2, 9 + power / 11 ); + beam.colour = WHITE; + beam.flavour = BEAM_COLD; + beam.hit = 17 + power / 25; + beam.is_beam = true; + beam.is_big_cloud = true; + break; + + case SPELL_SHOCK: + beam.name = "zap"; + beam.damage = dice_def( 1, 8 + (power / 20) ); + beam.colour = LIGHTCYAN; + beam.flavour = BEAM_ELECTRICITY; + beam.hit = 17 + power / 20; + beam.is_beam = true; + break; + + case SPELL_LIGHTNING_BOLT: + beam.name = "bolt of lightning"; + beam.damage = dice_def( 3, 10 + power / 17 ); + beam.colour = LIGHTCYAN; + beam.flavour = BEAM_ELECTRICITY; + beam.hit = 16 + power / 40; + beam.is_beam = true; + break; + + case SPELL_INVISIBILITY: + beam.flavour = BEAM_INVISIBILITY; + break; + + case SPELL_FIREBALL: + beam.colour = RED; + beam.name = "fireball"; + beam.damage = dice_def( 3, 7 + power / 10 ); + beam.hit = 40; + beam.flavour = BEAM_FIRE; + beam.foe_ratio = 60; + beam.is_explosion = true; + break; + + case SPELL_FIRE_STORM: + setup_fire_storm(mons, power / 2, beam); + beam.foe_ratio = random_range(40, 55); + break; + + case SPELL_ICE_STORM: + beam.name = "great blast of cold"; + beam.colour = BLUE; + beam.damage = calc_dice( 10, 18 + power / 2 ); + beam.hit = 20 + power / 10; // 50: 25 100: 30 + beam.ench_power = power; // used for radius + beam.flavour = BEAM_ICE; // half resisted + beam.is_explosion = true; + beam.foe_ratio = random_range(40, 55); + break; + + case SPELL_HELLFIRE_BURST: + beam.aux_source = "burst of hellfire"; + beam.name = "burst of hellfire"; + beam.ex_size = 1; + beam.flavour = BEAM_HELLFIRE; + beam.is_explosion = true; + beam.colour = RED; + beam.aux_source.clear(); + beam.is_tracer = false; + beam.hit = 20; + beam.damage = mons_foe_is_mons(mons) ? dice_def(5, 7) + : dice_def(3, 20); + break; + + case SPELL_MINOR_HEALING: + beam.flavour = BEAM_HEALING; + beam.hit = 25 + (power / 5); + break; + + case SPELL_TELEPORT_SELF: + beam.flavour = BEAM_TELEPORT; + break; + + case SPELL_TELEPORT_OTHER: + beam.flavour = BEAM_TELEPORT; + beam.is_beam = true; + break; + + case SPELL_LEHUDIBS_CRYSTAL_SPEAR: // was splinters + beam.name = "crystal spear"; + beam.damage = dice_def( 3, 16 + power / 10 ); + beam.colour = WHITE; + beam.type = dchar_glyph(DCHAR_FIRED_MISSILE); + beam.flavour = BEAM_MMISSILE; + beam.hit = 22 + power / 20; + break; + + case SPELL_DIG: + beam.flavour = BEAM_DIGGING; + beam.is_beam = true; + break; + + case SPELL_BOLT_OF_DRAINING: // negative energy + beam.name = "bolt of negative energy"; + beam.damage = dice_def( 3, 6 + power / 13 ); + beam.colour = DARKGREY; + beam.flavour = BEAM_NEG; + beam.hit = 16 + power / 35; + beam.is_beam = true; + break; + + case SPELL_ISKENDERUNS_MYSTIC_BLAST: // mystic blast + beam.colour = LIGHTMAGENTA; + beam.name = "orb of energy"; + beam.short_name = "energy"; + beam.damage = dice_def( 3, 7 + (power / 14) ); + beam.hit = 20 + (power / 20); + beam.flavour = BEAM_MMISSILE; + break; + + case SPELL_STEAM_BALL: + beam.colour = LIGHTGREY; + beam.name = "ball of steam"; + beam.damage = dice_def( 3, 7 + (power / 15) ); + beam.hit = 20 + power / 20; + beam.flavour = BEAM_STEAM; + break; + + case SPELL_PAIN: + beam.flavour = BEAM_PAIN; + beam.damage = dice_def( 1, 7 + (power / 20) ); + beam.ench_power = std::max(50, 8 * mons->hit_dice); + beam.is_beam = true; + break; + + case SPELL_STICKY_FLAME_SPLASH: + case SPELL_STICKY_FLAME: + beam.colour = RED; + beam.name = "sticky flame"; + beam.damage = dice_def( 3, 3 + power / 50 ); + beam.hit = 18 + power / 15; + beam.flavour = BEAM_FIRE; + break; + + case SPELL_POISONOUS_CLOUD: + beam.name = "blast of poison"; + beam.damage = dice_def( 3, 3 + power / 25 ); + beam.colour = LIGHTGREEN; + beam.flavour = BEAM_POISON; + beam.hit = 18 + power / 25; + beam.is_beam = true; + beam.is_big_cloud = true; + break; + + case SPELL_ENERGY_BOLT: // eye of devastation + beam.colour = YELLOW; + beam.name = "bolt of energy"; + beam.short_name = "energy"; + beam.damage = dice_def( 3, 20 ); + beam.hit = 15 + power / 30; + beam.flavour = BEAM_NUKE; // a magical missile which destroys walls + beam.is_beam = true; + break; + + case SPELL_STING: // sting + beam.colour = GREEN; + beam.name = "sting"; + beam.damage = dice_def( 1, 6 + power / 25 ); + beam.hit = 60; + beam.flavour = BEAM_POISON; + break; + + case SPELL_IRON_SHOT: + beam.colour = LIGHTCYAN; + beam.name = "iron shot"; + beam.damage = dice_def( 3, 8 + (power / 9) ); + beam.hit = 20 + (power / 25); + beam.type = dchar_glyph(DCHAR_FIRED_MISSILE); + beam.flavour = BEAM_MMISSILE; // similarly unresisted thing + break; + + case SPELL_STONE_ARROW: + beam.colour = LIGHTGREY; + beam.name = "stone arrow"; + beam.damage = dice_def( 3, 5 + (power / 10) ); + beam.hit = 14 + power / 35; + beam.type = dchar_glyph(DCHAR_FIRED_MISSILE); + beam.flavour = BEAM_MMISSILE; // similarly unresisted thing + break; + + case SPELL_POISON_SPLASH: + beam.colour = GREEN; + beam.name = "splash of poison"; + beam.damage = dice_def( 1, 4 + power / 10 ); + beam.hit = 16 + power / 20; + beam.flavour = BEAM_POISON; + break; + + case SPELL_ACID_SPLASH: + beam.colour = YELLOW; + beam.name = "splash of acid"; + beam.damage = dice_def( 3, 7 ); + beam.hit = 20 + (3 * mons->hit_dice); + beam.flavour = BEAM_ACID; + break; + + case SPELL_DISINTEGRATE: + beam.flavour = BEAM_DISINTEGRATION; + beam.ench_power = 50; + beam.damage = dice_def( 1, 30 + (power / 10) ); + beam.is_beam = true; + break; + + case SPELL_MEPHITIC_CLOUD: // swamp drake, player ghost + beam.name = "foul vapour"; + beam.damage = dice_def(1,0); + beam.colour = GREEN; + // Well, it works, even if the name isn't quite intuitive. + beam.flavour = BEAM_POTION_STINKING_CLOUD; + beam.hit = 14 + power / 30; + beam.ench_power = power; // probably meaningless + beam.is_explosion = true; + beam.is_big_cloud = true; + break; + + case SPELL_MIASMA: // death drake + beam.name = "foul vapour"; + beam.damage = dice_def( 3, 5 + power / 24 ); + beam.colour = DARKGREY; + beam.flavour = BEAM_MIASMA; + beam.hit = 17 + power / 20; + beam.is_beam = true; + beam.is_big_cloud = true; + break; + + case SPELL_QUICKSILVER_BOLT: // Quicksilver dragon + beam.colour = random_colour(); + beam.name = "bolt of energy"; + beam.short_name = "energy"; + beam.damage = dice_def( 3, 25 ); + beam.hit = 16 + power / 25; + beam.flavour = BEAM_MMISSILE; + break; + + case SPELL_HELLFIRE: // fiend's hellfire + beam.name = "blast of hellfire"; + beam.aux_source = "blast of hellfire"; + beam.colour = RED; + beam.damage = dice_def( 3, 25 ); + beam.hit = 24; + beam.flavour = BEAM_HELLFIRE; + beam.is_beam = true; + beam.is_explosion = true; + break; + + case SPELL_METAL_SPLINTERS: + beam.name = "spray of metal splinters"; + beam.short_name = "metal splinters"; + beam.damage = dice_def( 3, 20 + power / 20 ); + beam.colour = CYAN; + beam.flavour = BEAM_FRAG; + beam.hit = 19 + power / 30; + beam.is_beam = true; + break; + + case SPELL_BANISHMENT: + beam.flavour = BEAM_BANISH; + beam.is_beam = true; + break; + + case SPELL_BLINK_OTHER: + beam.flavour = BEAM_BLINK; + beam.is_beam = true; + break; + + case SPELL_FIRE_BREATH: + beam.name = "blast of flame"; + beam.aux_source = "blast of fiery breath"; + beam.damage = dice_def( 3, (mons->hit_dice * 2) ); + beam.colour = RED; + beam.hit = 30; + beam.flavour = BEAM_FIRE; + beam.is_beam = true; + break; + + case SPELL_COLD_BREATH: + beam.name = "blast of cold"; + beam.aux_source = "blast of icy breath"; + beam.short_name = "frost"; + beam.damage = dice_def( 3, (mons->hit_dice * 2) ); + beam.colour = WHITE; + beam.hit = 30; + beam.flavour = BEAM_COLD; + beam.is_beam = true; + break; + + case SPELL_DRACONIAN_BREATH: + beam.damage = dice_def( 3, (mons->hit_dice * 2) ); + beam.hit = 30; + beam.is_beam = true; + break; + + case SPELL_PORKALATOR: + beam.name = "porkalator"; + beam.type = 0; + beam.flavour = BEAM_PORKALATOR; + beam.thrower = KILL_MON_MISSILE; + beam.is_beam = true; + break; + + default: + if (!is_valid_spell(real_spell)) + DEBUGSTR("Invalid spell #%d cast by %s", (int) real_spell, + mons->name(DESC_PLAIN, true).c_str()); + + DEBUGSTR("Unknown monster spell '%s' cast by %s", + spell_title(real_spell), + mons->name(DESC_PLAIN, true).c_str()); + + return (beam); + } + + if (beam.is_enchantment()) + { + beam.type = dchar_glyph(DCHAR_SPACE); + beam.name = "0"; + } + + if (spell_cast == SPELL_DRACONIAN_BREATH) + _scale_draconian_breath(beam, drac_type); + + // Accuracy is lowered by one quarter if the dragon is attacking + // a target that is wielding a weapon of dragon slaying (which + // makes the dragon/draconian avoid looking at the foe). + // FIXME: This effect is not yet implemented for player draconians + // or characters in dragon form breathing at monsters wielding a + // weapon with this brand. + if (is_dragonkind(mons)) + { + if (actor *foe = mons->get_foe()) + { + if (const item_def *weapon = foe->weapon()) + { + if (get_weapon_brand(*weapon) == SPWPN_DRAGON_SLAYING) + { + beam.hit *= 3; + beam.hit /= 4; + } + } + } + } + + return (beam); +} + +static bool _los_free_spell(spell_type spell_cast) +{ + return (spell_cast == SPELL_HELLFIRE_BURST + || spell_cast == SPELL_BRAIN_FEED + || spell_cast == SPELL_SMITING + || spell_cast == SPELL_HAUNT + || spell_cast == SPELL_FIRE_STORM + || spell_cast == SPELL_AIRSTRIKE); +} + +// Set up bolt structure for monster spell casting. +void setup_mons_cast(monsters *monster, bolt &pbolt, spell_type spell_cast) +{ + // always set these -- used by things other than fire_beam() + + // [ds] Used to be 12 * MHD and later buggily forced to -1 downstairs. + // Setting this to a more realistic number now that that bug is + // squashed. + pbolt.ench_power = 4 * monster->hit_dice; + + if (spell_cast == SPELL_TELEPORT_SELF) + pbolt.ench_power = 2000; + + pbolt.beam_source = monster_index(monster); + + // Convenience for the hapless innocent who assumes that this + // damn function does all possible setup. [ds] + if (pbolt.target.origin()) + pbolt.target = monster->target; + + // Set bolt type and range. + if (_los_free_spell(spell_cast)) + { + pbolt.range = 0; + switch (spell_cast) + { + case SPELL_BRAIN_FEED: + pbolt.type = DMNBM_BRAIN_FEED; + return; + case SPELL_SMITING: + case SPELL_AIRSTRIKE: + pbolt.type = DMNBM_SMITING; + return; + default: + // Other spells get normal setup: + break; + } + } + + // The below are no-ops since they don't involve direct_effect, + // fire_tracer, or beam. + switch (spell_cast) + { + case SPELL_SUMMON_SMALL_MAMMALS: + case SPELL_MAJOR_HEALING: + case SPELL_VAMPIRE_SUMMON: + case SPELL_SHADOW_CREATURES: // summon anything appropriate for level + case SPELL_FAKE_RAKSHASA_SUMMON: + case SPELL_SUMMON_DEMON: + case SPELL_SUMMON_UGLY_THING: + case SPELL_ANIMATE_DEAD: + case SPELL_CALL_IMP: + case SPELL_SUMMON_SCORPIONS: + case SPELL_SUMMON_UFETUBUS: + case SPELL_SUMMON_BEAST: // Geryon + case SPELL_SUMMON_UNDEAD: // summon undead around player + case SPELL_SUMMON_ICE_BEAST: + case SPELL_SUMMON_MUSHROOMS: + case SPELL_CONJURE_BALL_LIGHTNING: + case SPELL_SUMMON_DRAKES: + case SPELL_SUMMON_HORRIBLE_THINGS: + case SPELL_HAUNT: + case SPELL_SYMBOL_OF_TORMENT: + case SPELL_SUMMON_GREATER_DEMON: + case SPELL_CANTRIP: + case SPELL_BERSERKER_RAGE: + case SPELL_WATER_ELEMENTALS: + case SPELL_KRAKEN_TENTACLES: + case SPELL_BLINK: + case SPELL_CONTROLLED_BLINK: + case SPELL_TOMB_OF_DOROKLOHE: + return; + default: + break; + } + + // Need to correct this for power of spellcaster + int power = 12 * monster->hit_dice; + + bolt theBeam = mons_spells(monster, spell_cast, power); + + pbolt.colour = theBeam.colour; + pbolt.range = theBeam.range; + pbolt.hit = theBeam.hit; + pbolt.damage = theBeam.damage; + + if (theBeam.ench_power != -1) + pbolt.ench_power = theBeam.ench_power; + + pbolt.type = theBeam.type; + pbolt.flavour = theBeam.flavour; + pbolt.thrower = theBeam.thrower; + pbolt.name = theBeam.name; + pbolt.short_name = theBeam.short_name; + pbolt.is_beam = theBeam.is_beam; + pbolt.source = monster->pos(); + pbolt.is_tracer = false; + pbolt.is_explosion = theBeam.is_explosion; + pbolt.ex_size = theBeam.ex_size; + + pbolt.foe_ratio = theBeam.foe_ratio; + + if (!pbolt.is_enchantment()) + pbolt.aux_source = pbolt.name; + else + pbolt.aux_source.clear(); + + if (spell_cast == SPELL_HASTE + || spell_cast == SPELL_INVISIBILITY + || spell_cast == SPELL_MINOR_HEALING + || spell_cast == SPELL_TELEPORT_SELF) + { + pbolt.target = monster->pos(); + } + else if (spell_cast == SPELL_PORKALATOR && one_chance_in(3)) + { + int target = -1; + int count = 0; + monster_type hog_type = MONS_HOG; + for (int i = 0; i < MAX_MONSTERS; i++) + { + monsters *targ = &menv[i]; + + if (!monster->can_see(targ)) + continue; + + hog_type = MONS_HOG; + if (targ->holiness() == MH_DEMONIC) + hog_type = MONS_HELL_HOG; + else if (targ->holiness() != MH_NATURAL) + continue; + + if (targ->type != hog_type + && mons_atts_aligned(monster->attitude, targ->attitude) + && mons_power(hog_type) + random2(4) >= mons_power(targ->type) + && (!mons_class_flag(targ->type, M_SPELLCASTER) || coinflip()) + && one_chance_in(++count)) + { + target = i; + } + } + + if (target != -1) + { + monsters *targ = &menv[target]; + pbolt.target = targ->pos(); +#if DEBUG_DIAGNOSTICS + mprf("Porkalator: targetting %s instead", + targ->name(DESC_PLAIN).c_str()); +#endif + monster_polymorph(targ, hog_type); + } + // else target remains as specified + } +} + +// 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 _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); +} + +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); +} + +//--------------------------------------------------------------- +// +// handle_spell +// +// Give the monster a chance to cast a spell. Returns true if +// a spell was cast. +// +//--------------------------------------------------------------- +bool handle_mon_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 (monster->asleep() + || monster->submerged() + || !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); + + // 1KB: the following code is never used for unfriendlies! + 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 (!you.visible_to(monster) + && (monster->target != you.pos() || coinflip())) + { + spellOK = false; + } + } + else if (!monster->can_see(&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)) + && !player_or_mon_in_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); + monster->lose_energy(EUT_SPELL); + } + } // end "if mons_class_flag(monster->type, M_SPELLCASTER) ... + + return (true); +} + +static int _monster_abjure_square(const coord_def &pos, + int pow, int actual, + int wont_attack) +{ + monsters *target = monster_at(pos); + if (target == NULL) + return (0); + + if (!target->alive() + || ((bool)wont_attack == mons_wont_attack_real(target))) + { + return (0); + } + + int duration; + + if (!target->is_summoned(&duration)) + return (0); + + pow = std::max(20, fuzz_value(pow, 40, 25)); + + if (!actual) + return (pow > 40 || pow >= duration); + + // TSO and Trog's abjuration protection. + bool shielded = false; + if (you.religion == GOD_SHINING_ONE) + { + pow = pow * (30 - target->hit_dice) / 30; + if (pow < duration) + { + simple_god_message(" protects your fellow warrior from evil " + "magic!"); + shielded = true; + } + } + else if (you.religion == GOD_TROG) + { + pow = pow * 4 / 5; + if (pow < duration) + { + simple_god_message(" shields your ally from puny magic!"); + shielded = true; + } + } + else if (is_sanctuary(target->pos())) + { + pow = 0; + mpr("Zin's power protects your fellow warrior from evil magic!", + MSGCH_GOD); + shielded = true; + } + +#ifdef DEBUG_DIAGNOSTICS + mprf(MSGCH_DIAGNOSTICS, "Abj: dur: %d, pow: %d, ndur: %d", + duration, pow, duration - pow); +#endif + + mon_enchant abj = target->get_ench(ENCH_ABJ); + if (!target->lose_ench_duration(abj, pow)) + { + if (!shielded) + simple_monster_message(target, " shudders."); + return (1); + } + + return (0); +} + +static int _apply_radius_around_square( const coord_def &c, int radius, + int (*fn)(const coord_def &, int, int, int), + int pow, int par1, int par2) +{ + int res = 0; + for (int yi = -radius; yi <= radius; ++yi) + { + const coord_def c1(c.x - radius, c.y + yi); + const coord_def c2(c.x + radius, c.y + yi); + if (in_bounds(c1)) + res += fn(c1, pow, par1, par2); + if (in_bounds(c2)) + res += fn(c2, pow, par1, par2); + } + + for (int xi = -radius + 1; xi < radius; ++xi) + { + const coord_def c1(c.x + xi, c.y - radius); + const coord_def c2(c.x + xi, c.y + radius); + if (in_bounds(c1)) + res += fn(c1, pow, par1, par2); + if (in_bounds(c2)) + res += fn(c2, pow, par1, par2); + } + return (res); +} + +static int _monster_abjuration(const monsters *caster, bool actual) +{ + const bool wont_attack = mons_wont_attack_real(caster); + int maffected = 0; + + if (actual) + mpr("Send 'em back where they came from!"); + + int pow = std::min(caster->hit_dice * 90, 2500); + + // Abjure radius. + for (int rad = 1; rad < 5 && pow >= 30; ++rad) + { + int number_hit = + _apply_radius_around_square(caster->pos(), rad, + _monster_abjure_square, + pow, actual, wont_attack); + + maffected += number_hit; + + // Each affected monster drops power. + // + // We could further tune this by the actual amount of abjuration + // damage done to each summon, but the player will probably never + // notice. :-) + while (number_hit-- > 0) + pow = pow * 90 / 100; + + pow /= 2; + } + return (maffected); +} + + +static bool _mons_abjured(monsters *monster, bool nearby) +{ + if (nearby && _monster_abjuration(monster, false) > 0 + && coinflip()) + { + _monster_abjuration(monster, true); + return (true); + } + + return (false); +} + +static monster_type _pick_random_wraith() +{ + static monster_type wraiths[] = + { + MONS_WRAITH, MONS_SHADOW_WRAITH, MONS_FREEZING_WRAITH, + MONS_SPECTRAL_WARRIOR, MONS_PHANTOM, MONS_HUNGRY_GHOST, + MONS_FLAYED_GHOST + }; + + return (RANDOM_ELEMENT(wraiths)); +} + +static monster_type _pick_horrible_thing() +{ + return (one_chance_in(4) ? MONS_TENTACLED_MONSTROSITY + : MONS_ABOMINATION_LARGE); +} + +static monster_type _pick_undead_summon() +{ + static monster_type undead[] = + { + MONS_NECROPHAGE, MONS_GHOUL, MONS_HUNGRY_GHOST, MONS_FLAYED_GHOST, + MONS_ZOMBIE_SMALL, MONS_SKELETON_SMALL, MONS_SIMULACRUM_SMALL, + MONS_FLYING_SKULL, MONS_FLAMING_CORPSE, MONS_MUMMY, MONS_VAMPIRE, + MONS_WIGHT, MONS_WRAITH, MONS_SHADOW_WRAITH, MONS_FREEZING_WRAITH, + MONS_SPECTRAL_WARRIOR, MONS_ZOMBIE_LARGE, MONS_SKELETON_LARGE, + MONS_SIMULACRUM_LARGE, MONS_SHADOW + }; + + return (RANDOM_ELEMENT(undead)); +} + +static void _do_high_level_summon(monsters *monster, bool monsterNearby, + spell_type spell_cast, + monster_type (*mpicker)(), int nsummons, + god_type god, coord_def *target = NULL) +{ + if (_mons_abjured(monster, monsterNearby)) + return; + + const int duration = std::min(2 + monster->hit_dice / 5, 6); + + for (int i = 0; i < nsummons; ++i) + { + monster_type which_mons = mpicker(); + + if (which_mons == MONS_NO_MONSTER) + continue; + + create_monster( + mgen_data(which_mons, SAME_ATTITUDE(monster), + duration, spell_cast, target ? *target : monster->pos(), + monster->foe, 0, god)); + } +} + +// Returns true if a message referring to the player's legs makes sense. +static bool _legs_msg_applicable() +{ + return (you.species != SP_NAGA + && (you.species != SP_MERFOLK || !player_is_swimming())); +} + +void mons_cast_haunt(monsters *monster) +{ + coord_def fpos; + + switch (monster->foe) + { + case MHITNOT: + return; + + case MHITYOU: + fpos = you.pos(); + break; + + default: + fpos = menv[monster->foe].pos(); + } + + _do_high_level_summon(monster, mons_near(monster), SPELL_HAUNT, + _pick_random_wraith, random_range(3, 6), GOD_NO_GOD, &fpos); +} + +void mons_cast(monsters *monster, bolt &pbolt, spell_type spell_cast, + bool do_noise) +{ + // Always do setup. It might be done already, but it doesn't hurt + // to do it again (cheap). + setup_mons_cast(monster, pbolt, spell_cast); + + // single calculation permissible {dlb} + bool monsterNearby = mons_near(monster); + + int sumcount = 0; + int sumcount2; + int duration = 0; + +#if DEBUG_DIAGNOSTICS + mprf(MSGCH_DIAGNOSTICS, "Mon #%d casts %s (#%d)", + monster_index(monster), spell_title(spell_cast), spell_cast); +#endif + + if (spell_cast == SPELL_CANTRIP) + do_noise = false; // Spell itself does the messaging. + + if (_los_free_spell(spell_cast) && !spell_is_direct_explosion(spell_cast)) + { + if (monster->foe == MHITYOU || monster->foe == MHITNOT) + { + if (monsterNearby) + { + if (do_noise) + mons_cast_noise(monster, pbolt, spell_cast); + direct_effect(monster, spell_cast, pbolt, &you); + } + return; + } + + if (do_noise) + mons_cast_noise(monster, pbolt, spell_cast); + direct_effect(monster, spell_cast, pbolt, monster->get_foe()); + return; + } + +#ifdef DEBUG + const unsigned int flags = get_spell_flags(spell_cast); + + ASSERT(!(flags & (SPFLAG_TESTING | SPFLAG_MAPPING))); + + // Targeted spells need a valid target. + ASSERT(!(flags & SPFLAG_TARGETTING_MASK) || in_bounds(pbolt.target)); +#endif + + if (do_noise) + mons_cast_noise(monster, pbolt, spell_cast); + + // 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; + + switch (spell_cast) + { + default: + break; + + case SPELL_MAJOR_HEALING: + if (heal_monster(monster, 50 + random2avg(monster->hit_dice * 10, 2), + false)) + { + simple_monster_message(monster, " is healed."); + } + return; + + case SPELL_BERSERKER_RAGE: + monster->go_berserk(true); + return; + + case SPELL_SUMMON_SMALL_MAMMALS: + case SPELL_VAMPIRE_SUMMON: + if (spell_cast == SPELL_SUMMON_SMALL_MAMMALS) + sumcount2 = 1 + random2(4); + else + sumcount2 = 3 + random2(3) + monster->hit_dice / 5; + + for (sumcount = 0; sumcount < sumcount2; ++sumcount) + { + const monster_type rats[] = { MONS_ORANGE_RAT, MONS_GREEN_RAT, + MONS_GREY_RAT, MONS_RAT }; + const monster_type mon = (one_chance_in(3) ? MONS_GIANT_BAT + : RANDOM_ELEMENT(rats)); + create_monster( + mgen_data(mon, SAME_ATTITUDE(monster), + 5, spell_cast, monster->pos(), monster->foe, 0, god)); + } + return; + + case SPELL_SHADOW_CREATURES: // summon anything appropriate for level + if (_mons_abjured(monster, monsterNearby)) + return; + + sumcount2 = 1 + random2(4) + random2(monster->hit_dice / 7 + 1); + + for (sumcount = 0; sumcount < sumcount2; ++sumcount) + { + create_monster( + mgen_data(RANDOM_MONSTER, SAME_ATTITUDE(monster), + 5, spell_cast, monster->pos(), monster->foe, 0, god)); + } + return; + + case SPELL_WATER_ELEMENTALS: + if (_mons_abjured(monster, monsterNearby)) + return; + + sumcount2 = 1 + random2(4) + random2(monster->hit_dice / 7 + 1); + + for (sumcount = 0; sumcount < sumcount2; sumcount++) + { + create_monster( + mgen_data(MONS_WATER_ELEMENTAL, SAME_ATTITUDE(monster), + 3, spell_cast, monster->pos(), monster->foe, 0, god)); + } + return; + + case SPELL_KRAKEN_TENTACLES: + { + int kraken_index = monster_index(monster); + if (invalid_monster_index(duration)) + { + mpr("Error! Kraken is not a part of the current environment!", + MSGCH_ERROR); + return; + } + sumcount2 = std::max(random2(9), random2(9)); // up to eight tentacles + if (sumcount2 == 0) + return; + + for (sumcount = 0; sumcount < MAX_MONSTERS; ++sumcount) + if (menv[sumcount].type == MONS_KRAKEN_TENTACLE + && (int)menv[sumcount].number == kraken_index) + { + // Reduce by tentacles already placed. + sumcount2--; + } + + for (sumcount = sumcount2; sumcount > 0; --sumcount) + { + // Tentacles aren't really summoned (controlled by spell_cast + // being passed to summon_type), so I'm not sure what the + // abjuration value (3) is doing there. (jpeg) + if (create_monster( + mgen_data(MONS_KRAKEN_TENTACLE, SAME_ATTITUDE(monster), + 3, spell_cast, monster->pos(), monster->foe, 0, god, + MONS_NO_MONSTER, kraken_index, monster->colour, + you.your_level, PROX_CLOSE_TO_PLAYER, + you.level_type)) == -1) + { + sumcount2--; + } + } + if (sumcount2 == 1) + mpr("A tentacle rises from the water!"); + else if (sumcount2 > 1) + mpr("Tentacles burst out of the water!"); + return; + } + case SPELL_FAKE_RAKSHASA_SUMMON: + sumcount2 = (coinflip() ? 2 : 3); + + for (sumcount = 0; sumcount < sumcount2; sumcount++) + { + create_monster( + mgen_data(MONS_RAKSHASA_FAKE, SAME_ATTITUDE(monster), + 3, spell_cast, monster->pos(), monster->foe, 0, god)); + } + return; + + case SPELL_SUMMON_DEMON: // class 2-4 demons + if (_mons_abjured(monster, monsterNearby)) + return; + + sumcount2 = 1 + random2(2) + random2(monster->hit_dice / 10 + 1); + + duration = std::min(2 + monster->hit_dice / 10, 6); + for (sumcount = 0; sumcount < sumcount2; sumcount++) + { + create_monster( + mgen_data(summon_any_demon(DEMON_COMMON), + SAME_ATTITUDE(monster), duration, spell_cast, + monster->pos(), monster->foe, 0, god)); + } + return; + + case SPELL_SUMMON_UGLY_THING: + if (_mons_abjured(monster, monsterNearby)) + return; + + sumcount2 = 1 + random2(2) + random2(monster->hit_dice / 10 + 1); + + duration = std::min(2 + monster->hit_dice / 10, 6); + for (sumcount = 0; sumcount < sumcount2; ++sumcount) + { + const int chance = std::max(6 - (monster->hit_dice / 6), 1); + monster_type mon = (one_chance_in(chance) ? MONS_VERY_UGLY_THING + : MONS_UGLY_THING); + + create_monster( + mgen_data(mon, SAME_ATTITUDE(monster), + duration, spell_cast, monster->pos(), monster->foe, 0, + god)); + } + return; + + case SPELL_ANIMATE_DEAD: + // see special handling in monstuff::handle_spell() {dlb} + animate_dead(monster, 5 + random2(5), SAME_ATTITUDE(monster), + monster->foe, god); + return; + + case SPELL_CALL_IMP: // class 5 demons + sumcount2 = 1 + random2(3) + random2(monster->hit_dice / 5 + 1); + + duration = std::min(2 + monster->hit_dice / 5, 6); + for (sumcount = 0; sumcount < sumcount2; ++sumcount) + { + create_monster( + mgen_data(summon_any_demon(DEMON_LESSER), + SAME_ATTITUDE(monster), + duration, spell_cast, monster->pos(), monster->foe, 0, + god)); + } + return; + + case SPELL_SUMMON_SCORPIONS: + if (_mons_abjured(monster, monsterNearby)) + return; + + sumcount2 = 1 + random2(3) + random2(monster->hit_dice / 5 + 1); + + duration = std::min(2 + monster->hit_dice / 5, 6); + for (sumcount = 0; sumcount < sumcount2; ++sumcount) + { + create_monster( + mgen_data(MONS_SCORPION, SAME_ATTITUDE(monster), + duration, spell_cast, monster->pos(), monster->foe, 0, + god)); + } + return; + + case SPELL_SUMMON_UFETUBUS: + sumcount2 = 2 + random2(2) + random2(monster->hit_dice / 5 + 1); + + duration = std::min(2 + monster->hit_dice / 5, 6); + + for (sumcount = 0; sumcount < sumcount2; ++sumcount) + { + create_monster( + mgen_data(MONS_UFETUBUS, SAME_ATTITUDE(monster), + duration, spell_cast, monster->pos(), monster->foe, 0, + god)); + } + return; + + case SPELL_SUMMON_BEAST: // Geryon + create_monster( + mgen_data(MONS_BEAST, SAME_ATTITUDE(monster), + 4, spell_cast, monster->pos(), monster->foe, 0, god)); + return; + + case SPELL_SUMMON_ICE_BEAST: + create_monster( + mgen_data(MONS_ICE_BEAST, SAME_ATTITUDE(monster), + 5, spell_cast, monster->pos(), monster->foe, 0, god)); + return; + + case SPELL_SUMMON_MUSHROOMS: // Summon swarms of icky crawling fungi. + if (_mons_abjured(monster, monsterNearby)) + return; + + sumcount2 = 1 + random2(2) + random2(monster->hit_dice / 4 + 1); + + duration = std::min(2 + monster->hit_dice / 5, 6); + for (int i = 0; i < sumcount2; ++i) + { + create_monster( + mgen_data(MONS_WANDERING_MUSHROOM, SAME_ATTITUDE(monster), + duration, spell_cast, monster->pos(), monster->foe, 0, + god)); + } + return; + + case SPELL_SUMMON_HORRIBLE_THINGS: + _do_high_level_summon(monster, monsterNearby, spell_cast, + _pick_horrible_thing, random_range(3, 5), god); + return; + + case SPELL_CONJURE_BALL_LIGHTNING: + { + const int n = 2 + random2(monster->hit_dice / 4); + for (int i = 0; i < n; ++i) + { + create_monster( + mgen_data(MONS_BALL_LIGHTNING, SAME_ATTITUDE(monster), + 2, spell_cast, monster->pos(), monster->foe, 0, god)); + } + return; + } + + case SPELL_SUMMON_UNDEAD: // Summon undead around player. + _do_high_level_summon(monster, monsterNearby, spell_cast, + _pick_undead_summon, + 2 + random2(2) + + random2(monster->hit_dice / 4 + 1), god); + return; + + case SPELL_SYMBOL_OF_TORMENT: + if (!monsterNearby || mons_friendly(monster)) + return; + + torment(monster_index(monster), monster->pos()); + return; + + case SPELL_SUMMON_GREATER_DEMON: + if (_mons_abjured(monster, monsterNearby)) + return; + + sumcount2 = 1 + random2(monster->hit_dice / 10 + 1); + + duration = std::min(2 + monster->hit_dice / 10, 6); + for (sumcount = 0; sumcount < sumcount2; ++sumcount) + { + create_monster( + mgen_data(summon_any_demon(DEMON_GREATER), + SAME_ATTITUDE(monster), + duration, spell_cast, monster->pos(), monster->foe, + 0, god)); + } + return; + + // Journey -- Added in Summon Lizards and Draconian + case SPELL_SUMMON_DRAKES: + if (_mons_abjured(monster, monsterNearby)) + return; + + sumcount2 = 1 + random2(3) + random2(monster->hit_dice / 5 + 1); + + duration = std::min(2 + monster->hit_dice / 10, 6); + + { + std::vector monsters; + + for (sumcount = 0; sumcount < sumcount2; sumcount++) + { + monster_type mon = summon_any_dragon(DRAGON_LIZARD); + + if (mon == MONS_DRAGON) + { + monsters.clear(); + monsters.push_back(summon_any_dragon(DRAGON_DRAGON)); + break; + } + + monsters.push_back(mon); + } + + for (int i = 0, size = monsters.size(); i < size; ++i) + { + create_monster( + mgen_data(monsters[i], SAME_ATTITUDE(monster), + duration, spell_cast, + monster->pos(), monster->foe, 0, god)); + } + } + return; + + // TODO: Outsource the cantrip messages and allow specification of + // special cantrip spells per monster, like for speech, both as + // "self buffs" and "player enchantments". + case SPELL_CANTRIP: + { + // Monster spell of uselessness, just prints a message. + // This spell exists so that some monsters with really strong + // spells (ie orc priest) can be toned down a bit. -- bwr + // + // XXX: Needs expansion, and perhaps different priest/mage flavours. + + // Don't give any message if the monster isn't nearby. + // (Otherwise you could get them from halfway across the level.) + if (!mons_near(monster)) + return; + + const bool friendly = mons_friendly(monster); + const bool buff_only = !friendly && is_sanctuary(you.pos()); + const msg_channel_type channel = (friendly) ? MSGCH_FRIEND_ENCHANT + : MSGCH_MONSTER_ENCHANT; + + if (monster->type == MONS_GASTRONOK) + { + bool has_mon_foe = !invalid_monster_index(monster->foe); + std::string slugform = ""; + if (buff_only || crawl_state.arena && !has_mon_foe + || friendly && !has_mon_foe || coinflip()) + { + slugform = getSpeakString("gastronok_self_buff"); + if (!slugform.empty()) + { + slugform = replace_all(slugform, "@The_monster@", + monster->name(DESC_CAP_THE)); + mpr(slugform.c_str(), channel); + } + } + else if (!friendly && !has_mon_foe) + { + mons_cast_noise(monster, pbolt, spell_cast); + + // "Enchant" the player. + slugform = getSpeakString("gastronok_debuff"); + if (!slugform.empty() + && (slugform.find("legs") == std::string::npos + || _legs_msg_applicable())) + { + mpr(slugform.c_str()); + } + } + else + { + // "Enchant" another monster. + const monsters* foe + = dynamic_cast(monster->get_foe()); + slugform = getSpeakString("gastronok_other_buff"); + if (!slugform.empty()) + { + slugform = replace_all(slugform, "@The_monster@", + foe->name(DESC_CAP_THE)); + mpr(slugform.c_str(), MSGCH_MONSTER_ENCHANT); + } + } + } + else + { + // Messages about the monster influencing itself. + const char* buff_msgs[] = { " glows brightly for a moment.", + " looks stronger.", + " becomes somewhat translucent.", + "'s eyes start to glow." }; + + // Messages about the monster influencing you. + const char* other_msgs[] = { + "You feel troubled.", + "You feel a wave of unholy energy pass over you." + }; + + if (buff_only || crawl_state.arena || x_chance_in_y(2,3)) + { + simple_monster_message(monster, RANDOM_ELEMENT(buff_msgs), + channel); + } + else if (friendly) + { + simple_monster_message(monster, " shimmers for a moment.", + channel); + } + else // "Enchant" the player. + { + mons_cast_noise(monster, pbolt, spell_cast); + mpr(RANDOM_ELEMENT(other_msgs)); + } + } + return; + } + case SPELL_BLINK_OTHER: + { + // Allow the caster to comment on moving the foe. + std::string msg = getSpeakString(monster->name(DESC_PLAIN) + + " blink_other"); + if (!msg.empty() && msg != "__NONE") + { + mons_speaks_msg(monster, msg, MSGCH_TALK, + silenced(you.pos()) || silenced(monster->pos())); + } + break; + } + case SPELL_TOMB_OF_DOROKLOHE: + { + sumcount = 0; + for (adjacent_iterator ai(monster->pos()); ai; ++ai) + { + // we can blink away the crowd, but only our allies + if (mgrd(*ai) != NON_MONSTER + && monster_at(*ai)->attitude != monster->attitude) + sumcount++; + if (grd(*ai) != DNGN_FLOOR && grd(*ai) > DNGN_MAX_NONREACH + && !feat_is_trap(grd(*ai))) + sumcount++; + } + if (abs(you.pos().x-monster->pos().x)<=1 && + abs(you.pos().y-monster->pos().y)<=1) + sumcount++; + if (sumcount) + { + monster->blink(); + return; + } + + sumcount = 0; + for (adjacent_iterator ai(monster->pos()); ai; ++ai) + { + if (mgrd(*ai) != NON_MONSTER && monster_at(*ai) != monster) + { + monster_at(*ai)->blink(); + if (mgrd(*ai) != NON_MONSTER) + { + monster_at(*ai)->teleport(true); + if (mgrd(*ai) != NON_MONSTER) + continue; + } + } + if (grd(*ai) == DNGN_FLOOR || feat_is_trap(grd(*ai))) + { + grd(*ai) = DNGN_ROCK_WALL; + sumcount++; + } + } + if (sumcount) + mpr("Walls emerge from the floor!"); + monster->number = 1; // mark Khufu as entombed + return; + } + } + + // If a monster just came into view and immediately cast a spell, + // we need to refresh the screen before drawing the beam. + viewwindow(true, false); + if (spell_is_direct_explosion(spell_cast)) + { + const actor *foe = monster->get_foe(); + const bool need_more = foe && (foe == &you || see_cell(foe->pos())); + pbolt.in_explosion_phase = false; + pbolt.explode(need_more); + } + else + pbolt.fire(); +} + +void mons_cast_noise(monsters *monster, bolt &pbolt, spell_type spell_cast) +{ + bool force_silent = false; + + spell_type real_spell = spell_cast; + + if (spell_cast == SPELL_DRACONIAN_BREATH) + { + int type = monster->type; + if (mons_genus(type) == MONS_DRACONIAN) + type = draco_subspecies(monster); + + switch (type) + { + case MONS_MOTTLED_DRACONIAN: + real_spell = SPELL_STICKY_FLAME_SPLASH; + break; + + case MONS_YELLOW_DRACONIAN: + real_spell = SPELL_ACID_SPLASH; + break; + + case MONS_PLAYER_GHOST: + // Draining breath is silent. + force_silent = true; + break; + + default: + break; + } + } + else if (monster->type == MONS_SHADOW_DRAGON) + // Draining breath is silent. + force_silent = true; + + const bool unseen = !you.can_see(monster); + const bool silent = silenced(monster->pos()) || force_silent; + const bool no_silent = mons_class_flag(monster->type, M_SPELL_NO_SILENT); + + if (unseen && silent) + return; + + const unsigned int flags = get_spell_flags(real_spell); + + const bool priest = mons_class_flag(monster->type, M_PRIEST); + const bool wizard = mons_class_flag(monster->type, M_ACTUAL_SPELLS); + const bool innate = !(priest || wizard || no_silent) + || (flags & SPFLAG_INNATE); + + int noise; + if (silent + || (innate + && !mons_class_flag(monster->type, M_NOISY_SPELLS) + && !(flags & SPFLAG_NOISY) + && mons_genus(monster->type) != MONS_DRAGON)) + { + noise = 0; + } + else + { + if (mons_genus(monster->type) == MONS_DRAGON) + noise = get_shout_noise_level(S_ROAR); + else + noise = spell_noise(real_spell); + } + + const std::string cast_str = " cast"; + + const std::string spell_name = spell_title(real_spell); + const mon_body_shape shape = get_mon_shape(monster); + + std::vector key_list; + + // First try the spells name. + if (shape <= MON_SHAPE_NAGA) + { + if (!innate && (priest || wizard)) + key_list.push_back(spell_name + cast_str + " real"); + if (mons_intel(monster) >= I_NORMAL) + key_list.push_back(spell_name + cast_str + " gestures"); + } + key_list.push_back(spell_name + cast_str); + + const unsigned int num_spell_keys = key_list.size(); + + // Next the monster type name, then species name, then genus name. + key_list.push_back(mons_type_name(monster->type, DESC_PLAIN) + cast_str); + key_list.push_back(mons_type_name(mons_species(monster->type), DESC_PLAIN) + + cast_str); + key_list.push_back(mons_type_name(mons_genus(monster->type), DESC_PLAIN) + + cast_str); + + // Last, generic wizard, priest or demon. + if (wizard) + key_list.push_back("wizard" + cast_str); + else if (priest) + key_list.push_back("priest" + cast_str); + else if (mons_is_demon(monster->type)) + key_list.push_back("demon" + cast_str); + + const bool visible_beam = pbolt.type != 0 && pbolt.type != ' ' + && pbolt.name[0] != '0' + && !pbolt.is_enchantment(); + + const bool targeted = (flags & SPFLAG_TARGETTING_MASK) + && (pbolt.target != monster->pos() || visible_beam); + + if (targeted) + { + // For targeted spells, try with the targeted suffix first. + for (unsigned int i = key_list.size() - 1; i >= num_spell_keys; i--) + { + std::string str = key_list[i] + " targeted"; + key_list.insert(key_list.begin() + i, str); + } + + // Generic beam messages. + if (visible_beam) + { + key_list.push_back(pbolt.get_short_name() + " beam " + cast_str); + key_list.push_back("beam catchall cast"); + } + } + + std::string prefix; + if (silent) + prefix = "silent "; + else if (unseen) + prefix = "unseen "; + + std::string msg; + for (unsigned int i = 0; i < key_list.size(); i++) + { + const std::string key = key_list[i]; + + msg = getSpeakString(prefix + key); + if (msg == "__NONE") + { + msg = ""; + break; + } + else if (msg == "__NEXT") + { + msg = ""; + if (i < num_spell_keys) + i = num_spell_keys - 1; + else if (ends_with(key, " targeted")) + i++; + continue; + } + else if (!msg.empty()) + break; + + // If we got no message and we're using the silent prefix, then + // try again without the prefix. + if (prefix != "silent") + continue; + + msg = getSpeakString(key); + if (msg == "__NONE") + { + msg = ""; + break; + } + else if (msg == "__NEXT") + { + msg = ""; + if (i < num_spell_keys) + i = num_spell_keys - 1; + else if (ends_with(key, " targeted")) + i++; + continue; + } + else if (!msg.empty()) + break; + } + + if (msg.empty()) + { + if (silent) + return; + + noisy(noise, monster->pos(), monster->mindex()); + return; + } + + ///////////////////// + // We have a message. + ///////////////////// + + const bool gestured = msg.find("Gesture") != std::string::npos + || msg.find(" gesture") != std::string::npos + || msg.find("Point") != std::string::npos + || msg.find(" point") != std::string::npos; + + bolt tracer = pbolt; + if (targeted) + { + // For a targeted but rangeless spell make the range positive so that + // fire_tracer() will fill out path_taken. + if (pbolt.range == 0 && pbolt.target != monster->pos()) + tracer.range = ENV_SHOW_DIAMETER; + + fire_tracer(monster, tracer); + } + + std::string targ_prep = "at"; + std::string target = "nothing"; + + if (!targeted) + target = "NO TARGET"; + else if (pbolt.target == you.pos()) + target = "you"; + else if (pbolt.target == monster->pos()) + target = monster->pronoun(PRONOUN_REFLEXIVE); + // Monsters should only use targeted spells while foe == MHITNOT + // if they're targetting themselves. + else if (monster->foe == MHITNOT && !monster->confused()) + target = "NONEXISTENT FOE"; + else if (!invalid_monster_index(monster->foe) + && menv[monster->foe].type == MONS_NO_MONSTER) + { + target = "DEAD FOE"; + } + else if (in_bounds(pbolt.target) && see_cell(pbolt.target)) + { + if (const monsters* mtarg = monster_at(pbolt.target)) + { + if (you.can_see(mtarg)) + target = mtarg->name(DESC_NOCAP_THE); + } + } + + // Monster might be aiming past the real target, or maybe some fuzz has + // been applied because the target is invisible. + if (target == "nothing" && targeted) + { + if (pbolt.aimed_at_spot) + { + int count = 0; + for (adjacent_iterator ai(pbolt.target); ai; ++ai) + { + const actor* act = actor_at(*ai); + if (act && act != monster && you.can_see(act)) + { + targ_prep = "next to"; + + if (act->atype() == ACT_PLAYER || one_chance_in(++count)) + target = act->name(DESC_NOCAP_THE); + + if (act->atype() == ACT_PLAYER) + break; + } + } + } + + const bool visible_path = visible_beam || gestured; + bool mons_targ_aligned = false; + + const std::vector &path = tracer.path_taken; + for (unsigned int i = 0; i < path.size(); i++) + { + const coord_def pos = path[i]; + + if (pos == monster->pos()) + continue; + + const monsters *m = monster_at(pos); + if (pos == you.pos()) + { + // Be egotistical and assume that the monster is aiming at + // the player, rather than the player being in the path of + // a beam aimed at an ally. + if (!mons_wont_attack(monster)) + { + targ_prep = "at"; + target = "you"; + break; + } + // If the ally is confused or aiming at an invisible enemy, + // with the player in the path, act like it's targeted at + // the player if there isn't any visible target earlier + // in the path. + else if (target == "nothing") + { + targ_prep = "at"; + target = "you"; + mons_targ_aligned = true; + } + } + else if (visible_path && m && you.can_see(m)) + { + bool is_aligned = mons_aligned(m->mindex(), monster->mindex()); + std::string name = m->name(DESC_NOCAP_THE); + + if (target == "nothing") + { + mons_targ_aligned = is_aligned; + target = name; + } + // If the first target was aligned with the beam source then + // the first subsequent non-aligned monster in the path will + // take it's place. + else if (mons_targ_aligned && !is_aligned) + { + mons_targ_aligned = false; + target = name; + } + targ_prep = "at"; + } + else if (visible_path && target == "nothing") + { + int count = 0; + for (adjacent_iterator ai(pbolt.target); ai; ++ai) + { + const actor* act = monster_at(*ai); + if (act && act != monster && you.can_see(act)) + { + targ_prep = "past"; + if (act->atype() == ACT_PLAYER + || one_chance_in(++count)) + { + target = act->name(DESC_NOCAP_THE); + } + + if (act->atype() == ACT_PLAYER) + break; + } + } + } + } // for (unsigned int i = 0; i < path.size(); i++) + } // if (target == "nothing" && targeted) + + const actor* foe = monster->get_foe(); + + // If we still can't find what appears to be the target, and the + // monster isn't just throwing the spell in a random direction, + // we should be able to tell what the monster was aiming for if + // we can see the monster's foe and the beam (or the beam path + // implied by gesturing). But only if the beam didn't actually hit + // anything (but if it did hit something, why didn't that monster + // show up in the beam's path?) + if (targeted + && target == "nothing" + && (tracer.foe_info.count + tracer.friend_info.count) == 0 + && foe != NULL + && you.can_see(foe) + && !monster->confused() + && (visible_beam || gestured)) + { + target = foe->name(DESC_NOCAP_THE); + targ_prep = (pbolt.aimed_at_spot ? "next to" : "past"); + } + + // If the monster gestures to create an invisible beam then + // assume that anything close to the beam is the intended target. + // Also, if the monster gestures to create a visible beam but it + // misses still say that the monster gestured "at" the target, + // rather than "past". + if (gestured || target == "nothing") + targ_prep = "at"; + + msg = replace_all(msg, "@at@", targ_prep); + msg = replace_all(msg, "@target@", target); + + std::string beam_name; + if (!targeted) + beam_name = "NON TARGETED BEAM"; + else if (pbolt.name.empty()) + beam_name = "INVALID BEAM"; + else if (!tracer.seen) + beam_name = "UNSEEN BEAM"; + else + beam_name = pbolt.get_short_name(); + + msg = replace_all(msg, "@beam@", beam_name); + + const msg_channel_type chan = + (unseen ? MSGCH_SOUND : + mons_friendly_real(monster) ? MSGCH_FRIEND_SPELL + : MSGCH_MONSTER_SPELL); + + if (silent) + mons_speaks_msg(monster, msg, chan, true); + else if (noisy(noise, monster->pos(), monster->mindex()) || !unseen) + { + // noisy() returns true if the player heard the noise. + mons_speaks_msg(monster, msg, chan); + } +} + + + + + + + diff --git a/crawl-ref/source/mon-cast.h b/crawl-ref/source/mon-cast.h new file mode 100644 index 0000000000..5d10125d98 --- /dev/null +++ b/crawl-ref/source/mon-cast.h @@ -0,0 +1,25 @@ +/* + * File: mon-cast.h + * Summary: Monster spell casting. + * Written by: Linley Henzell + */ + +#ifndef MONCAST_H +#define MONCAST_H + +#include "enum.h" + +class monsters; +class bolt; + +bool handle_mon_spell(monsters *monster, bolt &beem); + +bolt mons_spells(monsters *mons, spell_type spell_cast, int power); +void mons_cast(monsters *monster, bolt &pbolt, spell_type spell_cast, + bool do_noise = true); +void mons_cast_noise(monsters *monster, bolt &pbolt, spell_type spell_cast); +void setup_mons_cast(monsters *monster, bolt &pbolt, spell_type spell_cast); + +void mons_cast_haunt(monsters *monster); + +#endif diff --git a/crawl-ref/source/mon-util.cc b/crawl-ref/source/mon-util.cc index 381fce5e8e..dd7940fd58 100644 --- a/crawl-ref/source/mon-util.cc +++ b/crawl-ref/source/mon-util.cc @@ -21,6 +21,7 @@ #include "itemname.h" #include "kills.h" #include "los.h" +#include "mon-behv.h" #include "monplace.h" #include "monstuff.h" #include "random.h" @@ -3813,3 +3814,9 @@ mon_resist_def mon_resist_def::operator | (const mon_resist_def &o) const mon_resist_def c(*this); return (c |= o); } + +bool player_or_mon_in_sanct(const monsters* monster) +{ + return (is_sanctuary(you.pos()) + || is_sanctuary(monster->pos())); +} diff --git a/crawl-ref/source/mon-util.h b/crawl-ref/source/mon-util.h index c6470c9169..3b6eb54433 100644 --- a/crawl-ref/source/mon-util.h +++ b/crawl-ref/source/mon-util.h @@ -157,17 +157,6 @@ enum mons_class_flags M_NO_EXP_GAIN = (1<<31) // worth 0 xp }; -enum mon_event_type -{ - ME_EVAL, // 0, evaluate monster AI state - ME_DISTURB, // noisy - ME_ANNOY, // annoy at range - ME_ALERT, // alert to presence - ME_WHACK, // physical attack - ME_SCARE, // frighten monster - ME_CORNERED // cannot flee -}; - enum mon_intel_type // Must be in increasing intelligence order { I_PLANT = 0, @@ -894,4 +883,6 @@ mon_inv_type item_to_mslot(const item_def &item); int scan_mon_inv_randarts(const monsters *mon, artefact_prop_type ra_prop); + +bool player_or_mon_in_sanct(const monsters* monster); #endif diff --git a/crawl-ref/source/monplace.cc b/crawl-ref/source/monplace.cc index 33ec1450e2..8f0eed6800 100644 --- a/crawl-ref/source/monplace.cc +++ b/crawl-ref/source/monplace.cc @@ -19,8 +19,9 @@ #include "los.h" #include "makeitem.h" #include "message.h" -#include "monstuff.h" +#include "mon-behv.h" #include "mon-pick.h" +#include "monstuff.h" #include "mon-util.h" #include "player.h" #include "random.h" diff --git a/crawl-ref/source/monster.cc b/crawl-ref/source/monster.cc index a12666dc2e..637fb3c078 100644 --- a/crawl-ref/source/monster.cc +++ b/crawl-ref/source/monster.cc @@ -17,9 +17,10 @@ #include "items.h" #include "kills.h" #include "misc.h" +#include "mon-abil.h" +#include "mon-behv.h" #include "monplace.h" #include "monstuff.h" -#include "mstuff2.h" #include "mtransit.h" #include "random.h" #include "religion.h" diff --git a/crawl-ref/source/monstuff.cc b/crawl-ref/source/monstuff.cc index 3f4e706d27..0f01faf0e4 100644 --- a/crawl-ref/source/monstuff.cc +++ b/crawl-ref/source/monstuff.cc @@ -7,55 +7,36 @@ #include "AppHdr.h" #include "monstuff.h" -#include -#include -#include -#include +//#include +//#include +//#include +//#include #ifdef TARGET_OS_DOS #include #endif -#include "externs.h" - #include "arena.h" #include "artefact.h" -#include "beam.h" #include "cloud.h" -#include "colour.h" -#include "database.h" -#include "debug.h" #include "delay.h" -#include "describe.h" #include "dgnevent.h" #include "directn.h" -#include "exclude.h" -#include "fight.h" #include "files.h" -#include "ghost.h" #include "godabil.h" -#include "hiscores.h" -#include "it_use2.h" -#include "itemname.h" #include "items.h" -#include "itemprop.h" #include "kills.h" -#include "los.h" -#include "makeitem.h" -#include "mapmark.h" #include "message.h" #include "misc.h" +#include "mon-behv.h" #include "monplace.h" #include "monspeak.h" -#include "mon-util.h" -#include "mutation.h" -#include "mstuff2.h" #include "notes.h" #include "player.h" +#include "random.h" #include "religion.h" #include "spl-mis.h" #include "spl-util.h" -#include "spells3.h" #include "state.h" #include "stuff.h" #include "terrain.h" @@ -67,38 +48,6 @@ #include "xom.h" static bool _wounded_damaged(monster_type mon_type); -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 _is_item_jelly_edible(const item_def &item); - -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... @@ -2012,70 +1961,6 @@ void monster_cleanup(monsters *monster) 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) { @@ -2593,52 +2478,6 @@ void slimify_monster(monsters *mon, bool hostile) mons_make_god_gift(mon, GOD_JIYVA); } -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; - } -} - -static void _set_random_slime_target(monsters* mon) -{ - // Strictly neutral slimes will go for the nearest item. - int item_idx; - coord_def orig_target = mon->target; - - for (radius_iterator ri(mon->pos(), LOS_RADIUS, true, false); ri; ++ri) - { - item_idx = igrd(*ri); - if (item_idx != NON_ITEM) - { - for (stack_iterator si(*ri); si; ++si) - { - item_def& item(*si); - - if (_is_item_jelly_edible(item)) - { - mon->target = *ri; - break; - } - } - } - } - - if (mon->target == mon->pos() || mon->target == you.pos()) - _set_random_target(mon); -} - // 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, @@ -2958,6602 +2797,480 @@ static bool _wounded_damaged(monster_type mon_type) return (holi == MH_UNDEAD || holi == MH_NONLIVING || holi == MH_PLANT); } -//--------------------------------------------------------------- -// -// 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) +// 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) { - 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 = mon->asleep(); - - if (src == MHITYOU) - sourceWontAttack = true; - else if (src != MHITNOT) - sourceWontAttack = mons_wont_attack_real( &menv[src] ); + const dungeon_feature_type feat = grd(mon->pos()); + const bool is_shaft = (get_trap_type(mon->pos()) == TRAP_SHAFT); - if (is_sanctuary(mon->pos()) && mons_is_fleeing_sanctuary(mon)) + if (feat_is_gate(feat)) + simple_monster_message(mon, " passes through the gate."); + else if (feat_is_travelable_stair(feat)) { - mon->behaviour = BEH_FLEE; - mon->foe = MHITYOU; - mon->target = env.sanctuary_pos; - return; + command_type dir = feat_stair_direction(feat); + simple_monster_message(mon, + make_stringf(" %s the %s.", + dir == CMD_GO_UPSTAIRS ? "goes up" : + dir == CMD_GO_DOWNSTAIRS ? "goes down" + : "takes", + feat_is_escape_hatch(feat) ? "escape hatch" + : "stairs").c_str()); } - - switch (event) + else if (is_shaft) { - case ME_DISTURB: - // Assumes disturbed by noise... - if (mon->asleep()) - { - mon->behaviour = BEH_WANDER; - - if (mons_near(mon)) - remove_auto_exclude(mon, true); - } - - // A bit of code to make Projected 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))) - { - // Monster types that you can't gain experience from cannot - // fight back, so don't bother having them do so. If you - // worship Feawn, create a ring of friendly plants, and try - // to break out of the ring by killing a plant, you'll get - // a warning prompt and penance only once. Without the - // hostility check, the plant will remain friendly until it - // dies, and you'll get a warning prompt and penance once - // *per hit*. This may not be the best way to address the - // issue, though. -cao - if (mons_class_flag(mon->type, M_NO_EXP_GAIN) - && mon->attitude != ATT_FRIENDLY - && mon->attitude != ATT_GOOD_NEUTRAL) - { - return; - } + simple_monster_message(mon, + make_stringf(" %s the shaft.", + mons_flies(mon) ? "goes down" + : "jumps into").c_str()); + } +} - mon->foe = src; +void make_mons_leave_level(monsters *mon) +{ + if (mons_is_pacified(mon)) + { + if (you.can_see(mon)) + _mons_indicate_level_exit(mon); - if (mon->asleep() && mons_near(mon)) - remove_auto_exclude(mon, true); + // Pacified monsters leaving the level take their stuff with + // them. + mon->flags |= MF_HARD_RESET; + monster_die(mon, KILL_DISMISSED, NON_MONSTER); + } +} - if (!mons_is_cornered(mon)) - mon->behaviour = BEH_SEEK; +// Checks whether there is a straight path from p1 to p2 that passes +// through features >= allowed. +// If it exists, such a path may be missed; on the other hand, it +// is not guaranteed that p2 is visible from p1 according to LOS rules. +// Not symmetric. +bool can_go_straight(const coord_def& p1, const coord_def& p2, + dungeon_feature_type allowed) +{ + if (distance(p1, p2) > get_los_radius_sq()) + return (false); - if (src == MHITYOU) - { - mon->attitude = ATT_HOSTILE; - breakCharm = true; - } - } + dungeon_feature_type max_disallowed = DNGN_MAXOPAQUE; + if (allowed != DNGN_UNSEEN) + max_disallowed = static_cast(allowed - 1); - // Now set target so that monster can whack back (once) at an - // invisible foe. - if (event == ME_WHACK) - setTarget = true; - break; + return (!num_feats_between(p1, p2, DNGN_UNSEEN, max_disallowed, + true, true)); +} - 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() - && !mon->asleep()) - { - break; - } +// The default suitable() function for choose_random_nearby_monster(). +bool choose_any_monster(const monsters* mon) +{ + return (true); +} - if (mon->asleep() && mons_near(mon)) - remove_auto_exclude(mon, 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); +} - // 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; - } +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; - if (mon->foe == MHITNOT) - mon->foe = src; + // 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); - if (!src_pos.origin() - && (mon->foe == MHITNOT || mon->foe == src - || mons_is_wandering(mon))) + for (; ri; ++ri) + { + if (monsters *mon = monster_at(*ri)) { - 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_cell(mon->pos())) + if (suitable(mon)) { - 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 (mon->holiness() == MH_PLANT - || mon->holiness() == MH_NONLIVING) - { - mon->del_ench(ENCH_FEAR, true, true); - break; - } + // 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. - // 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) && !crawl_state.arena) - mon->foe = MHITYOU; + // 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) - if (see_cell(mon->pos())) - learned_something_new(TUT_FLEEING_MONSTER); - break; + // Named or priestly monsters have doubled chances. + int mon_weight = 1; - case ME_CORNERED: - // Some monsters can't flee. - if (mon->behaviour != BEH_FLEE && !mon->has_ench(ENCH_FEAR)) - break; + if (prefer_named && mon->is_named()) + mon_weight++; - // Pacified monsters shouldn't change their behaviour. - if (mons_is_pacified(mon)) - break; + if (prefer_priest && mons_class_flag(mon->type, M_PRIEST)) + mon_weight++; - // Just set behaviour... foe doesn't change. - if (!mons_is_cornered(mon)) - { - if (mons_friendly(mon) && !crawl_state.arena) - { - mon->foe = MHITYOU; - simple_monster_message(mon, " returns to your side!"); + if (x_chance_in_y(mon_weight, (weight += mon_weight))) + chosen = mon; } - 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); + return chosen; +} - // Do any resultant foe or state changes. - _handle_behaviour(mon); - ASSERT(in_bounds(mon->target) || mon->target.origin()); +// 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 it woke up and you're its new foe, it might shout. - if (was_sleeping && !mon->asleep() && allow_shout - && mon->foe == MHITYOU && !mons_wont_attack(mon)) + if ((mons_near(monster) || crawl_state.arena) + && (channel == MSGCH_MONSTER_SPELL || channel == MSGCH_FRIEND_SPELL + || monster->visible_to(&you) || crawl_state.arena)) { - handle_monster_shouts(mon); - } + std::string msg = monster->name(descrip); + msg += event; + msg = apostrophise_fixup(msg); - const bool wasLurking = - (old_behaviour == BEH_LURK && !mons_is_lurking(mon)); - const bool isPacified = mons_is_pacified(mon); + if (channel == MSGCH_PLAIN && mons_wont_attack_real(monster)) + channel = MSGCH_FRIEND_ACTION; - 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; + mpr(msg.c_str(), channel, param); + return (true); } - ASSERT(!crawl_state.arena - || mon->foe != MHITYOU && mon->target != you.pos()); + return (false); } -static bool _choose_random_patrol_target_grid(monsters *mon) +bool mons_avoids_cloud(const monsters *monster, cloud_struct cloud, + bool placement) { - 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); + bool extra_careful = placement; + cloud_type cl_type = cloud.type; - // Can the monster see the patrol point from its current position? - const bool patrol_seen = mon->mon_see_cell(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); - - los_def patrol(mon->patrol_point, opacity_monmove(*mon), bounds_radius(rad)); - patrol.update(); - los_def lm(mon->pos(), opacity_monmove(*mon)); - if (is_smart || !patrol_seen) - { - // For stupid monsters, don't bother if the patrol point is in sight. - lm.update(); - } - - 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.see_cell(*ri) && - (!is_smart || !lm.see_cell(*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.see_cell(*ri) || - !lm.see_cell(*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) - { - // XXX: remove explicit coordinate translation. - const coord_def ep = *ri - you.pos() + coord_def(ENV_SHOW_OFFSET, - ENV_SHOW_OFFSET); - 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_cell(*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 (feat_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 feat = grd(mon->pos()); - const bool is_shaft = (get_trap_type(mon->pos()) == TRAP_SHAFT); - - if (feat_is_gate(feat)) - simple_monster_message(mon, " passes through the gate."); - else if (feat_is_travelable_stair(feat)) - { - command_type dir = feat_stair_direction(feat); - simple_monster_message(mon, - make_stringf(" %s the %s.", - dir == CMD_GO_UPSTAIRS ? "goes up" : - dir == CMD_GO_DOWNSTAIRS ? "goes down" - : "takes", - feat_is_escape_hatch(feat) ? "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); -} - -// Checks whether there is a straight path from p1 to p2 that passes -// through features >= allowed. -// If it exists, such a path may be missed; on the other hand, it -// is not guaranteed that p2 is visible from p1 according to LOS rules. -// Not symmetric. -bool can_go_straight(const coord_def& p1, const coord_def& p2, - dungeon_feature_type allowed) -{ - if (distance(p1, p2) > get_los_radius_sq()) - return (false); - - dungeon_feature_type max_disallowed = DNGN_MAXOPAQUE; - if (allowed != DNGN_UNSEEN) - max_disallowed = static_cast(allowed - 1); - - return (!num_feats_between(p1, p2, DNGN_UNSEEN, max_disallowed, - true, true)); -} - -// 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 - // realise 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 - || can_go_straight(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 (can_go_straight(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 (can_go_straight(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); - } - - 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 (!feat_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 (cell_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; - } - - 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 (!cell_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 (!can_go_straight(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 (can_go_straight(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); - - // Whether the player is in LOS of the monster and can see - // or has guessed the player's location. - 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 targetting the player in arena mode. - else if (mon->foe == MHITYOU) - mon->foe = MHITNOT; - } - - if (mons_wall_shielded(mon) && cell_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.visible_to(mon)) - { - proxPlayer = false; - - const int intel = mons_intel(mon); - // 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. - 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->mons_species() != MONS_GIANT_SPORE ) - { - mon->foe = you.pet_target; - } - - // Instead, berserkers attack nearest monsters. - if ((mon->has_ench(ENCH_BERSERK) || mon->mons_species() == MONS_GIANT_SPORE) - && (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. - // Others choose the nearest foe. - if (!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) - { - actor* afoe = mon->get_foe(); - proxFoe = afoe && mon->can_see(afoe); - - coord_def foepos = coord_def(0,0); - if (afoe) - foepos = afoe->pos(); - - if (mon->foe == MHITYOU) - proxFoe = proxPlayer; // Take invis into account. - - // 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 (crawl_state.arena || !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 || crawl_state.arena) - { - new_foe = MHITNOT; - new_beh = BEH_WANDER; - } - else - { - new_foe = MHITYOU; - mon->target = foepos; - } - break; - } - - ASSERT(mon->foe != MHITNOT); - if (mon->foe_memory > 0) - { - // 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 = 0; - } - else - { - if (coinflip()) // XXX: cheesy! - mon->target = menv[mon->foe].pos(); - else - mon->foe_memory = 0; - } - } - - // Either keep chasing, or start wandering. - if (mon->foe_memory < 2) - { - mon->foe_memory = 0; - new_beh = BEH_WANDER; - } - break; - } - - ASSERT(mon->foe_memory == 0); - // Hack: smarter monsters will tend to pursue the player longer. - switch (mons_intel(mon)) - { - case I_HIGH: - mon->foe_memory = 100 + random2(200); - break; - case I_NORMAL: - mon->foe_memory = 50 + random2(100); - break; - case I_ANIMAL: - case I_INSECT: - mon->foe_memory = 25 + random2(75); - break; - case I_PLANT: - mon->foe_memory = 10 + random2(50); - break; - } - break; // switch/case BEH_SEEK - } - - ASSERT(proxFoe && mon->foe != MHITNOT); - // 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) - && mon->holiness() != MH_PLANT - && mon->holiness() != 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; - } - - if (mons_strict_neutral(mon) && mons_is_slime(mon) - && you.religion == GOD_JIYVA) - { - _set_random_slime_target(mon); - } - - // 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 and nonliving monsters cannot fight back. - if (mon->holiness() == MH_PLANT - || mon->holiness() == 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 && cell_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_foe(monsters *mon, const coord_def& p, - bool friendly, bool neutral) -{ - if (!inside_level_bounds(p)) - return (false); - - if (!friendly && !neutral && p == you.pos() - && you.visible_to(mon) && !is_sanctuary(p)) - { - return (true); - } - - if (monsters *foe = monster_at(p)) - { - if (foe != mon - && mon->can_see(foe) - && (friendly || !is_sanctuary(p)) - && (mons_friendly(foe) != friendly - || (neutral && !mons_neutral(foe)))) - { - return (true); - } - } - return (false); -} - -// Choose random nearest monster as a foe. -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) - { - std::vector monster_pos; - 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 (_mons_check_foe(mon, p, friendly, neutral)) - monster_pos.push_back(p); - } - if (monster_pos.empty()) - continue; - - const coord_def mpos = monster_pos[random2(monster_pos.size())]; - if (mpos == you.pos()) - mon->foe = MHITYOU; - else - mon->foe = env.mgrid(mpos); - return; - } -} - -// 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 - || monster->visible_to(&you) || 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_cell(monster->target, !glass_ok))) - { - monster_pathfind mp; - if (mp.init_pathfind(monster, monster->target)) - delta = mp.next_pos(monster->pos()) - monster->pos(); - } - } - } - - // Move the monster. - mmov.x = (delta.x > 0) ? 1 : ((delta.x < 0) ? -1 : 0); - mmov.y = (delta.y > 0) ? 1 : ((delta.y < 0) ? -1 : 0); - - if (mons_is_fleeing(monster) && monster->travel_target != MTRAV_WALL - && (!mons_friendly(monster) - || monster->target != you.pos())) - { - mmov *= -1; - } - - // Don't allow monsters to enter a sanctuary or attack you inside a - // sanctuary, even if you're right next to them. - if (is_sanctuary(monster->pos() + mmov) - && (!is_sanctuary(monster->pos()) - || monster->pos() + mmov == you.pos())) - { - mmov.reset(); - } - - // Bounds check: don't let fleeing monsters try to run off the grid. - const coord_def s = monster->pos() + mmov; - if (!in_bounds_x(s.x)) - mmov.x = 0; - if (!in_bounds_y(s.y)) - mmov.y = 0; - - // Now quit if we can't move. - if (mmov.origin()) - return; - - if (delta.rdist() > 3) - { - // Reproduced here is some semi-legacy code that makes monsters - // move somewhat randomly along oblique paths. It is an - // exceedingly good idea, given crawl's unique line of sight - // properties. - // - // Added a check so that oblique movement paths aren't used when - // close to the target square. -- bwr - - // Sometimes we'll just move parallel the x axis. - if (abs(delta.x) > abs(delta.y) && coinflip()) - mmov.y = 0; - - // Sometimes we'll just move parallel the y axis. - if (abs(delta.y) > abs(delta.x) && coinflip()) - mmov.x = 0; - } - - const coord_def newpos(monster->pos() + mmov); - FixedArray < bool, 3, 3 > good_move; - - for (int count_x = 0; count_x < 3; count_x++) - for (int count_y = 0; count_y < 3; count_y++) - { - const int targ_x = monster->pos().x + count_x - 1; - const int targ_y = monster->pos().y + count_y - 1; - - // Bounds check: don't consider moving out of grid! - if (!in_bounds(targ_x, targ_y)) - { - good_move[count_x][count_y] = false; - continue; - } - - good_move[count_x][count_y] = - _mon_can_move_to_pos(monster, coord_def(count_x-1, count_y-1)); - } - - if (mons_wall_shielded(monster)) - { - // The rock worm will try to move along through rock for as long as - // possible. If the player is walking through a corridor, for example, - // moving along in the wall beside him is much preferable to actually - // leaving the wall. - // This might cause the rock worm to take detours but it still - // comes off as smarter than otherwise. - if (mmov.x != 0 && mmov.y != 0) // diagonal movement - { - bool updown = false; - bool leftright = false; - - coord_def t = monster->pos() + coord_def(mmov.x, 0); - if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t))) - updown = true; - - t = monster->pos() + coord_def(0, mmov.y); - if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t))) - leftright = true; - - if (updown && (!leftright || coinflip())) - mmov.y = 0; - else if (leftright) - mmov.x = 0; - } - else if (mmov.x == 0 && monster->target.x == monster->pos().x) - { - bool left = false; - bool right = false; - coord_def t = monster->pos() + coord_def(-1, mmov.y); - if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t))) - left = true; - - t = monster->pos() + coord_def(1, mmov.y); - if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t))) - right = true; - - if (left && (!right || coinflip())) - mmov.x = -1; - else if (right) - mmov.x = 1; - } - else if (mmov.y == 0 && monster->target.y == monster->pos().y) - { - bool up = false; - bool down = false; - coord_def t = monster->pos() + coord_def(mmov.x, -1); - if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t))) - up = true; - - t = monster->pos() + coord_def(mmov.x, 1); - if (in_bounds(t) && feat_is_rock(grd(t)) && !feat_is_permarock(grd(t))) - down = true; - - if (up && (!down || coinflip())) - mmov.y = -1; - else if (down) - mmov.y = 1; - } - } - - // If the monster is moving in your direction, whether to attack or - // protect you, or towards a monster it intends to attack, check - // whether we first need to take a step to the side to make sure the - // reinforcement can follow through. Only do this with 50% chance, - // though, so it's not completely predictable. - - // First, check whether the monster is smart enough to even consider - // this. - if ((newpos == you.pos() - || mgrd(newpos) != NON_MONSTER && monster->foe == mgrd(newpos)) - && mons_intel(monster) >= I_ANIMAL - && coinflip() - && !mons_is_confused(monster) && !mons_is_caught(monster) - && !monster->has_ench(ENCH_BERSERK)) - { - // If the monster is moving parallel to the x or y axis, check - // whether - // - // a) the neighbouring grids are blocked - // b) there are other unblocked grids adjacent to the target - // c) there's at least one allied monster waiting behind us. - // - // (For really smart monsters, also check whether there's a - // monster farther back in the corridor that has some kind of - // ranged attack.) - if (mmov.y == 0) - { - if (!good_move[1][0] && !good_move[1][2] - && (good_move[mmov.x+1][0] || good_move[mmov.x+1][2]) - && (_allied_monster_at(monster, coord_def(-mmov.x, -1), - coord_def(-mmov.x, 0), - coord_def(-mmov.x, 1)) - || mons_intel(monster) >= I_NORMAL - && !mons_wont_attack_real(monster) - && _ranged_allied_monster_in_dir(monster, - coord_def(-mmov.x, 0)))) - { - if (good_move[mmov.x+1][0]) - mmov.y = -1; - if (good_move[mmov.x+1][2] && (mmov.y == 0 || coinflip())) - mmov.y = 1; - } - } - else if (mmov.x == 0) - { - if (!good_move[0][1] && !good_move[2][1] - && (good_move[0][mmov.y+1] || good_move[2][mmov.y+1]) - && (_allied_monster_at(monster, coord_def(-1, -mmov.y), - coord_def(0, -mmov.y), - coord_def(1, -mmov.y)) - || mons_intel(monster) >= I_NORMAL - && !mons_wont_attack_real(monster) - && _ranged_allied_monster_in_dir(monster, - coord_def(0, -mmov.y)))) - { - if (good_move[0][mmov.y+1]) - mmov.x = -1; - if (good_move[2][mmov.y+1] && (mmov.x == 0 || coinflip())) - mmov.x = 1; - } - } - else // We're moving diagonally. - { - if (good_move[mmov.x+1][1]) - { - if (!good_move[1][mmov.y+1] - && (_allied_monster_at(monster, coord_def(-mmov.x, -1), - coord_def(-mmov.x, 0), - coord_def(-mmov.x, 1)) - || mons_intel(monster) >= I_NORMAL - && !mons_wont_attack_real(monster) - && _ranged_allied_monster_in_dir(monster, - coord_def(-mmov.x, -mmov.y)))) - { - mmov.y = 0; - } - } - else if (good_move[1][mmov.y+1] - && (_allied_monster_at(monster, coord_def(-1, -mmov.y), - coord_def(0, -mmov.y), - coord_def(1, -mmov.y)) - || mons_intel(monster) >= I_NORMAL - && !mons_wont_attack_real(monster) - && _ranged_allied_monster_in_dir(monster, - coord_def(-mmov.x, -mmov.y)))) - { - mmov.x = 0; - } - } - } - - // Now quit if we can't move. - if (mmov.origin()) - return; - - // Try to stay in sight of the player if we're moving towards - // him/her, in order to avoid the monster coming into view, - // shouting, and then taking a step in a path to the player which - // temporarily takes it out of view, which can lead to the player - // getting "comes into view" and shout messages with no monster in - // view. - - // Doesn't matter for arena mode. - if (crawl_state.arena) - return; - - // Did we just come into view? - if (monster->seen_context != _just_seen) - return; - - monster->seen_context.clear(); - - // If the player can't see us, it doesn't matter. - if (!(monster->flags & MF_WAS_IN_VIEW)) - return; - - const coord_def old_pos = monster->pos(); - const int old_dist = grid_distance(you.pos(), old_pos); - - // We're not moving towards the player. - if (grid_distance(you.pos(), old_pos + mmov) >= old_dist) - { - // Give a message if we move back out of view. - monster->seen_context = _just_seen; - return; - } - - // We're already staying in the player's LOS. - if (see_cell(old_pos + mmov)) - return; - - // Try to find a move that brings us closer to the player while - // keeping us in view. - int matches = 0; - for (int i = 0; i < 3; i++) - for (int j = 0; j < 3; j++) - { - if (i == 0 && j == 0) - continue; - - if (!good_move[i][j]) - continue; - - delta.set(i - 1, j - 1); - coord_def tmp = old_pos + delta; - - if (grid_distance(you.pos(), tmp) < old_dist && see_cell(tmp)) - { - if (one_chance_in(++matches)) - mmov = delta; - break; - } - } - - // The only way to get closer to the player is to step out of view; - // give a message so they player isn't confused about its being - // announced as coming into view but not being seen. - monster->seen_context = _just_seen; -} - -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_struct cloud, - bool placement) -{ - bool extra_careful = placement; - cloud_type cl_type = cloud.type; - - if (placement) - extra_careful = true; - - switch (cl_type) - { - case CLOUD_MIASMA: - // Even the dumbest monsters will avoid miasma if they can. - return (!monster->res_rotting()); - - case CLOUD_FIRE: - case CLOUD_FOREST_FIRE: - if (monster->res_fire() > 1) - return (false); - - if (extra_careful) - return (true); - - if (mons_intel(monster) >= I_ANIMAL && monster->res_fire() < 0) - return (true); - - if (monster->hit_points >= 15 + random2avg(46, 5)) - return (false); - break; - - case CLOUD_STINK: - if (monster->res_poison() > 0) - return (false); - - if (extra_careful) - return (true); - - if (mons_intel(monster) >= I_ANIMAL && monster->res_poison() < 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 (monster->res_cold() > 1) - return (false); - - if (extra_careful) - return (true); - - if (mons_intel(monster) >= I_ANIMAL && monster->res_cold() < 0) - return (true); - - if (monster->hit_points >= 15 + random2avg(46, 5)) - return (false); - break; - - case CLOUD_POISON: - if (monster->res_poison() > 0) - return (false); - - if (extra_careful) - return (true); - - if (mons_intel(monster) >= I_ANIMAL && monster->res_poison() < 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 (monster->res_fire() > 0) - return (false); - - if (monster->hit_points >= random2avg(19, 2)) - return (false); - break; - - case CLOUD_RAIN: - // Fiery monsters dislike the rain. - if (monster->is_fiery() && extra_careful) - return (true); - - // We don't care about what's underneath the rain cloud if we can fly. - if (monster->flight_mode() != FL_NONE) - return (false); - - // These don't care about deep water. - if (monster_habitable_grid(monster, DNGN_DEEP_WATER)) - return (false); - - // This position could become deep water, and they might drown. - if (grd(cloud.pos) == DNGN_SHALLOW_WATER) - return (true); - - // Otherwise, it's safe for everyone else. - 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, 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, 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 - || !monster->can_see(foe) - || monster->asleep() - || monster->submerged()) - { - 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_feat_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) - && !mon->asleep() - && swap_check(mon, you.pos(), true)) - { - swapping = true; - } - else if (!mon->submerged()) - 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 ); - - // Slime creatures can split while out of sight. - if ((!mons_near(monster) - || monster->asleep() - || monster->submerged()) - && monster->mons_species() != MONS_SLIME_CREATURE) - { - 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_UGLY_THING: - case MONS_VERY_UGLY_THING: - // A (very) ugly thing's proximity to you if you're glowing, or - // to others of its kind, can mutate it into a different (very) - // ugly thing. - used = ugly_thing_mutate(monster, true); - break; - - case MONS_SLIME_CREATURE: - // Slime creatures may split or merge depending on the - // situation. - used = slime_split_merge(monster); - if (!monster->alive()) - return (true); - break; - - 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 == MONS_NO_MONSTER) - 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 (!feat_is_solid(grd(t))) - { - monster->hit_points = -1; - used = true; - break; - } - } - break; - - case MONS_LAVA_SNAKE: - if (monster->has_ench(ENCH_CONFUSION)) - break; - - if (!you.visible_to(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_ELECTRIC_EEL: - if (monster->has_ench(ENCH_CONFUSION)) - break; - - if (!you.visible_to(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 (!you.visible_to(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 (!you.visible_to(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(), monster->mindex(), 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 (monster->asleep() - || monster->inv[MSLOT_POTION] == NON_ITEM - || !one_chance_in(3)) - { - return (false); - } - - bool rc = false; - - const int potion_idx = monster->inv[MSLOT_POTION]; - item_def& potion = mitm[potion_idx]; - const potion_type ptype = static_cast(potion.sub_type); - - if (monster->can_drink_potion(ptype) && monster->should_drink_potion(ptype)) - { - const bool was_visible = you.can_see(monster); - - // Drink the potion. - const item_type_id_state_type id = monster->drink_potion_effect(ptype); - - // Give ID if necessary. - if (was_visible && id != ID_UNKNOWN_TYPE) - set_ident_type(OBJ_POTIONS, ptype, id); - - // Remove it from inventory. - if (dec_mitm_item_quantity(potion_idx, 1)) - monster->inv[MSLOT_POTION] = NON_ITEM; - else if (is_blood_potion(potion)) - remove_oldest_blood_potion(potion); - - monster->lose_energy(EUT_ITEM); - rc = true; - } - - return (rc); -} - -static bool _handle_reaching(monsters *monster) -{ - bool ret = false; - const int wpn = monster->inv[MSLOT_WEAPON]; - - if (monster->submerged()) - return (false); - - if (mons_aligned(monster_index(monster), monster->foe)) - return (false); - - if (wpn != NON_ITEM && get_weapon_brand(mitm[wpn]) == SPWPN_REACHING) - { - if (monster->foe == MHITYOU) - { - const coord_def delta = monster->pos() - you.pos(); - const int x_middle = std::max(monster->pos().x, you.pos().x) - - (abs(delta.x) / 2); - const int y_middle = std::max(monster->pos().y, you.pos().y) - - (abs(delta.y) / 2); - const coord_def middle(x_middle, y_middle); - - // This check isn't redundant -- player may be invisible. - if (monster->target == you.pos() - && grid_distance(monster->pos(), you.pos()) == 2 - && (see_cell_no_trans(monster->pos()) - || grd(middle) > DNGN_MAX_NONREACH)) - { - ret = true; - monster_attack(monster, false); - } - } - else if (monster->foe != MHITNOT) - { - monsters& mfoe = menv[monster->foe]; - coord_def foepos = mfoe.pos(); - // Same comments as to invisibility as above. - if (monster->target == foepos - && monster->mon_see_cell(foepos, true) - && grid_distance(monster->pos(), foepos) == 2) - { - ret = true; - monsters_fight(monster, &mfoe, false); - } - } - } - - // Player saw the item reach. - if (ret && !is_artefact(mitm[wpn]) && you.can_see(monster)) - set_ident_flags(mitm[wpn], ISFLAG_KNOW_TYPE); - - return (ret); -} - -//--------------------------------------------------------------- -// -// handle_scroll -// -// Give the monster a chance to read a scroll. Returns true if -// the monster read something. -// -//--------------------------------------------------------------- -static bool _handle_scroll(monsters *monster) -{ - // Yes, there is a logic to this ordering {dlb}: - if (monster->asleep() - || mons_is_confused(monster) - || monster->submerged() - || monster->inv[MSLOT_SCROLL] == NON_ITEM - || !one_chance_in(3)) - { - return (false); - } - - // Make sure the item actually is a scroll. - if (mitm[monster->inv[MSLOT_SCROLL]].base_type != OBJ_SCROLLS) - return (false); - - bool read = false; - item_type_id_state_type ident = ID_UNKNOWN_TYPE; - bool was_visible = you.can_see(monster); - - // Notice how few cases are actually accounted for here {dlb}: - const int scroll_type = mitm[monster->inv[MSLOT_SCROLL]].sub_type; - switch (scroll_type) - { - case SCR_TELEPORTATION: - if (!monster->has_ench(ENCH_TP)) - { - if (mons_is_caught(monster) || mons_is_fleeing(monster) - || mons_is_pacified(monster)) - { - simple_monster_message(monster, " reads a scroll."); - monster_teleport(monster, false); - read = true; - ident = ID_KNOWN_TYPE; - } - } - break; - - case SCR_BLINKING: - if (mons_is_caught(monster) || mons_is_fleeing(monster) - || mons_is_pacified(monster)) - { - if (mons_near(monster)) - { - simple_monster_message(monster, " reads a scroll."); - monster_blink(monster); - read = true; - ident = ID_KNOWN_TYPE; - } - } - break; - - case SCR_SUMMONING: - if (mons_near(monster)) - { - simple_monster_message(monster, " reads a scroll."); - const int mon = create_monster( - mgen_data(MONS_ABOMINATION_SMALL, SAME_ATTITUDE(monster), - 0, 0, monster->pos(), monster->foe, MG_FORCE_BEH)); - - read = true; - if (mon != -1) - { - if (you.can_see(&menv[mon])) - { - mprf("%s appears!", menv[mon].name(DESC_CAP_A).c_str()); - ident = ID_KNOWN_TYPE; - } - player_angers_monster(&menv[mon]); - } - else if (you.can_see(monster)) - canned_msg(MSG_NOTHING_HAPPENS); - } - break; - } - - if (read) - { - if (dec_mitm_item_quantity(monster->inv[MSLOT_SCROLL], 1)) - monster->inv[MSLOT_SCROLL] = NON_ITEM; - - if (ident != ID_UNKNOWN_TYPE && was_visible) - set_ident_type(OBJ_SCROLLS, scroll_type, ident); - - monster->lose_energy(EUT_ITEM); - } - - return read; -} - -//--------------------------------------------------------------- -// -// handle_wand -// -// Give the monster a chance to zap a wand. Returns true if the -// monster zapped. -// -//--------------------------------------------------------------- -static bool _handle_wand(monsters *monster, bolt &beem) -{ - // Yes, there is a logic to this ordering {dlb}: - if (!mons_near(monster) - || monster->asleep() - || monster->has_ench(ENCH_SUBMERGED) - || monster->inv[MSLOT_WAND] == NON_ITEM - || mitm[monster->inv[MSLOT_WAND]].plus <= 0 - || coinflip()) - { - return (false); - } - - bool niceWand = false; - bool zap = false; - bool was_visible = you.can_see(monster); - - item_def &wand(mitm[monster->inv[MSLOT_WAND]]); - - // map wand type to monster spell type - const spell_type mzap = _map_wand_to_mspell(wand.sub_type); - if (mzap == SPELL_NO_SPELL) - return (false); - - // set up the beam - int power = 30 + monster->hit_dice; - bolt theBeam = mons_spells(monster, mzap, power); - - beem.name = theBeam.name; - beem.beam_source = monster_index(monster); - beem.source = monster->pos(); - beem.colour = theBeam.colour; - beem.range = theBeam.range; - beem.damage = theBeam.damage; - beem.ench_power = theBeam.ench_power; - beem.hit = theBeam.hit; - beem.type = theBeam.type; - beem.flavour = theBeam.flavour; - beem.thrower = theBeam.thrower; - beem.is_beam = theBeam.is_beam; - beem.is_explosion = theBeam.is_explosion; - -#if HISCORE_WEAPON_DETAIL - beem.aux_source = - wand.name(DESC_QUALNAME, false, true, false, false); -#else - beem.aux_source = - wand.name(DESC_QUALNAME, false, true, false, false, - ISFLAG_KNOW_CURSE | ISFLAG_KNOW_PLUSES); -#endif - - const int wand_type = wand.sub_type; - switch (wand_type) - { - case WAND_DISINTEGRATION: - // Dial down damage from wands of disintegration, since - // disintegration beams can do large amounts of damage. - beem.damage.size = beem.damage.size * 2 / 3; - break; - - case WAND_ENSLAVEMENT: - case WAND_DIGGING: - case WAND_RANDOM_EFFECTS: - // These have been deemed "too tricky" at this time {dlb}: - return (false); - - case WAND_POLYMORPH_OTHER: - // Monsters can be very trigger happy with wands, reduce this - // for polymorph. - if (!one_chance_in(5)) - return (false); - break; - - // These are wands that monsters will aim at themselves {dlb}: - case WAND_HASTING: - if (!monster->has_ench(ENCH_HASTE)) - { - beem.target = monster->pos(); - niceWand = true; - break; - } - return (false); - - case WAND_HEALING: - if (monster->hit_points <= monster->max_hit_points / 2) - { - beem.target = monster->pos(); - niceWand = true; - break; - } - return (false); - - case WAND_INVISIBILITY: - if (!monster->has_ench(ENCH_INVIS) - && !monster->has_ench(ENCH_SUBMERGED) - && (!mons_friendly(monster) || you.can_see_invisible(false))) - { - beem.target = monster->pos(); - niceWand = true; - break; - } - return (false); - - case WAND_TELEPORTATION: - if (monster->hit_points <= monster->max_hit_points / 2 - || mons_is_caught(monster)) - { - if (!monster->has_ench(ENCH_TP) - && !one_chance_in(20)) - { - beem.target = monster->pos(); - niceWand = true; - break; - } - // This break causes the wand to be tried on the player. - break; - } - return (false); - } - - // Fire tracer, if necessary. - if (!niceWand) - { - fire_tracer( monster, beem ); - - // Good idea? - zap = mons_should_fire(beem); - } - - if (niceWand || zap) - { - if (!niceWand) - _make_mons_stop_fleeing(monster); - - if (!simple_monster_message(monster, " zaps a wand.")) - { - if (!silenced(you.pos())) - mpr("You hear a zap.", MSGCH_SOUND); - } - - // charge expenditure {dlb} - wand.plus--; - beem.is_tracer = false; - beem.fire(); - - if (was_visible) - { - if (niceWand || !beem.is_enchantment() || beem.obvious_effect) - set_ident_type(OBJ_WANDS, wand_type, ID_KNOWN_TYPE); - else - set_ident_type(OBJ_WANDS, wand_type, ID_MON_TRIED_TYPE); - - // Increment zap count. - if (wand.plus2 >= 0) - wand.plus2++; - } - - monster->lose_energy(EUT_ITEM); - - return (true); - } - - return (false); -} - -// 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 (monster->asleep() - || monster->submerged() - || !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); - - // 1KB: the following code is never used for unfriendlies! - 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 (!you.visible_to(monster) - && (monster->target != you.pos() || coinflip())) - { - spellOK = false; - } - } - else if (!monster->can_see(&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 - && feat_is_watery(grd(monster->pos()))) - - || (monster->type == MONS_AIR_ELEMENTAL - && env.cgrid(monster->pos()) == EMPTY_CLOUD - && one_chance_in(3)) - - || _mons_natural_regen_roll(monster)) - { - heal_monster(monster, 1, false); - } -} - -static bool _swap_monsters(monsters* mover, monsters* moved) -{ - // Can't swap with a stationary monster. - if (mons_is_stationary(moved)) - return (false); - - // Swapping is a purposeful action. - if (mover->confused()) - return (false); - - // Right now just happens in sanctuary. - if (!is_sanctuary(mover->pos()) || !is_sanctuary(moved->pos())) - return (false); - - // A friendly or good-neutral monster moving past a fleeing hostile - // or neutral monster, or vice versa. - if (mons_wont_attack_real(mover) == mons_wont_attack_real(moved) - || mons_is_fleeing(mover) == mons_is_fleeing(moved)) - { - return (false); - } - - // Don't swap places if the player explicitly ordered their pet to - // attack monsters. - if ((mons_friendly(mover) || mons_friendly(moved)) - && you.pet_target != MHITYOU && you.pet_target != MHITNOT) - { - return (false); - } - - if (!mover->can_pass_through(moved->pos()) - || !moved->can_pass_through(mover->pos())) - { - return (false); - } - - if (!monster_habitable_grid(mover, grd(moved->pos())) - || !monster_habitable_grid(moved, grd(mover->pos()))) - { - return (false); - } - - // Okay, we can do the swap. - const coord_def mover_pos = mover->pos(); - const coord_def moved_pos = moved->pos(); - - mover->pos() = moved_pos; - moved->pos() = mover_pos; - - mgrd(mover->pos()) = mover->mindex(); - mgrd(moved->pos()) = moved->mindex(); - - if (you.can_see(mover) && you.can_see(moved)) - { - mprf("%s and %s swap places.", mover->name(DESC_CAP_THE).c_str(), - moved->name(DESC_NOCAP_THE).c_str()); - } - - return (true); -} - -static 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 ); -} - -static void _khufu_drop_tomb(monsters *monster) -{ - int count = 0; - - monster->behaviour = BEH_SEEK; // don't wander on duty! - for (adjacent_iterator ai(monster->pos()); ai; ++ai) - { - if (grd(*ai) == DNGN_ROCK_WALL) - { - grd(*ai) = DNGN_FLOOR; - count++; - } - } - if (count) - if (mons_near(monster)) - mpr("The walls disappear!"); - else - mpr("You hear a deep rumble."); - monster->number = 0; - monster->lose_energy(EUT_SPELL); -} - -#ifdef DEBUG -# define DEBUG_ENERGY_USE(problem) \ - if (monster->speed_increment == old_energy && monster->alive()) \ - mprf(MSGCH_DIAGNOSTICS, \ - problem " for monster '%s' consumed no energy", \ - monster->name(DESC_PLAIN).c_str(), true); -#else -# define DEBUG_ENERGY_USE(problem) ((void) 0) -#endif - -static void _handle_monster_move(monsters *monster) -{ - monster->hit_points = std::min(monster->max_hit_points, - monster->hit_points); - - // Monster just summoned (or just took stairs), skip this action. - if (testbits( monster->flags, MF_JUST_SUMMONED )) - { - monster->flags &= ~MF_JUST_SUMMONED; - return; - } - - mon_acting mact(monster); - - _monster_add_energy(monster); - - // Handle clouds on nonmoving monsters. - if (monster->speed == 0 - && env.cgrid(monster->pos()) != EMPTY_CLOUD - && !monster->submerged()) - { - _mons_in_cloud( monster ); - } - - // Apply monster enchantments once for every normal-speed - // player turn. - monster->ench_countdown -= you.time_taken; - while (monster->ench_countdown < 0) - { - monster->ench_countdown += 10; - monster->apply_enchantments(); - - // If the monster *merely* died just break from the loop - // rather than quit altogether, since we have to deal with - // giant spores and ball lightning exploding at the end of the - // function, but do return if the monster's data has been - // reset, since then the monster type is invalid. - if (monster->type == MONS_NO_MONSTER) - return; - else if (monster->hit_points < 1) - break; - } - - // Memory is decremented here for a reason -- we only want it - // decrementing once per monster "move". - if (monster->foe_memory > 0) - monster->foe_memory--; - - // Otherwise there are potential problems with summonings. - if (monster->type == MONS_GLOWING_SHAPESHIFTER) - monster->add_ench(ENCH_GLOWING_SHAPESHIFTER); - - if (monster->type == MONS_SHAPESHIFTER) - monster->add_ench(ENCH_SHAPESHIFTER); - - // We reset batty monsters from wander to seek here, instead - // of in handle_behaviour() since that will be called with - // every single movement, and we want these monsters to - // hit and run. -- bwr - if (monster->foe != MHITNOT && mons_is_wandering(monster) - && mons_is_batty(monster)) - { - monster->behaviour = BEH_SEEK; - } - - monster->check_speed(); - - monsterentry* entry = get_monster_data(monster->type); - if (!entry) - return; - - int old_energy = INT_MAX; - int non_move_energy = std::min(entry->energy_usage.move, - entry->energy_usage.swim); - -#if DEBUG_MONS_SCAN - bool monster_was_floating = mgrd(monster->pos()) != monster->mindex(); -#endif - - while (monster->has_action_energy()) - { - // The continues & breaks are WRT this. - if (!monster->alive()) - break; - - const coord_def old_pos = monster->pos(); - -#if DEBUG_MONS_SCAN - if (!monster_was_floating - && mgrd(monster->pos()) != monster->mindex()) - { - mprf(MSGCH_ERROR, "Monster %s became detached from mgrd " - "in _handle_monster_move() loop", - monster->name(DESC_PLAIN, true).c_str()); - mpr("[[[[[[[[[[[[[[[[[[", MSGCH_WARN); - debug_mons_scan(); - mpr("]]]]]]]]]]]]]]]]]]", MSGCH_WARN); - monster_was_floating = true; - } - else if (monster_was_floating - && mgrd(monster->pos()) == monster->mindex()) - { - mprf(MSGCH_DIAGNOSTICS, "Monster %s re-attached itself to mgrd " - "in _handle_monster_move() loop", - monster->name(DESC_PLAIN, true).c_str()); - monster_was_floating = false; - } -#endif - - if (monster->speed_increment >= old_energy) - { -#ifdef DEBUG - if (monster->speed_increment == old_energy) - { - mprf(MSGCH_DIAGNOSTICS, "'%s' has same energy as last loop", - monster->name(DESC_PLAIN, true).c_str()); - } - else - { - mprf(MSGCH_DIAGNOSTICS, "'%s' has MORE energy than last loop", - monster->name(DESC_PLAIN, true).c_str()); - } -#endif - monster->speed_increment = old_energy - 10; - old_energy = monster->speed_increment; - continue; - } - old_energy = monster->speed_increment; - - monster->shield_blocks = 0; - - cloud_type cl_type; - const int cloud_num = env.cgrid(monster->pos()); - const bool avoid_cloud = mons_avoids_cloud(monster, cloud_num, - &cl_type); - if (cl_type != CLOUD_NONE) - { - if (avoid_cloud) - { - if (monster->submerged()) - { - monster->speed_increment -= entry->energy_usage.swim; - break; - } - - if (monster->type == MONS_NO_MONSTER) - { - monster->speed_increment -= entry->energy_usage.move; - break; // problem with vortices - } - } - - _mons_in_cloud(monster); - - if (monster->type == MONS_NO_MONSTER) - { - monster->speed_increment = 1; - break; - } - } - - if (monster->type == MONS_TIAMAT && one_chance_in(3)) - { - const int cols[] = { RED, WHITE, DARKGREY, GREEN, MAGENTA }; - monster->colour = RANDOM_ELEMENT(cols); - } - - _monster_regenerate(monster); - - if (mons_cannot_act(monster)) - { - monster->speed_increment -= non_move_energy; - continue; - } - - _handle_behaviour(monster); - - // _handle_behaviour() could make the monster leave the level. - if (!monster->alive()) - break; - - ASSERT(!crawl_state.arena || monster->foe != MHITYOU); - ASSERT(in_bounds(monster->target) || monster->target.origin()); - - // Submerging monsters will hide from clouds. - if (avoid_cloud - && monster_can_submerge(monster, grd(monster->pos())) - && !monster->caught() - && !monster->submerged()) - { - monster->add_ench(ENCH_SUBMERGED); - monster->speed_increment -= ENERGY_SUBMERGE(entry); - continue; - } - - if (monster->speed >= 100) - { - monster->speed_increment -= non_move_energy; - continue; - } - - if (igrd(monster->pos()) != NON_ITEM - && (mons_itemuse(monster) >= MONUSE_WEAPONS_ARMOUR - || mons_itemeat(monster) != MONEAT_NOTHING)) - { - // Keep neutral and charmed monsters from picking up stuff. - // Same for friendlies if friendly_pickup is set to "none". - if (!mons_neutral(monster) && !monster->has_ench(ENCH_CHARM) - || (you.religion == GOD_JIYVA && mons_is_slime(monster)) - && (!mons_friendly(monster) - || you.friendly_pickup != FRIENDLY_PICKUP_NONE)) - { - if (_handle_pickup(monster)) - { - DEBUG_ENERGY_USE("handle_pickup()"); - continue; - } - } - } - - // Lurking monsters only stop lurking if their target is right - // next to them, otherwise they just sit there. - // However, if the monster is involuntarily submerged but - // still alive (e.g., nonbreathing which had water poured - // on top of it), this doesn't apply. - if (mons_is_lurking(monster) || monster->has_ench(ENCH_SUBMERGED)) - { - if (monster->foe != MHITNOT - && grid_distance(monster->target, monster->pos()) <= 1) - { - if (monster->submerged()) - { - // Don't unsubmerge if the monster is too damaged or - // if the monster is afraid, or if it's avoiding the - // cloud on top of the water. - if (monster->hit_points <= monster->max_hit_points / 2 - || monster->has_ench(ENCH_FEAR) - || avoid_cloud) - { - monster->speed_increment -= non_move_energy; - continue; - } - - if (!monster->del_ench(ENCH_SUBMERGED)) - { - // Couldn't unsubmerge. - monster->speed_increment -= non_move_energy; - continue; - } - } - monster->behaviour = BEH_SEEK; - } - else - { - monster->speed_increment -= non_move_energy; - continue; - } - } - - if (mons_is_caught(monster)) - { - // Struggling against the net takes time. - _swim_or_move_energy(monster); - } - else if (!mons_is_petrified(monster)) - { - // Calculates mmov based on monster target. - _handle_movement(monster); - - if (mons_is_confused(monster) - || monster->type == MONS_AIR_ELEMENTAL - && monster->submerged()) - { - mmov.reset(); - int pfound = 0; - for (adjacent_iterator ai(monster->pos(), false); ai; ++ai) - if (monster->can_pass_through(*ai)) - if (one_chance_in(++pfound)) - mmov = *ai - monster->pos(); - - // OK, mmov determined. - const coord_def newcell = mmov + monster->pos(); - monsters* enemy = monster_at(newcell); - if (enemy - && newcell != monster->pos() - && !is_sanctuary(monster->pos())) - { - if (monsters_fight(monster, enemy)) - { - mmov.reset(); - DEBUG_ENERGY_USE("monsters_fight()"); - continue; - } - else - { - // FIXME: None of these work! - // Instead run away! - if (monster->add_ench(mon_enchant(ENCH_FEAR))) - { - behaviour_event(monster, ME_SCARE, - MHITNOT, newcell); - } - break; - } - } - } - } - _handle_nearby_ability(monster); - - if (monster->type == MONS_KHUFU && monster->number - && monster->hit_points==monster->max_hit_points) - _khufu_drop_tomb(monster); - - if (!monster->asleep() && !mons_is_wandering(monster) - // Berserking monsters are limited to running up and - // hitting their foes. - && !monster->has_ench(ENCH_BERSERK) - // Slime creatures can split while wandering or resting. - || monster->type == MONS_SLIME_CREATURE) - { - bolt beem; - - beem.source = monster->pos(); - beem.target = monster->target; - beem.beam_source = monster->mindex(); - - // Prevents unfriendlies from nuking you from offscreen. - // How nice! - const bool friendly_or_near = - mons_friendly(monster) || monster->near_foe(); - if (friendly_or_near - || monster->type == MONS_TEST_SPAWNER - // Slime creatures can split when offscreen. - || monster->type == MONS_SLIME_CREATURE) - { - // [ds] Special abilities shouldn't overwhelm - // spellcasting in monsters that have both. This aims - // to give them both roughly the same weight. - if (coinflip() ? _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)) - { - if (monster->pos() + mmov == you.pos()) - { - ASSERT(!crawl_state.arena); - - if (!mons_friendly(monster)) - { - // If it steps into you, cancel other targets. - monster->foe = MHITYOU; - monster->target = you.pos(); - - monster_attack(monster); - - if (mons_is_batty(monster)) - { - monster->behaviour = BEH_WANDER; - _set_random_target(monster); - } - DEBUG_ENERGY_USE("monster_attack()"); - mmov.reset(); - continue; - } - } - - // See if we move into (and fight) an unfriendly monster. - monsters* targ = monster_at(monster->pos() + mmov); - if (targ - && targ != monster - && !mons_aligned(monster->mindex(), targ->mindex()) - && monster_can_hit_monster(monster, targ)) - { - // Maybe they can swap places? - if (_swap_monsters(monster, targ)) - { - _swim_or_move_energy(monster); - continue; - } - // Figure out if they fight. - else if (monsters_fight(monster, targ)) - { - if (mons_is_batty(monster)) - { - monster->behaviour = BEH_WANDER; - _set_random_target(monster); - // monster->speed_increment -= monster->speed; - } - - mmov.reset(); - DEBUG_ENERGY_USE("monsters_fight()"); - continue; - } - } - - if (invalid_monster(monster) || mons_is_stationary(monster)) - { - if (monster->speed_increment == old_energy) - monster->speed_increment -= non_move_energy; - continue; - } - - if (mons_cannot_move(monster) || !_monster_move(monster)) - monster->speed_increment -= non_move_energy; - } - update_beholders(monster); - - // Reevaluate behaviour, since the monster's surroundings have - // changed (it may have moved, or died for that matter). Don't - // bother for dead monsters. :) - if (monster->alive()) - { - _handle_behaviour(monster); - ASSERT(in_bounds(monster->target) || monster->target.origin()); - } - } - - if (monster->type != MONS_NO_MONSTER && monster->hit_points < 1) - monster_die(monster, KILL_MISC, NON_MONSTER); -} - -//--------------------------------------------------------------- -// -// handle_monsters -// -// This is the routine that controls monster AI. -// -//--------------------------------------------------------------- -void handle_monsters() -{ - // Keep track of monsters that have already moved and don't allow - // them to move again. - memset(immobile_monster, 0, sizeof immobile_monster); - - for (int i = 0; i < MAX_MONSTERS; ++i) - { - monsters *monster = &menv[i]; - - if (!monster->alive() || immobile_monster[i]) - continue; - - const coord_def oldpos = monster->pos(); - - _handle_monster_move(monster); - - if (!invalid_monster(monster) && monster->pos() != oldpos) - immobile_monster[i] = true; - - // If the player got banished, discard pending monster actions. - if (you.banished) - { - // Clear list of mesmerising monsters. - if (you.duration[DUR_MESMERISED]) - { - you.mesmerised_by.clear(); - you.duration[DUR_MESMERISED] = 0; - } - break; - } - } - - // Clear any summoning flags so that lower indiced - // monsters get their actions in the next round. - for (int i = 0; i < MAX_MONSTERS; i++) - menv[i].flags &= ~MF_JUST_SUMMONED; -} - -static bool _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); -} - -// XXX: This function assumes that only jellies eat items. -static bool _monster_eat_item(monsters *monster, bool nearby) -{ - if (!mons_eats_items(monster)) - return (false); - - // Friendly jellies won't eat (unless worshipping Jiyva). - if (mons_friendly(monster) && you.religion != GOD_JIYVA) - return (false); - - int hps_gained = 0; - int max_eat = roll_dice(1, 10); - int eaten = 0; - bool eaten_net = false; - - for (stack_iterator si(monster->pos()); - si && eaten < max_eat && hps_gained < 50; ++si) - { - if (!_is_item_jelly_edible(*si)) - continue; - -#if DEBUG_DIAGNOSTICS || DEBUG_EATERS - mprf(MSGCH_DIAGNOSTICS, - "%s eating %s", monster->name(DESC_PLAIN, true).c_str(), - si->name(DESC_PLAIN).c_str()); -#endif - - int quant = si->quantity; - - if (si->base_type != OBJ_GOLD) - { - quant = std::min(quant, max_eat - eaten); - - hps_gained += (quant * item_mass(*si)) / 20 + quant; - eaten += quant; - - if (mons_is_caught(monster) - && si->base_type == OBJ_MISSILES - && si->sub_type == MI_THROWING_NET - && item_is_stationary(*si)) - { - monster->del_ench(ENCH_HELD, true); - eaten_net = true; - } - } - else - { - // Shouldn't be much trouble to digest a huge pile of gold! - if (quant > 500) - quant = 500 + roll_dice(2, (quant - 500) / 2); - - hps_gained += quant / 10 + 1; - eaten++; - } - - if (you.religion == GOD_JIYVA) - { - const int quantity = si->quantity; - const int value = item_value(*si) / quantity; - int pg = 0; - int timeout = 0; - - for (int m = 0; m < quantity; ++m) - { - if (x_chance_in_y(value / 2 + 1, 30 + you.piety / 4)) - { - if (timeout <= 0) - pg += random2(item_value(*si) / 6); - else - timeout -= value / 5; - } - } - - if (pg > 0) - { - simple_god_message(" appreciates your sacrifice."); - gain_piety(pg); - } - - if (you.piety > 80 && random2(you.piety) > 50 && one_chance_in(4)) - { - if (you.can_safely_mutate()) - { - simple_god_message(" alters your body."); - - bool success = false; - const int rand = random2(100); - - if (rand < 40) - success = mutate(RANDOM_MUTATION, true, false, true); - else if (rand < 60) - { - success = delete_mutation(RANDOM_MUTATION, true, false, - true); - } - else - { - success = mutate(RANDOM_GOOD_MUTATION, true, false, - true); - } - - if (success) - { - timeout = (100 + roll_dice(2, 4)); - you.num_gifts[you.religion]++; - take_note(Note(NOTE_GOD_GIFT, you.religion)); - } - else - mpr("You feel as though nothing has changed."); - } - } - } - - if (quant >= si->quantity) - item_was_destroyed(*si, monster->mindex()); - - dec_mitm_item_quantity(si.link(), quant); - } - - if (eaten > 0) - { - hps_gained = std::max(hps_gained, 1); - hps_gained = std::min(hps_gained, 50); - - // This is done manually instead of using heal_monster(), - // because that function doesn't work quite this way. -- bwr - monster->hit_points += hps_gained; - monster->max_hit_points = std::max(monster->hit_points, - monster->max_hit_points); - - if (player_can_hear(monster->pos())) - { - mprf(MSGCH_SOUND, "You hear a%s slurping noise.", - nearby ? "" : " distant"); - } - - if (eaten_net) - simple_monster_message(monster, " devours the net!"); - - _jelly_divide(monster); - } - - return (eaten > 0); -} - -static bool _monster_eat_single_corpse(monsters *monster, item_def& item, - bool do_heal, bool nearby) -{ - if (item.base_type != OBJ_CORPSES || item.sub_type != CORPSE_BODY) - return (false); - - monster_type mt = static_cast(item.plus); - if (do_heal) - { - monster->hit_points += 1 + random2(mons_weight(mt)) / 100; - - // Limited growth factor here - should 77 really be the cap? {dlb}: - monster->hit_points = std::min(100, monster->hit_points); - monster->max_hit_points = std::max(monster->hit_points, - monster->max_hit_points); - } - - if (nearby) - { - mprf("%s eats %s.", monster->name(DESC_CAP_THE).c_str(), - item.name(DESC_NOCAP_THE).c_str()); - } - - // Assume that eating a corpse requires butchering it. Use logic - // from misc.cc:turn_corpse_into_chunks() and the butchery-related - // delays in delay.cc:stop_delay(). - - const int max_chunks = mons_weight(mt) / 150; - - // Only fresh corpses bleed enough to colour the ground. - if (!food_is_rotten(item)) - bleed_onto_floor(monster->pos(), mt, max_chunks, true); - - if (mons_skeleton(mt) && one_chance_in(3)) - turn_corpse_into_skeleton(item); - else - destroy_item(item.index()); - - return (true); -} - -static bool _monster_eat_corpse(monsters *monster, bool do_heal, bool nearby) -{ - if (!mons_eats_corpses(monster)) - return (false); - - int eaten = 0; - - for (stack_iterator si(monster->pos()); si; ++si) - { - if (_monster_eat_single_corpse(monster, *si, do_heal, nearby)) - { - eaten++; - break; - } - } - - return (eaten > 0); -} - -static bool _monster_eat_food(monsters *monster, bool nearby) -{ - if (!mons_eats_food(monster)) - return (false); - - if (mons_is_fleeing(monster)) - return (false); - - int eaten = 0; - - for (stack_iterator si(monster->pos()); si; ++si) - { - const bool is_food = (si->base_type == OBJ_FOOD); - const bool is_corpse = (si->base_type == OBJ_CORPSES - && si->sub_type == CORPSE_BODY); - - if (!is_food && !is_corpse) - continue; - - if ((mons_wont_attack(monster) - || grid_distance(monster->pos(), you.pos()) > 1) - && coinflip()) - { - if (is_food) - { - if (nearby) - { - mprf("%s eats %s.", monster->name(DESC_CAP_THE).c_str(), - quant_name(*si, 1, DESC_NOCAP_THE).c_str()); - } - - dec_mitm_item_quantity(si.link(), 1); - - eaten++; - break; - } - else - { - // Assume that only undead can heal from eating corpses. - if (_monster_eat_single_corpse(monster, *si, - monster->holiness() == MH_UNDEAD, - nearby)) - { - eaten++; - break; - } - } - } - } - - return (eaten > 0); -} - -//--------------------------------------------------------------- -// -// handle_pickup -// -// Returns false if monster doesn't spend any time picking something up. -// -//--------------------------------------------------------------- -static bool _handle_pickup(monsters *monster) -{ - if (monster->asleep() || monster->submerged()) - return (false); - - const bool nearby = mons_near(monster); - int count_pickup = 0; - - if (mons_itemeat(monster) != MONEAT_NOTHING) - { - if (mons_eats_items(monster)) - { - if (_monster_eat_item(monster, nearby)) - return (false); - } - else if (mons_eats_corpses(monster)) - { - // Assume that only undead can heal from eating corpses. - if (_monster_eat_corpse(monster, monster->holiness() == MH_UNDEAD, - nearby)) - { - return (false); - } - } - else if (mons_eats_food(monster)) - { - if (_monster_eat_food(monster, nearby)) - return (false); - } - } - - if (mons_itemuse(monster) >= MONUSE_WEAPONS_ARMOUR) - { - // Note: Monsters only look at stuff near the top of stacks. - // - // XXX: Need to put in something so that monster picks up - // multiple items (e.g. ammunition) identical to those it's - // carrying. - // - // Monsters may now pick up up to two items in the same turn. - // (jpeg) - for (stack_iterator si(monster->pos()); si; ++si) - { - if (monster->pickup_item(*si, nearby)) - count_pickup++; - - if (count_pickup > 1 || coinflip()) - break; - } - } - - return (count_pickup > 0); -} - -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) - || mpusher->asleep()) - { - return (false); - } - - // Batty monsters are unpushable. - if (mons_is_batty(mpusher) || mons_is_batty(mpushee)) - return (false); - - if (!monster_shover(mpusher)) - return (false); - - // Fleeing monsters of the same type may push past higher ranking ones. - if (!monster_senior(mpusher, mpushee, mons_is_fleeing(mpusher))) - return (false); - - return (true); -} - -static bool _monster_swaps_places( monsters *mon, const coord_def& delta ) -{ - if (delta.origin()) - return (false); - - monsters* const m2 = monster_at(mon->pos() + delta); - - if (!m2) - return (false); - - if (!_mons_can_displace(mon, m2)) - return (false); - - if (m2->asleep()) - { - if (coinflip()) - { -#ifdef DEBUG_DIAGNOSTICS - mprf(MSGCH_DIAGNOSTICS, - "Alerting monster %s at (%d,%d)", - m2->name(DESC_PLAIN).c_str(), m2->pos().x, m2->pos().y); -#endif - behaviour_event(m2, ME_ALERT, MHITNOT); - } - return (false); - } - - // Check that both monsters will be happy at their proposed new locations. - const coord_def c = mon->pos(); - const coord_def n = mon->pos() + delta; - - if (!_habitat_okay(mon, grd(n)) || !_habitat_okay(m2, grd(c))) - return (false); - - // Okay, do the swap! - _swim_or_move_energy(mon); - - mon->pos() = n; - mgrd(n) = monster_index(mon); - m2->pos() = c; - const int m2i = monster_index(m2); - ASSERT(m2i >= 0 && m2i < MAX_MONSTERS); - mgrd(c) = m2i; - immobile_monster[m2i] = true; - - mon->check_redraw(c); - mon->apply_location_effects(c); - m2->check_redraw(c); - m2->apply_location_effects(n); - - // The seen context no longer applies if the monster is moving normally. - mon->seen_context.clear(); - m2->seen_context.clear(); - - return (false); -} - -static bool _do_move_monster(monsters *monster, const coord_def& delta) -{ - const coord_def f = monster->pos() + delta; - - if (!in_bounds(f)) - return (false); - - if (f == you.pos()) - { - monster_attack(monster); - return (true); - } - - // This includes the case where the monster attacks itself. - if (monsters* def = monster_at(f)) - { - monsters_fight(monster, def); - return (true); - } - - // The monster gave a "comes into view" message and then immediately - // moved back out of view, leaing the player nothing to see, so give - // this message to avoid confusion. - if (monster->seen_context == _just_seen && !see_cell(f)) - simple_monster_message(monster, " moves out of view."); - else if (Options.tutorial_left && (monster->flags & MF_WAS_IN_VIEW) - && !see_cell(f)) - { - learned_something_new(TUT_MONSTER_LEFT_LOS, monster->pos()); - } - - // The seen context no longer applies if the monster is moving normally. - monster->seen_context.clear(); - - // This appears to be the real one, ie where the movement occurs: - _swim_or_move_energy(monster); - - if (grd(monster->pos()) == DNGN_DEEP_WATER && grd(f) != DNGN_DEEP_WATER - && !monster_habitable_grid(monster, DNGN_DEEP_WATER)) - { - monster->seen_context = "emerges from the water"; - } - mgrd(monster->pos()) = NON_MONSTER; - - monster->pos() = f; - - mgrd(monster->pos()) = monster_index(monster); - - monster->check_redraw(monster->pos() - delta); - monster->apply_location_effects(monster->pos() - delta); - - return (true); -} - -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 && monster->res_fire() >= 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); - } - } -} + if (placement) + extra_careful = true; -// Randomise potential damage. -static int _estimated_trap_damage(trap_type trap) -{ - switch (trap) + switch (cl_type) { - 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); + case CLOUD_MIASMA: + // Even the dumbest monsters will avoid miasma if they can. + return (!monster->res_rotting()); - // Dumb monsters don't care at all. - if (intel == I_PLANT) - return (true); + case CLOUD_FIRE: + case CLOUD_FOREST_FIRE: + if (monster->res_fire() > 1) + return (false); - if (trap.type == TRAP_SHAFT && monster->will_trigger_shaft()) - { - if (mons_is_fleeing(monster) && intel >= I_NORMAL - || mons_is_pacified(monster)) - { + if (extra_careful) return (true); - } - return (false); - } - - // Hostile monsters are not afraid of non-mechanical traps. - // Allies will try to avoid teleportation and zot traps. - const bool mechanical = (trap.category() == DNGN_TRAP_MECHANICAL); - - if (trap.is_known(monster)) - { - if (just_check) - return (false); // Square is blocked. - else - { - // Test for corridor-like environment. - const int x = where.x - monster->pos().x; - const int y = where.y - monster->pos().y; - - // The question is whether the monster (m) can easily reach its - // presumable destination (x) without stepping on the trap. Traps - // in corridors do not allow this. See e.g - // #x# ## - // #^# or m^x - // m ## - // - // The same problem occurs if paths are blocked by monsters, - // hostile terrain or other traps rather than walls. - // What we do is check whether the squares with the relative - // positions (-1,0)/(+1,0) or (0,-1)/(0,+1) form a "corridor" - // (relative to the _trap_ position rather than the monster one). - // If they don't, the trap square is marked as "unsafe" (because - // there's a good alternative move for the monster to take), - // otherwise the decision will be made according to later tests - // (monster hp, trap type, ...) - // If a monster still gets stuck in a corridor it will usually be - // because it has less than half its maximum hp. - - if ((_mon_can_move_to_pos(monster, coord_def(x-1, y), true) - || _mon_can_move_to_pos(monster, coord_def(x+1,y), true)) - && (_mon_can_move_to_pos(monster, coord_def(x,y-1), true) - || _mon_can_move_to_pos(monster, coord_def(x,y+1), true))) - { - return (false); - } - } - } - - // Friendlies will try not to be parted from you. - if (intelligent_ally(monster) && trap.type == TRAP_TELEPORT - && player_knows_trap && mons_near(monster)) - { - return (false); - } - - // Healthy monsters don't mind a little pain. - if (mechanical && monster->hit_points >= monster->max_hit_points / 2 - && (intel == I_ANIMAL - || monster->hit_points > _estimated_trap_damage(trap.type))) - { - return (true); - } - - // Friendly and good neutral monsters don't enjoy Zot trap perks; - // handle accordingly. In the arena Zot traps affect all monsters. - if (mons_wont_attack(monster) || crawl_state.arena) - { - return (mechanical ? mons_flies(monster) - : !trap.is_known(monster) || trap.type != TRAP_ZOT); - } - else - return (!mechanical || mons_flies(monster)); -} - -static void _mons_open_door(monsters* monster, const coord_def &pos) -{ - dungeon_feature_type grid = grd(pos); - const char *adj = "", *noun = "door"; - - bool was_secret = false; - bool was_seen = false; - - std::set all_door; - find_connected_range(pos, DNGN_CLOSED_DOOR, DNGN_SECRET_DOOR, all_door); - get_door_description(all_door.size(), &adj, &noun); - - for (std::set::iterator i = all_door.begin(); - i != all_door.end(); ++i) - { - const coord_def& dc = *i; - if (grd(dc) == DNGN_SECRET_DOOR && see_cell(dc)) - { - grid = grid_secret_door_appearance(dc); - was_secret = true; - } - - if (see_cell(dc)) - was_seen = true; - else - set_terrain_changed(dc); - - grd[dc.x][dc.y] = DNGN_OPEN_DOOR; - } - - if (was_seen) - { - viewwindow(true, false); - - if (was_secret) - { - mprf("%s was actually a secret door!", - feature_description(grid, NUM_TRAPS, false, - DESC_CAP_THE, false).c_str()); - learned_something_new(TUT_SEEN_SECRET_DOOR, pos); - } - - std::string open_str = "opens the "; - open_str += adj; - open_str += noun; - open_str += "."; - - monster->seen_context = open_str; - - if (!you.can_see(monster)) - { - mprf("Something unseen %s", open_str.c_str()); - interrupt_activity(AI_FORCE_INTERRUPT); - } - else if (!you_are_delayed()) - { - mprf("%s %s", monster->name(DESC_CAP_A).c_str(), - open_str.c_str()); - } - } - monster->lose_energy(EUT_MOVE); -} + if (mons_intel(monster) >= I_ANIMAL && monster->res_fire() < 0) + return (true); -static bool _no_habitable_adjacent_grids(const monsters *mon) -{ - for (adjacent_iterator ai(mon->pos()); ai; ++ai) - if (_habitat_okay(mon, grd(*ai))) + if (monster->hit_points >= 15 + random2avg(46, 5)) return (false); + break; - return (true); -} - -// Check whether a monster can move to given square (described by its relative -// coordinates to the current monster position). just_check is true only for -// calls from is_trap_safe when checking the surrounding squares of a trap. -static bool _mon_can_move_to_pos(const monsters *monster, - const coord_def& delta, bool just_check) -{ - const coord_def targ = monster->pos() + delta; - - // Bounds check: don't consider moving out of grid! - if (!in_bounds(targ)) - return (false); - - // No monster may enter the open sea. - if (grd(targ) == DNGN_OPEN_SEA) - return (false); - - // Non-friendly and non-good neutral monsters won't enter - // sanctuaries. - if (!mons_wont_attack(monster) - && is_sanctuary(targ) - && !is_sanctuary(monster->pos())) - { - return (false); - } - - // Inside a sanctuary don't attack anything! - if (is_sanctuary(monster->pos()) && actor_at(targ)) - return (false); - - const dungeon_feature_type target_grid = grd(targ); - const habitat_type habitat = mons_primary_habitat(monster); - - // The kraken is so large it cannot enter shallow water. - // Its tentacles can, and will, though. - if (monster->type == MONS_KRAKEN && target_grid == DNGN_SHALLOW_WATER) - return (false); - - // Effectively slows down monster movement across water. - // Fire elementals can't cross at all. - bool no_water = false; - if (monster->type == MONS_FIRE_ELEMENTAL || one_chance_in(5)) - no_water = true; - - cloud_type targ_cloud_type; - const int targ_cloud_num = env.cgrid(targ); - - if (mons_avoids_cloud(monster, targ_cloud_num, &targ_cloud_type)) - return (false); - - if (mons_class_flag(monster->type, M_BURROWS) - && (target_grid == DNGN_ROCK_WALL - || target_grid == DNGN_CLEAR_ROCK_WALL)) - { - // Don't burrow out of bounds. - if (!in_bounds(targ)) + case CLOUD_STINK: + if (monster->res_poison() > 0) return (false); - // Don't burrow at an angle (legacy behaviour). - if (delta.x != 0 && delta.y != 0) - return (false); - } - else if (!monster->can_pass_through_feat(target_grid) - || no_water && feat_is_water(target_grid)) - { - return (false); - } - else if (!_habitat_okay(monster, target_grid)) - { - // If the monster somehow ended up in this habitat (and is - // not dead by now), give it a chance to get out again. - if (grd(monster->pos()) == target_grid - && _no_habitable_adjacent_grids(monster)) - { + if (extra_careful) return (true); - } - - return (false); - } - - // Wandering mushrooms don't move while you are looking. - if (monster->type == MONS_WANDERING_MUSHROOM && see_cell(targ)) - return (false); - - // Water elementals avoid fire and heat. - if (monster->type == MONS_WATER_ELEMENTAL - && (target_grid == DNGN_LAVA - || targ_cloud_type == CLOUD_FIRE - || targ_cloud_type == CLOUD_FOREST_FIRE - || targ_cloud_type == CLOUD_STEAM)) - { - return (false); - } - - // Fire elementals avoid water and cold. - if (monster->type == MONS_FIRE_ELEMENTAL - && (feat_is_watery(target_grid) - || targ_cloud_type == CLOUD_COLD)) - { - return (false); - } - - // Submerged water creatures avoid the shallows where - // they would be forced to surface. -- bwr - // [dshaligram] Monsters now prefer to head for deep water only if - // they're low on hitpoints. No point in hiding if they want a - // fight. - if (habitat == HT_WATER - && targ != you.pos() - && target_grid != DNGN_DEEP_WATER - && grd(monster->pos()) == DNGN_DEEP_WATER - && monster->hit_points < (monster->max_hit_points * 3) / 4) - { - return (false); - } - - // Smacking the player is always a good move if we're - // hostile (even if we're heading somewhere else). - // Also friendlies want to keep close to the player - // so it's okay as well. - - // Smacking another monster is good, if the monsters - // are aligned differently. - if (monsters *targmonster = monster_at(targ)) - { - if (just_check) - { - if (targ == monster->pos()) - return (true); - return (false); // blocks square - } + if (mons_intel(monster) >= I_ANIMAL && monster->res_poison() < 0) + return (true); - if (mons_aligned(monster->mindex(), targmonster->mindex()) - && !_mons_can_displace(monster, targmonster)) - { + if (x_chance_in_y(monster->hit_dice - 1, 5)) return (false); - } - } - - // Friendlies shouldn't try to move onto the player's - // location, if they are aiming for some other target. - if (mons_wont_attack(monster) - && monster->foe != MHITYOU - && (monster->foe != MHITNOT || monster->is_patrolling()) - && targ == you.pos()) - { - return (false); - } - - // Wandering through a trap is OK if we're pretty healthy, - // really stupid, or immune to the trap. - if (!_is_trap_safe(monster, targ, just_check)) - return (false); - - // If we end up here the monster can safely move. - return (true); -} - -// Uses, and updates the global variable mmov. -static void _find_good_alternate_move(monsters *monster, - const FixedArray& good_move) -{ - const int current_distance = distance(monster->pos(), monster->target); - - int dir = -1; - for (int i = 0; i < 8; i++) - { - if (mon_compass[i] == mmov) - { - dir = i; - break; - } - } - - // Only handle if the original move is to an adjacent square. - if (dir == -1) - return; - - int dist[2]; - - // First 1 away, then 2 (3 is silly). - for (int j = 1; j <= 2; j++) - { - const int FAR_AWAY = 1000000; - - // Try both directions (but randomise which one is first). - const int sdir = coinflip() ? j : -j; - const int inc = -2 * sdir; - - for (int mod = sdir, i = 0; i < 2; mod += inc, i++) - { - const int newdir = (dir + 8 + mod) % 8; - if (good_move[mon_compass[newdir].x+1][mon_compass[newdir].y+1]) - { - dist[i] = distance(monster->pos()+mon_compass[newdir], - monster->target); - } - else - { - dist[i] = (mons_is_fleeing(monster)) ? (-FAR_AWAY) : FAR_AWAY; - } - } - const int dir0 = ((dir + 8 + sdir) % 8); - const int dir1 = ((dir + 8 - sdir) % 8); - - // Now choose. - if (dist[0] == dist[1] && abs(dist[0]) == FAR_AWAY) - continue; - - // Which one was better? -- depends on FLEEING or not. - if (mons_is_fleeing(monster)) - { - if (dist[0] >= dist[1] && dist[0] >= current_distance) - { - mmov = mon_compass[dir0]; - break; - } - if (dist[1] >= dist[0] && dist[1] >= current_distance) - { - mmov = mon_compass[dir1]; - break; - } - } - else - { - if (dist[0] <= dist[1] && dist[0] <= current_distance) - { - mmov = mon_compass[dir0]; - break; - } - if (dist[1] <= dist[0] && dist[1] <= current_distance) - { - mmov = mon_compass[dir1]; - break; - } - } - } -} - -static bool _monster_move(monsters *monster) -{ - FixedArray good_move; - - const habitat_type habitat = mons_primary_habitat(monster); - bool deep_water_available = false; - - if (monster->type == MONS_TRAPDOOR_SPIDER) - { - if (monster->submerged()) + if (monster->hit_points >= random2avg(19, 2)) return (false); + break; - // 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; + case CLOUD_COLD: + if (monster->res_cold() > 1) return (false); - } - } - // Berserking monsters make a lot of racket. - if (monster->has_ench(ENCH_BERSERK)) - { - int noise_level = get_shout_noise_level(mons_shouts(monster->type)); - if (noise_level > 0) - { - if (you.can_see(monster)) - { - if (one_chance_in(10)) - { - mprf(MSGCH_TALK_VISUAL, "%s rages.", - monster->name(DESC_CAP_THE).c_str()); - } - noisy(noise_level, monster->pos(), monster->mindex()); - } - else if (one_chance_in(5)) - handle_monster_shouts(monster, true); - else - { - // Just be noisy without messaging the player. - noisy(noise_level, monster->pos(), monster->mindex()); - } - } - } + if (extra_careful) + return (true); - if (monster->confused()) - { - if (!mmov.origin() || one_chance_in(15)) - { - const coord_def newpos = monster->pos() + mmov; - if (in_bounds(newpos) - && (habitat == HT_LAND - || monster_habitable_grid(monster, grd(newpos)))) - { - return _do_move_monster(monster, mmov); - } - } - return (false); - } + if (mons_intel(monster) >= I_ANIMAL && monster->res_cold() < 0) + return (true); - // If a water monster is currently flopping around on land, it cannot - // really control where it wants to move, though there's a 50% chance - // of flopping into an adjacent water grid. - if (monster->has_ench(ENCH_AQUATIC_LAND)) - { - std::vector adj_water; - std::vector adj_move; - for (adjacent_iterator ai(monster->pos()); ai; ++ai) - { - if (!cell_is_solid(*ai)) - { - adj_move.push_back(*ai); - if (feat_is_watery(grd(*ai))) - adj_water.push_back(*ai); - } - } - if (adj_move.empty()) - { - simple_monster_message(monster, " flops around on dry land!"); + if (monster->hit_points >= 15 + random2avg(46, 5)) return (false); - } + break; - std::vector moves = adj_water; - if (adj_water.empty() || coinflip()) - moves = adj_move; + case CLOUD_POISON: + if (monster->res_poison() > 0) + return (false); - coord_def newpos = monster->pos(); - int count = 0; - for (unsigned int i = 0; i < moves.size(); ++i) - if (one_chance_in(++count)) - newpos = moves[i]; + if (extra_careful) + return (true); - const monsters *mon2 = monster_at(newpos); - if (newpos == you.pos() && mons_wont_attack(monster) - || (mon2 && mons_wont_attack(monster) == mons_wont_attack(mon2))) - { + if (mons_intel(monster) >= I_ANIMAL && monster->res_poison() < 0) + return (true); - simple_monster_message(monster, " flops around on dry land!"); + if (monster->hit_points >= random2avg(37, 4)) return (false); - } + break; - return _do_move_monster(monster, newpos - monster->pos()); - } + case CLOUD_GREY_SMOKE: + if (placement) + return (false); - // Let's not even bother with this if mmov is zero. - if (mmov.origin()) - return (false); + // This isn't harmful, but dumb critters might think so. + if (mons_intel(monster) > I_ANIMAL || coinflip()) + 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; + if (monster->res_fire() > 0) + return (false); - // 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 (monster->hit_points >= random2avg(19, 2)) + return (false); + break; - if (target_grid == DNGN_DEEP_WATER) - deep_water_available = true; + case CLOUD_RAIN: + // Fiery monsters dislike the rain. + if (monster->is_fiery() && extra_careful) + return (true); - good_move[count_x][count_y] = - _mon_can_move_to_pos(monster, coord_def(count_x-1, count_y-1)); - } + // We don't care about what's underneath the rain cloud if we can fly. + if (monster->flight_mode() != FL_NONE) + return (false); - // Now we know where we _can_ move. + // These don't care about deep water. + if (monster_habitable_grid(monster, DNGN_DEEP_WATER)) + return (false); - const coord_def newpos = monster->pos() + mmov; - // Normal/smart monsters know about secret doors, since they live in - // the dungeon. - if (grd(newpos) == DNGN_CLOSED_DOOR - || feat_is_secret_door(grd(newpos)) && mons_intel(monster) >= I_NORMAL) - { - if (mons_is_zombified(monster)) - { - // For zombies, monster type is kept in mon->base_monster. - if (mons_class_itemuse(monster->base_monster) >= MONUSE_OPEN_DOORS) - { - _mons_open_door(monster, newpos); - return (true); - } - } - else if (mons_itemuse(monster) >= MONUSE_OPEN_DOORS) - { - _mons_open_door(monster, newpos); + // This position could become deep water, and they might drown. + if (grd(cloud.pos) == DNGN_SHALLOW_WATER) return (true); - } - } // endif - secret/closed doors - // Monsters that eat items (currently only jellies) also eat doors. - // However, they don't realise that secret doors make good eating. - if ((grd(newpos) == DNGN_CLOSED_DOOR || grd(newpos) == DNGN_OPEN_DOOR) - && mons_itemeat(monster) == MONEAT_ITEMS - // Doors with permarock marker cannot be eaten. - && !feature_marker_at(newpos, DNGN_PERMAROCK_WALL)) - { - grd(newpos) = DNGN_FLOOR; - - _jelly_grows(monster); + // Otherwise, it's safe for everyone else. + return (false); - if (see_cell(newpos)) - { - viewwindow(true, false); + break; - 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; - } - } - } + default: + break; } - // 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); + // 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 haven't found a good move by this point, we're not going to. - // ------------------------------------------------------------------ + // If we get here, the cloud is potentially harmful. + return (true); +} - // Take care of beetle burrowing. - if (mons_class_flag(monster->type, M_BURROWS)) +// 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) { - 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 (cl_type != NULL) + *cl_type = CLOUD_NONE; - 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); - } - } + return (false); } - 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; + const cloud_struct &cloud = env.cloud[cloud_num]; -#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 (cl_type != NULL) + *cl_type = cloud.type; -#if DEBUG_DIAGNOSTICS - mprf(MSGCH_DIAGNOSTICS, - "%s is skipping movement in order to follow.", - monster->name(DESC_CAP_THE).c_str(), true ); -#endif - } - } + // Is the target cloud okay? + if (!mons_avoids_cloud(monster, cloud, placement)) + return (false); - // 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 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); - // If the monster swapped places, the work's already done. - mmov.reset(); - } + const int our_cloud_num = env.cgrid(monster->pos()); - if (monster->type == MONS_EFREET - || monster->type == MONS_FIRE_ELEMENTAL) - { - place_cloud( CLOUD_FIRE, monster->pos(), - 2 + random2(4), monster->kill_alignment() ); - } + if (our_cloud_num == EMPTY_CLOUD) + return (true); - 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(); + const cloud_struct &our_cloud = env.cloud[our_cloud_num]; - // Fleeing monsters that can't move will panic and possibly - // turn to face their attacker. - _make_mons_stop_fleeing(monster); - } + return (!mons_avoids_cloud(monster, our_cloud, true)); +} - if (mmov.x || mmov.y || (monster->confused() && one_chance_in(6))) - return (_do_move_monster(monster, mmov)); +// 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 (ret); + return std::max(0, (property(*weap, PWPN_DAMAGE) + weap->plus2 / 2)); } -static void _mons_in_cloud(monsters *monster) +int mons_weapon_damage_rating(const item_def &launcher) { - int wc = env.cgrid(monster->pos()); - int hurted = 0; - bolt beam; + return (property(launcher, PWPN_DAMAGE) + launcher.plus2); +} - const int speed = ((monster->speed > 0) ? monster->speed : 10); - bool wake = false; +// 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); - if (mons_is_mimic( monster->type )) - { - mimic_alert(monster); - return; - } + 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); +} - const cloud_struct &cloud(env.cloud[wc]); - switch (cloud.type) +// 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) { - case CLOUD_DEBUGGING: - mprf(MSGCH_ERROR, - "Monster %s stepped on a nonexistent cloud at (%d,%d)", - monster->name(DESC_PLAIN, true).c_str(), - monster->pos().x, monster->pos().y); - return; - - case CLOUD_FIRE: - case CLOUD_FOREST_FIRE: - if (monster->type == MONS_FIRE_VORTEX - || monster->type == MONS_EFREET - || monster->type == MONS_FIRE_ELEMENTAL) + if (item_def *item = mons->mslot_item(static_cast(i))) { - return; + if (is_range_weapon(*item)) + launch = item; + else if (!ignore_melee) + melee = item; } + } - simple_monster_message(monster, " is engulfed in flames!"); - - hurted += - resist_adjust_damage( monster, - BEAM_FIRE, - monster->res_fire(), - ((random2avg(16, 3) + 6) * 10) / speed ); - - hurted -= random2(1 + monster->ac); - break; - - case CLOUD_STINK: - simple_monster_message(monster, " is engulfed in noxious gasses!"); - - if (monster->res_poison() > 0) - return; - - beam.flavour = BEAM_CONFUSION; - beam.thrower = cloud.killer; - - if (cloud.whose == KC_FRIENDLY) - beam.beam_source = ANON_FRIENDLY_MONSTER; - - if (mons_class_is_confusable(monster->type) - && 1 + random2(27) >= monster->hit_dice) - { - beam.apply_enchantment_to_monster(monster); - } + const item_def *missiles = mons->missiles(); + if (launch && missiles && !missiles->launched_by(*launch)) + launch = NULL; - hurted += (random2(3) * 10) / speed; - break; + const int tdam = mons_thrown_weapon_damage(melee); + const int fdam = mons_missile_damage(mons, launch, missiles); - case CLOUD_COLD: - simple_monster_message(monster, " is engulfed in freezing vapours!"); + if (!tdam && !fdam) + return (NON_ITEM); + else if (tdam >= fdam) + return (melee->index()); + else + { + *launcher = launch; + return (missiles->index()); + } +} - hurted += - resist_adjust_damage( monster, - BEAM_COLD, - monster->res_cold(), - ((6 + random2avg(16, 3)) * 10) / speed ); +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); - hurted -= random2(1 + monster->ac); + // 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; - case CLOUD_POISON: - simple_monster_message(monster, " is engulfed in a cloud of poison!"); - - if (monster->res_poison() > 0) - return; - - poison_monster(monster, cloud.whose); - // If the monster got poisoned, wake it up. - wake = true; - - hurted += (random2(8) * 10) / speed; - - if (monster->res_poison() < 0) - hurted += (random2(4) * 10) / speed; + // And golems have it worse. + case MH_NONLIVING: + divider *= 5; break; - case CLOUD_STEAM: - { - // FIXME: couldn't be bothered coding for armour of res fire - - simple_monster_message(monster, " is engulfed in steam!"); - - const int steam_base_damage = steam_cloud_damage(cloud); - hurted += - resist_adjust_damage( - monster, - BEAM_STEAM, - monster->res_steam(), - (random2avg(steam_base_damage, 2) * 10) / speed); - - hurted -= random2(1 + monster->ac); + default: break; } - case CLOUD_MIASMA: - simple_monster_message(monster, " is engulfed in a dark miasma!"); - - if (monster->res_rotting()) - return; - - miasma_monster(monster, cloud.whose); - - hurted += (10 * random2avg(12, 3)) / speed; // 3 - break; - - case CLOUD_RAIN: - if (monster->is_fiery()) - { - if (!silenced(monster->pos())) - simple_monster_message(monster, " sizzles in the rain!"); - else - simple_monster_message(monster, " steams in the rain!"); + return (std::max(div_rand_round(monster->hit_dice, divider), 1)); +} - hurted += ((4 * random2(3)) - random2(monster->ac)); - wake = true; - } - break; +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; - case CLOUD_MUTAGENIC: - simple_monster_message(monster, " is engulfed in a mutagenic fog!"); + 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); - // Will only polymorph a monster if they're not magic immune, can - // mutate, aren't res asphyx, and pass the same check as meph cloud. - if (monster->can_mutate() && !mons_immune_magic(monster) - && 1 + random2(27) >= monster->hit_dice - && !monster->res_asphyx()) + // 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)) { - if (monster->mutate()) - wake = true; + mprf("%s falls into the %s!", + monster->name(DESC_CAP_THE).c_str(), + grid == DNGN_LAVA ? "lava" : "water"); } - break; - default: // 'harmless' clouds -- colored smoke, etc {dlb}. - return; - } + if (grid == DNGN_LAVA && monster->res_fire() >= 2) + grid = DNGN_DEEP_WATER; - // A sleeping monster that sustains damage will wake up. - if ((wake || hurted > 0) && monster->asleep()) - { - // We have no good coords to give the monster as the source of the - // disturbance other than the cloud itself. - behaviour_event(monster, ME_DISTURB, MHITNOT, monster->pos()); - } + // 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 (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 (killer == KILL_NONE) + { + // Self-kill. + killer = KILL_MON; + killnum = monster_index(monster); + } - if (monster->hit_points < 1) - { - mon_enchant death_ench(ENCH_NONE, 0, cloud.whose); - monster_die(monster, cloud.killer, death_ench.kill_agent()); + // 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); } } } @@ -9698,30 +3415,6 @@ bool heal_monster(monsters * patient, int health_boost, 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 @@ -9839,3 +3532,369 @@ int dismiss_monsters(std::string pattern) { } return (ndismissed); } + +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); +} + +bool monster_random_space(const monsters *monster, coord_def& target, + bool forbid_sanctuary) +{ + int tries = 0; + while (tries++ < 1000) + { + target = random_in_bounds(); + + // Don't land on top of another monster. + if (actor_at(target)) + continue; + + if (is_sanctuary(target) && forbid_sanctuary) + continue; + + if (monster_habitable_grid(monster, grd(target))) + return (true); + } + + return (false); +} + +bool monster_random_space(monster_type mon, coord_def& target, + bool forbid_sanctuary) +{ + monsters dummy; + dummy.type = mon; + + return monster_random_space(&dummy, target, forbid_sanctuary); +} + +void monster_teleport(monsters *monster, bool instan, bool silent) +{ + if (!instan) + { + if (monster->del_ench(ENCH_TP)) + { + if (!silent) + simple_monster_message(monster, " seems more stable."); + } + else + { + if (!silent) + simple_monster_message(monster, " looks slightly unstable."); + + monster->add_ench( mon_enchant(ENCH_TP, 0, KC_OTHER, + random_range(20, 30)) ); + } + + return; + } + + bool was_seen = you.can_see(monster) && !mons_is_lurking(monster); + + if (!silent) + simple_monster_message(monster, " disappears!"); + + const coord_def oldplace = monster->pos(); + + // Pick the monster up. + mgrd(oldplace) = NON_MONSTER; + + coord_def newpos; + if (monster_random_space(monster, newpos, !mons_wont_attack(monster))) + monster->moveto(newpos); + + mgrd(monster->pos()) = monster_index(monster); + + // Mimics change form/colour when teleported. + if (mons_is_mimic(monster->type)) + { + monster_type old_type = monster->type; + monster->type = static_cast( + MONS_GOLD_MIMIC + random2(5)); + monster->colour = get_mimic_colour(monster); + + // If it's changed form, you won't recognise it. + // This assumes that a non-gold mimic turning into another item of + // the same description is really, really unlikely. + if (old_type != MONS_GOLD_MIMIC || monster->type != MONS_GOLD_MIMIC) + was_seen = false; + } + + const bool now_visible = mons_near(monster); + if (!silent && now_visible) + { + if (was_seen) + simple_monster_message(monster, " reappears nearby!"); + else + { + // Even if it doesn't interrupt an activity (the player isn't + // delayed, the monster isn't hostile) we still want to give + // a message. + activity_interrupt_data ai(monster, "thin air"); + if (!interrupt_activity(AI_SEE_MONSTER, ai)) + simple_monster_message(monster, " appears out of thin air!"); + } + } + + if (monster->visible_to(&you) && now_visible) + handle_seen_interrupt(monster); + + // Leave a purple cloud. + place_cloud(CLOUD_PURP_SMOKE, oldplace, 1 + random2(3), + monster->kill_alignment()); + + monster->check_redraw(oldplace); + monster->apply_location_effects(oldplace); + + mons_relocated(monster); + + // Teleporting mimics change form - if they reappear out of LOS, they are + // no longer known. + if (mons_is_mimic(monster->type)) + { + if (now_visible) + monster->flags |= MF_KNOWN_MIMIC; + else + monster->flags &= ~MF_KNOWN_MIMIC; + } +} + +void mons_clear_trapping_net(monsters *mon) +{ + if (!mons_is_caught(mon)) + return; + + const int net = get_trapping_net(mon->pos()); + if (net != NON_ITEM) + remove_item_stationary(mitm[net]); + + mon->del_ench(ENCH_HELD, true); +} + +bool mons_clonable(const monsters* mon, bool needs_adjacent) +{ + // No uniques or ghost demon monsters. Also, figuring out the name + // for the clone of a named monster isn't worth it. + if (mons_is_unique(mon->type) + || mons_is_ghost_demon(mon->type) + || mon->is_named()) + { + return (false); + } + + if (needs_adjacent) + { + // Is there space for the clone? + bool square_found = false; + for (int i = 0; i < 8; i++) + { + const coord_def p = mon->pos() + Compass[i]; + + if (in_bounds(p) + && !actor_at(p) + && monster_habitable_grid(mon, grd(p))) + { + square_found = true; + break; + } + } + if (!square_found) + return (false); + } + + // Is the monster carrying an artefact? + for (int i = 0; i < NUM_MONSTER_SLOTS; i++) + { + const int index = mon->inv[i]; + + if (index == NON_ITEM) + continue; + + if (is_artefact(mitm[index])) + return (false); + } + + return (true); +} + +int clone_mons(const monsters* orig, bool quiet, bool* obvious, + coord_def pos) +{ + // Is there an open slot in menv? + int midx = NON_MONSTER; + for (int i = 0; i < MAX_MONSTERS; i++) + if (menv[i].type == MONS_NO_MONSTER) + { + midx = i; + break; + } + + if (midx == NON_MONSTER) + return (NON_MONSTER); + + if (!in_bounds(pos)) + { + // Find an adjacent square. + int squares = 0; + for (int i = 0; i < 8; i++) + { + const coord_def p = orig->pos() + Compass[i]; + + if (in_bounds(p) + && !actor_at(p) + && monster_habitable_grid(orig, grd(p))) + { + if (one_chance_in(++squares)) + pos = p; + } + } + + if (squares == 0) + return (NON_MONSTER); + } + + ASSERT( !actor_at(pos) ); + + monsters &mon(menv[midx]); + + mon = *orig; + mon.position = pos; + mgrd(pos) = midx; + + // Duplicate objects, or unequip them if they can't be duplicated. + for (int i = 0; i < NUM_MONSTER_SLOTS; i++) + { + const int old_index = orig->inv[i]; + + if (old_index == NON_ITEM) + continue; + + const int new_index = get_item_slot(0); + if (new_index == NON_ITEM) + { + mon.unequip(mitm[old_index], i, 0, true); + mon.inv[i] = NON_ITEM; + continue; + } + + mon.inv[i] = new_index; + mitm[new_index] = mitm[old_index]; + mitm[new_index].set_holding_monster(midx); + } + + bool _obvious; + if (obvious == NULL) + obvious = &_obvious; + *obvious = false; + + if (you.can_see(orig) && you.can_see(&mon)) + { + if (!quiet) + simple_monster_message(orig, " is duplicated!"); + *obvious = true; + } + + mark_interesting_monst(&mon, mon.behaviour); + if (you.can_see(&mon)) + { + handle_seen_interrupt(&mon); + viewwindow(true, false); + } + + if (crawl_state.arena) + arena_placed_monster(&mon); + + return (midx); +} + +std::string summoned_poof_msg(const monsters* monster, bool plural) +{ + int summon_type = 0; + bool valid_mon = false; + if (monster != NULL && !invalid_monster(monster)) + { + (void) monster->is_summoned(NULL, &summon_type); + valid_mon = true; + } + + std::string msg = "disappear%s in a puff of smoke"; + bool no_chaos = false; + + switch (summon_type) + { + case SPELL_SHADOW_CREATURES: + msg = "dissolve%s into shadows"; + no_chaos = true; + break; + + case MON_SUMM_CHAOS: + msg = "degenerate%s into a cloud of primal chaos"; + break; + + case MON_SUMM_WRATH: + case MON_SUMM_AID: + if (valid_mon && is_good_god(monster->god)) + { + msg = "dissolve%s into sparkling lights"; + no_chaos = true; + } + break; + } + + if (valid_mon) + { + if (monster->god == GOD_XOM && !no_chaos && one_chance_in(10) + || monster->type == MONS_CHAOS_SPAWN) + { + msg = "degenerate%s into a cloud of primal chaos"; + } + + if (mons_is_holy(monster) && summon_type != SPELL_SHADOW_CREATURES + && summon_type != MON_SUMM_CHAOS) + { + msg = "dissolve%s into sparkling lights"; + } + } + + // Conjugate. + msg = make_stringf(msg.c_str(), plural ? "" : "s"); + + return (msg); +} + +std::string summoned_poof_msg(const int midx, const item_def &item) +{ + if (midx == NON_MONSTER) + return summoned_poof_msg(static_cast(NULL), item); + else + return summoned_poof_msg(&menv[midx], item); +} + +std::string summoned_poof_msg(const monsters* monster, const item_def &item) +{ + ASSERT(item.flags & ISFLAG_SUMMONED); + + return summoned_poof_msg(monster, item.quantity > 1); +} diff --git a/crawl-ref/source/monstuff.h b/crawl-ref/source/monstuff.h index 5d6f3925cb..1b808fed6d 100644 --- a/crawl-ref/source/monstuff.h +++ b/crawl-ref/source/monstuff.h @@ -1,5 +1,5 @@ /* - * File: monstuff.cc + * File: monstuff.h * Summary: Misc monster related functions. * Written by: Linley Henzell */ @@ -111,10 +111,6 @@ void monster_cleanup(monsters *monster); int dismiss_monsters(std::string pattern); -void behaviour_event(monsters *mon, mon_event_type event_type, - int src = MHITNOT, coord_def src_pos = coord_def(), - bool allow_shout = true); - bool curse_an_item(bool decay_potions, bool quiet = false); @@ -147,8 +143,6 @@ bool simple_monster_message(const monsters *monster, const char *event, int param = 0, description_level_type descrip = DESC_CAP_THE); -void make_mons_leave_level(monsters *mon); - bool choose_any_monster(const monsters* mon); monsters *choose_random_nearby_monster( int weight, @@ -171,14 +165,10 @@ bool swap_check(monsters *monster, coord_def &loc, bool quiet = false); std::string get_wounds_description(const monsters *monster); void print_wounds(const monsters *monster); -void handle_monsters(void); bool monster_descriptor(int which_class, mon_desc_type which_descriptor); -bool message_current_target(void); unsigned int monster_index(const monsters *monster); -bool monster_can_hit_monster(monsters *monster, const monsters *targ); - void mons_get_damage_level(const monsters*, std::string& desc, mon_dam_level_type&); @@ -197,17 +187,25 @@ int mons_thrown_weapon_damage(const item_def *weap); int mons_natural_regen_rate(monsters *monster); -bool mons_avoids_cloud(const monsters *monster, cloud_type cl_type, - bool placement = false); - -// Like the above, but allow a monster to move from one damaging cloud -// to another. -bool mons_avoids_cloud(const monsters *monster, int cloud_num, - cloud_type *cl_type = NULL, bool placement = false); - void mons_relocated(monsters *mons); bool can_go_straight(const coord_def& p1, const coord_def& p2, dungeon_feature_type allowed); +bool is_item_jelly_edible(const item_def &item); + +bool monster_random_space(const monsters *monster, coord_def& target, + bool forbid_sanctuary = false); +bool monster_random_space(monster_type mon, coord_def& target, + bool forbid_sanctuary = false); +void monster_teleport(monsters *monster, bool instan, bool silent = false); +void mons_clear_trapping_net(monsters *mon); + +bool mons_clonable(const monsters* orig, bool needs_adjacent = true); +int clone_mons(const monsters* orig, bool quiet = false, + bool* obvious = NULL, coord_def pos = coord_def(0, 0) ); + +std::string summoned_poof_msg(const monsters* monster, bool plural = false); +std::string summoned_poof_msg(const int midx, const item_def &item); +std::string summoned_poof_msg(const monsters* monster, const item_def &item); #endif diff --git a/crawl-ref/source/mstuff2.cc b/crawl-ref/source/mstuff2.cc index a86faa226f..acaa231a67 100644 --- a/crawl-ref/source/mstuff2.cc +++ b/crawl-ref/source/mstuff2.cc @@ -48,3229 +48,4 @@ #include "traps.h" #include "view.h" -static int _monster_abjuration(const monsters *caster, bool actual); -static bool _mons_abjured(monsters *monster, bool nearby) -{ - if (nearby && _monster_abjuration(monster, false) > 0 - && coinflip()) - { - _monster_abjuration(monster, true); - return (true); - } - - return (false); -} - -static monster_type _pick_random_wraith() -{ - static monster_type wraiths[] = - { - MONS_WRAITH, MONS_SHADOW_WRAITH, MONS_FREEZING_WRAITH, - MONS_SPECTRAL_WARRIOR, MONS_PHANTOM, MONS_HUNGRY_GHOST, - MONS_FLAYED_GHOST - }; - - return (RANDOM_ELEMENT(wraiths)); -} - -static monster_type _pick_horrible_thing() -{ - return (one_chance_in(4) ? MONS_TENTACLED_MONSTROSITY - : MONS_ABOMINATION_LARGE); -} - -static monster_type _pick_undead_summon() -{ - static monster_type undead[] = - { - MONS_NECROPHAGE, MONS_GHOUL, MONS_HUNGRY_GHOST, MONS_FLAYED_GHOST, - MONS_ZOMBIE_SMALL, MONS_SKELETON_SMALL, MONS_SIMULACRUM_SMALL, - MONS_FLYING_SKULL, MONS_FLAMING_CORPSE, MONS_MUMMY, MONS_VAMPIRE, - MONS_WIGHT, MONS_WRAITH, MONS_SHADOW_WRAITH, MONS_FREEZING_WRAITH, - MONS_SPECTRAL_WARRIOR, MONS_ZOMBIE_LARGE, MONS_SKELETON_LARGE, - MONS_SIMULACRUM_LARGE, MONS_SHADOW - }; - - return (RANDOM_ELEMENT(undead)); -} - -static void _do_high_level_summon(monsters *monster, bool monsterNearby, - spell_type spell_cast, - monster_type (*mpicker)(), int nsummons, - god_type god, coord_def *target = NULL) -{ - if (_mons_abjured(monster, monsterNearby)) - return; - - const int duration = std::min(2 + monster->hit_dice / 5, 6); - - for (int i = 0; i < nsummons; ++i) - { - monster_type which_mons = mpicker(); - - if (which_mons == MONS_NO_MONSTER) - continue; - - create_monster( - mgen_data(which_mons, SAME_ATTITUDE(monster), - duration, spell_cast, target ? *target : monster->pos(), - monster->foe, 0, god)); - } -} - -static bool _los_free_spell(spell_type spell_cast) -{ - return (spell_cast == SPELL_HELLFIRE_BURST - || spell_cast == SPELL_BRAIN_FEED - || spell_cast == SPELL_SMITING - || spell_cast == SPELL_HAUNT - || spell_cast == SPELL_FIRE_STORM - || spell_cast == SPELL_AIRSTRIKE); -} - -// Returns true if a message referring to the player's legs makes sense. -static bool _legs_msg_applicable() -{ - return (you.species != SP_NAGA - && (you.species != SP_MERFOLK || !player_is_swimming())); -} - -void mons_cast_haunt(monsters *monster) -{ - coord_def fpos; - - switch (monster->foe) - { - case MHITNOT: - return; - - case MHITYOU: - fpos = you.pos(); - break; - - default: - fpos = menv[monster->foe].pos(); - } - - _do_high_level_summon(monster, mons_near(monster), SPELL_HAUNT, - _pick_random_wraith, random_range(3, 6), GOD_NO_GOD, &fpos); -} - -void mons_cast(monsters *monster, bolt &pbolt, spell_type spell_cast, - bool do_noise) -{ - // Always do setup. It might be done already, but it doesn't hurt - // to do it again (cheap). - setup_mons_cast(monster, pbolt, spell_cast); - - // single calculation permissible {dlb} - bool monsterNearby = mons_near(monster); - - int sumcount = 0; - int sumcount2; - int duration = 0; - -#if DEBUG_DIAGNOSTICS - mprf(MSGCH_DIAGNOSTICS, "Mon #%d casts %s (#%d)", - monster_index(monster), spell_title(spell_cast), spell_cast); -#endif - - if (spell_cast == SPELL_CANTRIP) - do_noise = false; // Spell itself does the messaging. - - if (_los_free_spell(spell_cast) && !spell_is_direct_explosion(spell_cast)) - { - if (monster->foe == MHITYOU || monster->foe == MHITNOT) - { - if (monsterNearby) - { - if (do_noise) - mons_cast_noise(monster, pbolt, spell_cast); - direct_effect(monster, spell_cast, pbolt, &you); - } - return; - } - - if (do_noise) - mons_cast_noise(monster, pbolt, spell_cast); - direct_effect(monster, spell_cast, pbolt, monster->get_foe()); - return; - } - -#ifdef DEBUG - const unsigned int flags = get_spell_flags(spell_cast); - - ASSERT(!(flags & (SPFLAG_TESTING | SPFLAG_MAPPING))); - - // Targeted spells need a valid target. - ASSERT(!(flags & SPFLAG_TARGETTING_MASK) || in_bounds(pbolt.target)); -#endif - - if (do_noise) - mons_cast_noise(monster, pbolt, spell_cast); - - // 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; - - switch (spell_cast) - { - default: - break; - - case SPELL_MAJOR_HEALING: - if (heal_monster(monster, 50 + random2avg(monster->hit_dice * 10, 2), - false)) - { - simple_monster_message(monster, " is healed."); - } - return; - - case SPELL_BERSERKER_RAGE: - monster->go_berserk(true); - return; - - case SPELL_SUMMON_SMALL_MAMMALS: - case SPELL_VAMPIRE_SUMMON: - if (spell_cast == SPELL_SUMMON_SMALL_MAMMALS) - sumcount2 = 1 + random2(4); - else - sumcount2 = 3 + random2(3) + monster->hit_dice / 5; - - for (sumcount = 0; sumcount < sumcount2; ++sumcount) - { - const monster_type rats[] = { MONS_ORANGE_RAT, MONS_GREEN_RAT, - MONS_GREY_RAT, MONS_RAT }; - const monster_type mon = (one_chance_in(3) ? MONS_GIANT_BAT - : RANDOM_ELEMENT(rats)); - create_monster( - mgen_data(mon, SAME_ATTITUDE(monster), - 5, spell_cast, monster->pos(), monster->foe, 0, god)); - } - return; - - case SPELL_SHADOW_CREATURES: // summon anything appropriate for level - if (_mons_abjured(monster, monsterNearby)) - return; - - sumcount2 = 1 + random2(4) + random2(monster->hit_dice / 7 + 1); - - for (sumcount = 0; sumcount < sumcount2; ++sumcount) - { - create_monster( - mgen_data(RANDOM_MONSTER, SAME_ATTITUDE(monster), - 5, spell_cast, monster->pos(), monster->foe, 0, god)); - } - return; - - case SPELL_WATER_ELEMENTALS: - if (_mons_abjured(monster, monsterNearby)) - return; - - sumcount2 = 1 + random2(4) + random2(monster->hit_dice / 7 + 1); - - for (sumcount = 0; sumcount < sumcount2; sumcount++) - { - create_monster( - mgen_data(MONS_WATER_ELEMENTAL, SAME_ATTITUDE(monster), - 3, spell_cast, monster->pos(), monster->foe, 0, god)); - } - return; - - case SPELL_KRAKEN_TENTACLES: - { - int kraken_index = monster_index(monster); - if (invalid_monster_index(duration)) - { - mpr("Error! Kraken is not a part of the current environment!", - MSGCH_ERROR); - return; - } - sumcount2 = std::max(random2(9), random2(9)); // up to eight tentacles - if (sumcount2 == 0) - return; - - for (sumcount = 0; sumcount < MAX_MONSTERS; ++sumcount) - if (menv[sumcount].type == MONS_KRAKEN_TENTACLE - && (int)menv[sumcount].number == kraken_index) - { - // Reduce by tentacles already placed. - sumcount2--; - } - - for (sumcount = sumcount2; sumcount > 0; --sumcount) - { - // Tentacles aren't really summoned (controlled by spell_cast - // being passed to summon_type), so I'm not sure what the - // abjuration value (3) is doing there. (jpeg) - if (create_monster( - mgen_data(MONS_KRAKEN_TENTACLE, SAME_ATTITUDE(monster), - 3, spell_cast, monster->pos(), monster->foe, 0, god, - MONS_NO_MONSTER, kraken_index, monster->colour, - you.your_level, PROX_CLOSE_TO_PLAYER, - you.level_type)) == -1) - { - sumcount2--; - } - } - if (sumcount2 == 1) - mpr("A tentacle rises from the water!"); - else if (sumcount2 > 1) - mpr("Tentacles burst out of the water!"); - return; - } - case SPELL_FAKE_RAKSHASA_SUMMON: - sumcount2 = (coinflip() ? 2 : 3); - - for (sumcount = 0; sumcount < sumcount2; sumcount++) - { - create_monster( - mgen_data(MONS_RAKSHASA_FAKE, SAME_ATTITUDE(monster), - 3, spell_cast, monster->pos(), monster->foe, 0, god)); - } - return; - - case SPELL_SUMMON_DEMON: // class 2-4 demons - if (_mons_abjured(monster, monsterNearby)) - return; - - sumcount2 = 1 + random2(2) + random2(monster->hit_dice / 10 + 1); - - duration = std::min(2 + monster->hit_dice / 10, 6); - for (sumcount = 0; sumcount < sumcount2; sumcount++) - { - create_monster( - mgen_data(summon_any_demon(DEMON_COMMON), - SAME_ATTITUDE(monster), duration, spell_cast, - monster->pos(), monster->foe, 0, god)); - } - return; - - case SPELL_SUMMON_UGLY_THING: - if (_mons_abjured(monster, monsterNearby)) - return; - - sumcount2 = 1 + random2(2) + random2(monster->hit_dice / 10 + 1); - - duration = std::min(2 + monster->hit_dice / 10, 6); - for (sumcount = 0; sumcount < sumcount2; ++sumcount) - { - const int chance = std::max(6 - (monster->hit_dice / 6), 1); - monster_type mon = (one_chance_in(chance) ? MONS_VERY_UGLY_THING - : MONS_UGLY_THING); - - create_monster( - mgen_data(mon, SAME_ATTITUDE(monster), - duration, spell_cast, monster->pos(), monster->foe, 0, - god)); - } - return; - - case SPELL_ANIMATE_DEAD: - // see special handling in monstuff::handle_spell() {dlb} - animate_dead(monster, 5 + random2(5), SAME_ATTITUDE(monster), - monster->foe, god); - return; - - case SPELL_CALL_IMP: // class 5 demons - sumcount2 = 1 + random2(3) + random2(monster->hit_dice / 5 + 1); - - duration = std::min(2 + monster->hit_dice / 5, 6); - for (sumcount = 0; sumcount < sumcount2; ++sumcount) - { - create_monster( - mgen_data(summon_any_demon(DEMON_LESSER), - SAME_ATTITUDE(monster), - duration, spell_cast, monster->pos(), monster->foe, 0, - god)); - } - return; - - case SPELL_SUMMON_SCORPIONS: - if (_mons_abjured(monster, monsterNearby)) - return; - - sumcount2 = 1 + random2(3) + random2(monster->hit_dice / 5 + 1); - - duration = std::min(2 + monster->hit_dice / 5, 6); - for (sumcount = 0; sumcount < sumcount2; ++sumcount) - { - create_monster( - mgen_data(MONS_SCORPION, SAME_ATTITUDE(monster), - duration, spell_cast, monster->pos(), monster->foe, 0, - god)); - } - return; - - case SPELL_SUMMON_UFETUBUS: - sumcount2 = 2 + random2(2) + random2(monster->hit_dice / 5 + 1); - - duration = std::min(2 + monster->hit_dice / 5, 6); - - for (sumcount = 0; sumcount < sumcount2; ++sumcount) - { - create_monster( - mgen_data(MONS_UFETUBUS, SAME_ATTITUDE(monster), - duration, spell_cast, monster->pos(), monster->foe, 0, - god)); - } - return; - - case SPELL_SUMMON_BEAST: // Geryon - create_monster( - mgen_data(MONS_BEAST, SAME_ATTITUDE(monster), - 4, spell_cast, monster->pos(), monster->foe, 0, god)); - return; - - case SPELL_SUMMON_ICE_BEAST: - create_monster( - mgen_data(MONS_ICE_BEAST, SAME_ATTITUDE(monster), - 5, spell_cast, monster->pos(), monster->foe, 0, god)); - return; - - case SPELL_SUMMON_MUSHROOMS: // Summon swarms of icky crawling fungi. - if (_mons_abjured(monster, monsterNearby)) - return; - - sumcount2 = 1 + random2(2) + random2(monster->hit_dice / 4 + 1); - - duration = std::min(2 + monster->hit_dice / 5, 6); - for (int i = 0; i < sumcount2; ++i) - { - create_monster( - mgen_data(MONS_WANDERING_MUSHROOM, SAME_ATTITUDE(monster), - duration, spell_cast, monster->pos(), monster->foe, 0, - god)); - } - return; - - case SPELL_SUMMON_HORRIBLE_THINGS: - _do_high_level_summon(monster, monsterNearby, spell_cast, - _pick_horrible_thing, random_range(3, 5), god); - return; - - case SPELL_CONJURE_BALL_LIGHTNING: - { - const int n = 2 + random2(monster->hit_dice / 4); - for (int i = 0; i < n; ++i) - { - create_monster( - mgen_data(MONS_BALL_LIGHTNING, SAME_ATTITUDE(monster), - 2, spell_cast, monster->pos(), monster->foe, 0, god)); - } - return; - } - - case SPELL_SUMMON_UNDEAD: // Summon undead around player. - _do_high_level_summon(monster, monsterNearby, spell_cast, - _pick_undead_summon, - 2 + random2(2) - + random2(monster->hit_dice / 4 + 1), god); - return; - - case SPELL_SYMBOL_OF_TORMENT: - if (!monsterNearby || mons_friendly(monster)) - return; - - torment(monster_index(monster), monster->pos()); - return; - - case SPELL_SUMMON_GREATER_DEMON: - if (_mons_abjured(monster, monsterNearby)) - return; - - sumcount2 = 1 + random2(monster->hit_dice / 10 + 1); - - duration = std::min(2 + monster->hit_dice / 10, 6); - for (sumcount = 0; sumcount < sumcount2; ++sumcount) - { - create_monster( - mgen_data(summon_any_demon(DEMON_GREATER), - SAME_ATTITUDE(monster), - duration, spell_cast, monster->pos(), monster->foe, - 0, god)); - } - return; - - // Journey -- Added in Summon Lizards and Draconian - case SPELL_SUMMON_DRAKES: - if (_mons_abjured(monster, monsterNearby)) - return; - - sumcount2 = 1 + random2(3) + random2(monster->hit_dice / 5 + 1); - - duration = std::min(2 + monster->hit_dice / 10, 6); - - { - std::vector monsters; - - for (sumcount = 0; sumcount < sumcount2; sumcount++) - { - monster_type mon = summon_any_dragon(DRAGON_LIZARD); - - if (mon == MONS_DRAGON) - { - monsters.clear(); - monsters.push_back(summon_any_dragon(DRAGON_DRAGON)); - break; - } - - monsters.push_back(mon); - } - - for (int i = 0, size = monsters.size(); i < size; ++i) - { - create_monster( - mgen_data(monsters[i], SAME_ATTITUDE(monster), - duration, spell_cast, - monster->pos(), monster->foe, 0, god)); - } - } - return; - - // TODO: Outsource the cantrip messages and allow specification of - // special cantrip spells per monster, like for speech, both as - // "self buffs" and "player enchantments". - case SPELL_CANTRIP: - { - // Monster spell of uselessness, just prints a message. - // This spell exists so that some monsters with really strong - // spells (ie orc priest) can be toned down a bit. -- bwr - // - // XXX: Needs expansion, and perhaps different priest/mage flavours. - - // Don't give any message if the monster isn't nearby. - // (Otherwise you could get them from halfway across the level.) - if (!mons_near(monster)) - return; - - const bool friendly = mons_friendly(monster); - const bool buff_only = !friendly && is_sanctuary(you.pos()); - const msg_channel_type channel = (friendly) ? MSGCH_FRIEND_ENCHANT - : MSGCH_MONSTER_ENCHANT; - - if (monster->type == MONS_GASTRONOK) - { - bool has_mon_foe = !invalid_monster_index(monster->foe); - std::string slugform = ""; - if (buff_only || crawl_state.arena && !has_mon_foe - || friendly && !has_mon_foe || coinflip()) - { - slugform = getSpeakString("gastronok_self_buff"); - if (!slugform.empty()) - { - slugform = replace_all(slugform, "@The_monster@", - monster->name(DESC_CAP_THE)); - mpr(slugform.c_str(), channel); - } - } - else if (!friendly && !has_mon_foe) - { - mons_cast_noise(monster, pbolt, spell_cast); - - // "Enchant" the player. - slugform = getSpeakString("gastronok_debuff"); - if (!slugform.empty() - && (slugform.find("legs") == std::string::npos - || _legs_msg_applicable())) - { - mpr(slugform.c_str()); - } - } - else - { - // "Enchant" another monster. - const monsters* foe - = dynamic_cast(monster->get_foe()); - slugform = getSpeakString("gastronok_other_buff"); - if (!slugform.empty()) - { - slugform = replace_all(slugform, "@The_monster@", - foe->name(DESC_CAP_THE)); - mpr(slugform.c_str(), MSGCH_MONSTER_ENCHANT); - } - } - } - else - { - // Messages about the monster influencing itself. - const char* buff_msgs[] = { " glows brightly for a moment.", - " looks stronger.", - " becomes somewhat translucent.", - "'s eyes start to glow." }; - - // Messages about the monster influencing you. - const char* other_msgs[] = { - "You feel troubled.", - "You feel a wave of unholy energy pass over you." - }; - - if (buff_only || crawl_state.arena || x_chance_in_y(2,3)) - { - simple_monster_message(monster, RANDOM_ELEMENT(buff_msgs), - channel); - } - else if (friendly) - { - simple_monster_message(monster, " shimmers for a moment.", - channel); - } - else // "Enchant" the player. - { - mons_cast_noise(monster, pbolt, spell_cast); - mpr(RANDOM_ELEMENT(other_msgs)); - } - } - return; - } - case SPELL_BLINK_OTHER: - { - // Allow the caster to comment on moving the foe. - std::string msg = getSpeakString(monster->name(DESC_PLAIN) - + " blink_other"); - if (!msg.empty() && msg != "__NONE") - { - mons_speaks_msg(monster, msg, MSGCH_TALK, - silenced(you.pos()) || silenced(monster->pos())); - } - break; - } - case SPELL_TOMB_OF_DOROKLOHE: - { - sumcount = 0; - for (adjacent_iterator ai(monster->pos()); ai; ++ai) - { - // we can blink away the crowd, but only our allies - if (mgrd(*ai) != NON_MONSTER - && monster_at(*ai)->attitude != monster->attitude) - sumcount++; - if (grd(*ai) != DNGN_FLOOR && grd(*ai) > DNGN_MAX_NONREACH - && !feat_is_trap(grd(*ai))) - sumcount++; - } - if (abs(you.pos().x-monster->pos().x)<=1 && - abs(you.pos().y-monster->pos().y)<=1) - sumcount++; - if (sumcount) - { - monster->blink(); - return; - } - - sumcount = 0; - for (adjacent_iterator ai(monster->pos()); ai; ++ai) - { - if (mgrd(*ai) != NON_MONSTER && monster_at(*ai) != monster) - { - monster_at(*ai)->blink(); - if (mgrd(*ai) != NON_MONSTER) - { - monster_at(*ai)->teleport(true); - if (mgrd(*ai) != NON_MONSTER) - continue; - } - } - if (grd(*ai) == DNGN_FLOOR || feat_is_trap(grd(*ai))) - { - grd(*ai) = DNGN_ROCK_WALL; - sumcount++; - } - } - if (sumcount) - mpr("Walls emerge from the floor!"); - monster->number = 1; // mark Khufu as entombed - return; - } - } - - // If a monster just came into view and immediately cast a spell, - // we need to refresh the screen before drawing the beam. - viewwindow(true, false); - if (spell_is_direct_explosion(spell_cast)) - { - const actor *foe = monster->get_foe(); - const bool need_more = foe && (foe == &you || see_cell(foe->pos())); - pbolt.in_explosion_phase = false; - pbolt.explode(need_more); - } - else - pbolt.fire(); -} - -void mons_cast_noise(monsters *monster, bolt &pbolt, spell_type spell_cast) -{ - bool force_silent = false; - - spell_type real_spell = spell_cast; - - if (spell_cast == SPELL_DRACONIAN_BREATH) - { - int type = monster->type; - if (mons_genus(type) == MONS_DRACONIAN) - type = draco_subspecies(monster); - - switch (type) - { - case MONS_MOTTLED_DRACONIAN: - real_spell = SPELL_STICKY_FLAME_SPLASH; - break; - - case MONS_YELLOW_DRACONIAN: - real_spell = SPELL_ACID_SPLASH; - break; - - case MONS_PLAYER_GHOST: - // Draining breath is silent. - force_silent = true; - break; - - default: - break; - } - } - else if (monster->type == MONS_SHADOW_DRAGON) - // Draining breath is silent. - force_silent = true; - - const bool unseen = !you.can_see(monster); - const bool silent = silenced(monster->pos()) || force_silent; - const bool no_silent = mons_class_flag(monster->type, M_SPELL_NO_SILENT); - - if (unseen && silent) - return; - - const unsigned int flags = get_spell_flags(real_spell); - - const bool priest = mons_class_flag(monster->type, M_PRIEST); - const bool wizard = mons_class_flag(monster->type, M_ACTUAL_SPELLS); - const bool innate = !(priest || wizard || no_silent) - || (flags & SPFLAG_INNATE); - - int noise; - if (silent - || (innate - && !mons_class_flag(monster->type, M_NOISY_SPELLS) - && !(flags & SPFLAG_NOISY) - && mons_genus(monster->type) != MONS_DRAGON)) - { - noise = 0; - } - else - { - if (mons_genus(monster->type) == MONS_DRAGON) - noise = get_shout_noise_level(S_ROAR); - else - noise = spell_noise(real_spell); - } - - const std::string cast_str = " cast"; - - const std::string spell_name = spell_title(real_spell); - const mon_body_shape shape = get_mon_shape(monster); - - std::vector key_list; - - // First try the spells name. - if (shape <= MON_SHAPE_NAGA) - { - if (!innate && (priest || wizard)) - key_list.push_back(spell_name + cast_str + " real"); - if (mons_intel(monster) >= I_NORMAL) - key_list.push_back(spell_name + cast_str + " gestures"); - } - key_list.push_back(spell_name + cast_str); - - const unsigned int num_spell_keys = key_list.size(); - - // Next the monster type name, then species name, then genus name. - key_list.push_back(mons_type_name(monster->type, DESC_PLAIN) + cast_str); - key_list.push_back(mons_type_name(mons_species(monster->type), DESC_PLAIN) - + cast_str); - key_list.push_back(mons_type_name(mons_genus(monster->type), DESC_PLAIN) - + cast_str); - - // Last, generic wizard, priest or demon. - if (wizard) - key_list.push_back("wizard" + cast_str); - else if (priest) - key_list.push_back("priest" + cast_str); - else if (mons_is_demon(monster->type)) - key_list.push_back("demon" + cast_str); - - const bool visible_beam = pbolt.type != 0 && pbolt.type != ' ' - && pbolt.name[0] != '0' - && !pbolt.is_enchantment(); - - const bool targeted = (flags & SPFLAG_TARGETTING_MASK) - && (pbolt.target != monster->pos() || visible_beam); - - if (targeted) - { - // For targeted spells, try with the targeted suffix first. - for (unsigned int i = key_list.size() - 1; i >= num_spell_keys; i--) - { - std::string str = key_list[i] + " targeted"; - key_list.insert(key_list.begin() + i, str); - } - - // Generic beam messages. - if (visible_beam) - { - key_list.push_back(pbolt.get_short_name() + " beam " + cast_str); - key_list.push_back("beam catchall cast"); - } - } - - std::string prefix; - if (silent) - prefix = "silent "; - else if (unseen) - prefix = "unseen "; - - std::string msg; - for (unsigned int i = 0; i < key_list.size(); i++) - { - const std::string key = key_list[i]; - - msg = getSpeakString(prefix + key); - if (msg == "__NONE") - { - msg = ""; - break; - } - else if (msg == "__NEXT") - { - msg = ""; - if (i < num_spell_keys) - i = num_spell_keys - 1; - else if (ends_with(key, " targeted")) - i++; - continue; - } - else if (!msg.empty()) - break; - - // If we got no message and we're using the silent prefix, then - // try again without the prefix. - if (prefix != "silent") - continue; - - msg = getSpeakString(key); - if (msg == "__NONE") - { - msg = ""; - break; - } - else if (msg == "__NEXT") - { - msg = ""; - if (i < num_spell_keys) - i = num_spell_keys - 1; - else if (ends_with(key, " targeted")) - i++; - continue; - } - else if (!msg.empty()) - break; - } - - if (msg.empty()) - { - if (silent) - return; - - noisy(noise, monster->pos(), monster->mindex()); - return; - } - - ///////////////////// - // We have a message. - ///////////////////// - - const bool gestured = msg.find("Gesture") != std::string::npos - || msg.find(" gesture") != std::string::npos - || msg.find("Point") != std::string::npos - || msg.find(" point") != std::string::npos; - - bolt tracer = pbolt; - if (targeted) - { - // For a targeted but rangeless spell make the range positive so that - // fire_tracer() will fill out path_taken. - if (pbolt.range == 0 && pbolt.target != monster->pos()) - tracer.range = ENV_SHOW_DIAMETER; - - fire_tracer(monster, tracer); - } - - std::string targ_prep = "at"; - std::string target = "nothing"; - - if (!targeted) - target = "NO TARGET"; - else if (pbolt.target == you.pos()) - target = "you"; - else if (pbolt.target == monster->pos()) - target = monster->pronoun(PRONOUN_REFLEXIVE); - // Monsters should only use targeted spells while foe == MHITNOT - // if they're targetting themselves. - else if (monster->foe == MHITNOT && !monster->confused()) - target = "NONEXISTENT FOE"; - else if (!invalid_monster_index(monster->foe) - && menv[monster->foe].type == MONS_NO_MONSTER) - { - target = "DEAD FOE"; - } - else if (in_bounds(pbolt.target) && see_cell(pbolt.target)) - { - if (const monsters* mtarg = monster_at(pbolt.target)) - { - if (you.can_see(mtarg)) - target = mtarg->name(DESC_NOCAP_THE); - } - } - - // Monster might be aiming past the real target, or maybe some fuzz has - // been applied because the target is invisible. - if (target == "nothing" && targeted) - { - if (pbolt.aimed_at_spot) - { - int count = 0; - for (adjacent_iterator ai(pbolt.target); ai; ++ai) - { - const actor* act = actor_at(*ai); - if (act && act != monster && you.can_see(act)) - { - targ_prep = "next to"; - - if (act->atype() == ACT_PLAYER || one_chance_in(++count)) - target = act->name(DESC_NOCAP_THE); - - if (act->atype() == ACT_PLAYER) - break; - } - } - } - - const bool visible_path = visible_beam || gestured; - bool mons_targ_aligned = false; - - const std::vector &path = tracer.path_taken; - for (unsigned int i = 0; i < path.size(); i++) - { - const coord_def pos = path[i]; - - if (pos == monster->pos()) - continue; - - const monsters *m = monster_at(pos); - if (pos == you.pos()) - { - // Be egotistical and assume that the monster is aiming at - // the player, rather than the player being in the path of - // a beam aimed at an ally. - if (!mons_wont_attack(monster)) - { - targ_prep = "at"; - target = "you"; - break; - } - // If the ally is confused or aiming at an invisible enemy, - // with the player in the path, act like it's targeted at - // the player if there isn't any visible target earlier - // in the path. - else if (target == "nothing") - { - targ_prep = "at"; - target = "you"; - mons_targ_aligned = true; - } - } - else if (visible_path && m && you.can_see(m)) - { - bool is_aligned = mons_aligned(m->mindex(), monster->mindex()); - std::string name = m->name(DESC_NOCAP_THE); - - if (target == "nothing") - { - mons_targ_aligned = is_aligned; - target = name; - } - // If the first target was aligned with the beam source then - // the first subsequent non-aligned monster in the path will - // take it's place. - else if (mons_targ_aligned && !is_aligned) - { - mons_targ_aligned = false; - target = name; - } - targ_prep = "at"; - } - else if (visible_path && target == "nothing") - { - int count = 0; - for (adjacent_iterator ai(pbolt.target); ai; ++ai) - { - const actor* act = monster_at(*ai); - if (act && act != monster && you.can_see(act)) - { - targ_prep = "past"; - if (act->atype() == ACT_PLAYER - || one_chance_in(++count)) - { - target = act->name(DESC_NOCAP_THE); - } - - if (act->atype() == ACT_PLAYER) - break; - } - } - } - } // for (unsigned int i = 0; i < path.size(); i++) - } // if (target == "nothing" && targeted) - - const actor* foe = monster->get_foe(); - - // If we still can't find what appears to be the target, and the - // monster isn't just throwing the spell in a random direction, - // we should be able to tell what the monster was aiming for if - // we can see the monster's foe and the beam (or the beam path - // implied by gesturing). But only if the beam didn't actually hit - // anything (but if it did hit something, why didn't that monster - // show up in the beam's path?) - if (targeted - && target == "nothing" - && (tracer.foe_info.count + tracer.friend_info.count) == 0 - && foe != NULL - && you.can_see(foe) - && !monster->confused() - && (visible_beam || gestured)) - { - target = foe->name(DESC_NOCAP_THE); - targ_prep = (pbolt.aimed_at_spot ? "next to" : "past"); - } - - // If the monster gestures to create an invisible beam then - // assume that anything close to the beam is the intended target. - // Also, if the monster gestures to create a visible beam but it - // misses still say that the monster gestured "at" the target, - // rather than "past". - if (gestured || target == "nothing") - targ_prep = "at"; - - msg = replace_all(msg, "@at@", targ_prep); - msg = replace_all(msg, "@target@", target); - - std::string beam_name; - if (!targeted) - beam_name = "NON TARGETED BEAM"; - else if (pbolt.name.empty()) - beam_name = "INVALID BEAM"; - else if (!tracer.seen) - beam_name = "UNSEEN BEAM"; - else - beam_name = pbolt.get_short_name(); - - msg = replace_all(msg, "@beam@", beam_name); - - const msg_channel_type chan = - (unseen ? MSGCH_SOUND : - mons_friendly_real(monster) ? MSGCH_FRIEND_SPELL - : MSGCH_MONSTER_SPELL); - - if (silent) - mons_speaks_msg(monster, msg, chan, true); - else if (noisy(noise, monster->pos(), monster->mindex()) || !unseen) - { - // noisy() returns true if the player heard the noise. - mons_speaks_msg(monster, msg, chan); - } -} - -// Set up bolt structure for monster spell casting. -void setup_mons_cast(monsters *monster, bolt &pbolt, - spell_type spell_cast) -{ - // always set these -- used by things other than fire_beam() - - // [ds] Used to be 12 * MHD and later buggily forced to -1 downstairs. - // Setting this to a more realistic number now that that bug is - // squashed. - pbolt.ench_power = 4 * monster->hit_dice; - - if (spell_cast == SPELL_TELEPORT_SELF) - pbolt.ench_power = 2000; - - pbolt.beam_source = monster_index(monster); - - // Convenience for the hapless innocent who assumes that this - // damn function does all possible setup. [ds] - if (pbolt.target.origin()) - pbolt.target = monster->target; - - // Set bolt type and range. - if (_los_free_spell(spell_cast)) - { - pbolt.range = 0; - switch (spell_cast) - { - case SPELL_BRAIN_FEED: - pbolt.type = DMNBM_BRAIN_FEED; - return; - case SPELL_SMITING: - case SPELL_AIRSTRIKE: - pbolt.type = DMNBM_SMITING; - return; - default: - // Other spells get normal setup: - break; - } - } - - // The below are no-ops since they don't involve direct_effect, - // fire_tracer, or beam. - switch (spell_cast) - { - case SPELL_SUMMON_SMALL_MAMMALS: - case SPELL_MAJOR_HEALING: - case SPELL_VAMPIRE_SUMMON: - case SPELL_SHADOW_CREATURES: // summon anything appropriate for level - case SPELL_FAKE_RAKSHASA_SUMMON: - case SPELL_SUMMON_DEMON: - case SPELL_SUMMON_UGLY_THING: - case SPELL_ANIMATE_DEAD: - case SPELL_CALL_IMP: - case SPELL_SUMMON_SCORPIONS: - case SPELL_SUMMON_UFETUBUS: - case SPELL_SUMMON_BEAST: // Geryon - case SPELL_SUMMON_UNDEAD: // summon undead around player - case SPELL_SUMMON_ICE_BEAST: - case SPELL_SUMMON_MUSHROOMS: - case SPELL_CONJURE_BALL_LIGHTNING: - case SPELL_SUMMON_DRAKES: - case SPELL_SUMMON_HORRIBLE_THINGS: - case SPELL_HAUNT: - case SPELL_SYMBOL_OF_TORMENT: - case SPELL_SUMMON_GREATER_DEMON: - case SPELL_CANTRIP: - case SPELL_BERSERKER_RAGE: - case SPELL_WATER_ELEMENTALS: - case SPELL_KRAKEN_TENTACLES: - case SPELL_BLINK: - case SPELL_CONTROLLED_BLINK: - case SPELL_TOMB_OF_DOROKLOHE: - return; - default: - break; - } - - // Need to correct this for power of spellcaster - int power = 12 * monster->hit_dice; - - bolt theBeam = mons_spells(monster, spell_cast, power); - - pbolt.colour = theBeam.colour; - pbolt.range = theBeam.range; - pbolt.hit = theBeam.hit; - pbolt.damage = theBeam.damage; - - if (theBeam.ench_power != -1) - pbolt.ench_power = theBeam.ench_power; - - pbolt.type = theBeam.type; - pbolt.flavour = theBeam.flavour; - pbolt.thrower = theBeam.thrower; - pbolt.name = theBeam.name; - pbolt.short_name = theBeam.short_name; - pbolt.is_beam = theBeam.is_beam; - pbolt.source = monster->pos(); - pbolt.is_tracer = false; - pbolt.is_explosion = theBeam.is_explosion; - pbolt.ex_size = theBeam.ex_size; - - pbolt.foe_ratio = theBeam.foe_ratio; - - if (!pbolt.is_enchantment()) - pbolt.aux_source = pbolt.name; - else - pbolt.aux_source.clear(); - - if (spell_cast == SPELL_HASTE - || spell_cast == SPELL_INVISIBILITY - || spell_cast == SPELL_MINOR_HEALING - || spell_cast == SPELL_TELEPORT_SELF) - { - pbolt.target = monster->pos(); - } - else if (spell_cast == SPELL_PORKALATOR && one_chance_in(3)) - { - int target = -1; - int count = 0; - monster_type hog_type = MONS_HOG; - for (int i = 0; i < MAX_MONSTERS; i++) - { - monsters *targ = &menv[i]; - - if (!monster->can_see(targ)) - continue; - - hog_type = MONS_HOG; - if (targ->holiness() == MH_DEMONIC) - hog_type = MONS_HELL_HOG; - else if (targ->holiness() != MH_NATURAL) - continue; - - if (targ->type != hog_type - && mons_atts_aligned(monster->attitude, targ->attitude) - && mons_power(hog_type) + random2(4) >= mons_power(targ->type) - && (!mons_class_flag(targ->type, M_SPELLCASTER) || coinflip()) - && one_chance_in(++count)) - { - target = i; - } - } - - if (target != -1) - { - monsters *targ = &menv[target]; - pbolt.target = targ->pos(); -#if DEBUG_DIAGNOSTICS - mprf("Porkalator: targetting %s instead", - targ->name(DESC_PLAIN).c_str()); -#endif - monster_polymorph(targ, hog_type); - } - // else target remains as specified - } -} - -bool monster_random_space(const monsters *monster, coord_def& target, - bool forbid_sanctuary) -{ - int tries = 0; - while (tries++ < 1000) - { - target = random_in_bounds(); - - // Don't land on top of another monster. - if (actor_at(target)) - continue; - - if (is_sanctuary(target) && forbid_sanctuary) - continue; - - if (monster_habitable_grid(monster, grd(target))) - return (true); - } - - return (false); -} - -bool monster_random_space(monster_type mon, coord_def& target, - bool forbid_sanctuary) -{ - monsters dummy; - dummy.type = mon; - - return monster_random_space(&dummy, target, forbid_sanctuary); -} - -void monster_teleport(monsters *monster, bool instan, bool silent) -{ - if (!instan) - { - if (monster->del_ench(ENCH_TP)) - { - if (!silent) - simple_monster_message(monster, " seems more stable."); - } - else - { - if (!silent) - simple_monster_message(monster, " looks slightly unstable."); - - monster->add_ench( mon_enchant(ENCH_TP, 0, KC_OTHER, - random_range(20, 30)) ); - } - - return; - } - - bool was_seen = you.can_see(monster) && !mons_is_lurking(monster); - - if (!silent) - simple_monster_message(monster, " disappears!"); - - const coord_def oldplace = monster->pos(); - - // Pick the monster up. - mgrd(oldplace) = NON_MONSTER; - - coord_def newpos; - if (monster_random_space(monster, newpos, !mons_wont_attack(monster))) - monster->moveto(newpos); - - mgrd(monster->pos()) = monster_index(monster); - - // Mimics change form/colour when teleported. - if (mons_is_mimic(monster->type)) - { - monster_type old_type = monster->type; - monster->type = static_cast( - MONS_GOLD_MIMIC + random2(5)); - monster->colour = get_mimic_colour(monster); - - // If it's changed form, you won't recognise it. - // This assumes that a non-gold mimic turning into another item of - // the same description is really, really unlikely. - if (old_type != MONS_GOLD_MIMIC || monster->type != MONS_GOLD_MIMIC) - was_seen = false; - } - - const bool now_visible = mons_near(monster); - if (!silent && now_visible) - { - if (was_seen) - simple_monster_message(monster, " reappears nearby!"); - else - { - // Even if it doesn't interrupt an activity (the player isn't - // delayed, the monster isn't hostile) we still want to give - // a message. - activity_interrupt_data ai(monster, "thin air"); - if (!interrupt_activity(AI_SEE_MONSTER, ai)) - simple_monster_message(monster, " appears out of thin air!"); - } - } - - if (monster->visible_to(&you) && now_visible) - handle_seen_interrupt(monster); - - // Leave a purple cloud. - place_cloud(CLOUD_PURP_SMOKE, oldplace, 1 + random2(3), - monster->kill_alignment()); - - monster->check_redraw(oldplace); - monster->apply_location_effects(oldplace); - - mons_relocated(monster); - - // Teleporting mimics change form - if they reappear out of LOS, they are - // no longer known. - if (mons_is_mimic(monster->type)) - { - if (now_visible) - monster->flags |= MF_KNOWN_MIMIC; - else - monster->flags &= ~MF_KNOWN_MIMIC; - } -} - -void setup_generic_throw(struct monsters *monster, struct bolt &pbolt) -{ - // FIXME we should use a sensible range here - pbolt.range = LOS_RADIUS; - pbolt.beam_source = monster_index(monster); - - pbolt.type = dchar_glyph(DCHAR_FIRED_MISSILE); - pbolt.flavour = BEAM_MISSILE; - pbolt.thrower = KILL_MON_MISSILE; - pbolt.aux_source.clear(); - pbolt.is_beam = false; -} - -bool mons_throw(struct monsters *monster, struct bolt &pbolt, int hand_used) -{ - std::string ammo_name; - - bool returning = false; - - int baseHit = 0, baseDam = 0; // from thrown or ammo - int ammoHitBonus = 0, ammoDamBonus = 0; // from thrown or ammo - int lnchHitBonus = 0, lnchDamBonus = 0; // special add from launcher - int exHitBonus = 0, exDamBonus = 0; // 'extra' bonus from skill/dex/str - int lnchBaseDam = 0; - - int hitMult = 0; - int damMult = 0; - int diceMult = 100; - - // Some initial convenience & initializations. - int wepClass = mitm[hand_used].base_type; - int wepType = mitm[hand_used].sub_type; - - int weapon = monster->inv[MSLOT_WEAPON]; - int lnchType = (weapon != NON_ITEM) ? mitm[weapon].sub_type : 0; - - mon_inv_type slot = get_mon_equip_slot(monster, mitm[hand_used]); - ASSERT(slot != NUM_MONSTER_SLOTS); - - const bool skilled = mons_class_flag(monster->type, M_FIGHTER); - - monster->lose_energy(EUT_MISSILE); - const int throw_energy = monster->action_energy(EUT_MISSILE); - - // Dropping item copy, since the launched item might be different. - item_def item = mitm[hand_used]; - item.quantity = 1; - if (mons_friendly(monster)) - item.flags |= ISFLAG_DROPPED_BY_ALLY; - - // FIXME we should actually determine a sensible range here - pbolt.range = LOS_RADIUS; - - if (setup_missile_beam(monster, pbolt, item, ammo_name, returning)) - return (false); - - pbolt.aimed_at_spot = returning; - - const launch_retval projected = - is_launched(monster, monster->mslot_item(MSLOT_WEAPON), - mitm[hand_used]); - - // extract launcher bonuses due to magic - if (projected == LRET_LAUNCHED) - { - lnchHitBonus = mitm[weapon].plus; - lnchDamBonus = mitm[weapon].plus2; - lnchBaseDam = property(mitm[weapon], PWPN_DAMAGE); - } - - // extract weapon/ammo bonuses due to magic - ammoHitBonus = item.plus; - ammoDamBonus = item.plus2; - - // Archers get a boost from their melee attack. - if (mons_class_flag(monster->type, M_ARCHER)) - { - const mon_attack_def attk = mons_attack_spec(monster, 0); - if (attk.type == AT_SHOOT) - ammoDamBonus += random2avg(attk.damage, 2); - } - - if (projected == LRET_THROWN) - { - // Darts are easy. - if (wepClass == OBJ_MISSILES && wepType == MI_DART) - { - baseHit = 11; - hitMult = 40; - damMult = 25; - } - else - { - baseHit = 6; - hitMult = 30; - damMult = 25; - } - - baseDam = property(item, PWPN_DAMAGE); - - if (wepClass == OBJ_MISSILES) // throw missile - { - // ammo damage needs adjusting here - OBJ_MISSILES - // don't get separate tohit/damage bonuses! - ammoDamBonus = ammoHitBonus; - - // [dshaligram] Thrown stones/darts do only half the damage of - // launched stones/darts. This matches 4.0 behaviour. - if (wepType == MI_DART || wepType == MI_STONE - || wepType == MI_SLING_BULLET) - { - baseDam = div_rand_round(baseDam, 2); - } - } - - // give monster "skill" bonuses based on HD - exHitBonus = (hitMult * monster->hit_dice) / 10 + 1; - exDamBonus = (damMult * monster->hit_dice) / 10 + 1; - } - - // Monsters no longer gain unfair advantages with weapons of - // fire/ice and incorrect ammo. They now have the same restrictions - // as players. - - int bow_brand = SPWPN_NORMAL; - const int ammo_brand = get_ammo_brand(item); - - if (projected == LRET_LAUNCHED) - { - bow_brand = get_weapon_brand(mitm[monster->inv[MSLOT_WEAPON]]); - - switch (lnchType) - { - case WPN_BLOWGUN: - baseHit = 12; - hitMult = 60; - damMult = 0; - lnchDamBonus = 0; - break; - case WPN_BOW: - case WPN_LONGBOW: - baseHit = 0; - hitMult = 60; - damMult = 35; - // monsters get half the launcher damage bonus, - // which is about as fair as I can figure it. - lnchDamBonus = (lnchDamBonus + 1) / 2; - break; - case WPN_CROSSBOW: - baseHit = 4; - hitMult = 70; - damMult = 30; - break; - case WPN_HAND_CROSSBOW: - baseHit = 2; - hitMult = 50; - damMult = 20; - break; - case WPN_SLING: - baseHit = 10; - hitMult = 40; - damMult = 20; - // monsters get half the launcher damage bonus, - // which is about as fair as I can figure it. - lnchDamBonus /= 2; - break; - } - - // Launcher is now more important than ammo for base damage. - baseDam = property(item, PWPN_DAMAGE); - if (lnchBaseDam) - baseDam = lnchBaseDam + random2(1 + baseDam); - - // missiles don't have pluses2; use hit bonus - ammoDamBonus = ammoHitBonus; - - exHitBonus = (hitMult * monster->hit_dice) / 10 + 1; - exDamBonus = (damMult * monster->hit_dice) / 10 + 1; - - if (!baseDam && elemental_missile_beam(bow_brand, ammo_brand)) - baseDam = 4; - - // [dshaligram] This is a horrible hack - we force beam.cc to - // consider this beam "needle-like". - if (wepClass == OBJ_MISSILES && wepType == MI_NEEDLE) - pbolt.ench_power = AUTOMATIC_HIT; - - // elven bow w/ elven arrow, also orcish - if (get_equip_race(mitm[monster->inv[MSLOT_WEAPON]]) - == get_equip_race(mitm[monster->inv[MSLOT_MISSILE]])) - { - baseHit++; - baseDam++; - - if (get_equip_race(mitm[monster->inv[MSLOT_WEAPON]]) == ISFLAG_ELVEN) - pbolt.hit++; - } - - // POISON brand launchers poison ammo - if (bow_brand == SPWPN_VENOM && ammo_brand == SPMSL_NORMAL) - set_item_ego_type(item, OBJ_MISSILES, SPMSL_POISONED); - - // Vorpal brand increases damage dice size. - if (bow_brand == SPWPN_VORPAL) - diceMult = diceMult * 130 / 100; - - // As do steel ammo. - if (ammo_brand == SPMSL_STEEL) - diceMult = diceMult * 150 / 100; - - // Note: we already have throw_energy taken off. -- bwr - int speed_delta = 0; - if (lnchType == WPN_CROSSBOW) - { - if (bow_brand == SPWPN_SPEED) - { - // Speed crossbows take 50% less time to use than - // ordinary crossbows. - speed_delta = div_rand_round(throw_energy * 2, 5); - } - else - { - // Ordinary crossbows take 20% more time to use - // than ordinary bows. - speed_delta = -div_rand_round(throw_energy, 5); - } - } - else if (bow_brand == SPWPN_SPEED) - { - // Speed bows take 50% less time to use than - // ordinary bows. - speed_delta = div_rand_round(throw_energy, 2); - } - - monster->speed_increment += speed_delta; - } - - // Chaos overides flame and frost - if (pbolt.flavour != BEAM_MISSILE) - { - baseHit += 2; - exDamBonus += 6; - } - - // monster intelligence bonus - if (mons_intel(monster) == I_HIGH) - exHitBonus += 10; - - // Now, if a monster is, for some reason, throwing something really - // stupid, it will have baseHit of 0 and damage of 0. Ah well. - std::string msg = monster->name(DESC_CAP_THE); - msg += ((projected == LRET_LAUNCHED) ? " shoots " : " throws "); - - if (!pbolt.name.empty() && projected == LRET_LAUNCHED) - msg += article_a(pbolt.name); - else - { - // build shoot message - msg += item.name(DESC_NOCAP_A); - - // build beam name - pbolt.name = item.name(DESC_PLAIN, false, false, false); - } - msg += "."; - - if (monster->observable()) - { - mpr(msg.c_str()); - - if (projected == LRET_LAUNCHED - && item_type_known(mitm[monster->inv[MSLOT_WEAPON]]) - || projected == LRET_THROWN - && mitm[hand_used].base_type == OBJ_MISSILES) - { - set_ident_flags(mitm[hand_used], ISFLAG_KNOW_TYPE); - } - } - - // [dshaligram] When changing bolt names here, you must edit - // hiscores.cc (scorefile_entry::terse_missile_cause()) to match. - char throw_buff[ITEMNAME_SIZE]; - if (projected == LRET_LAUNCHED) - { - snprintf(throw_buff, sizeof(throw_buff), "Shot with a%s %s by %s", - (is_vowel(pbolt.name[0]) ? "n" : ""), pbolt.name.c_str(), - monster->name(DESC_NOCAP_A).c_str()); - } - else - { - snprintf(throw_buff, sizeof(throw_buff), "Hit by a%s %s thrown by %s", - (is_vowel(pbolt.name[0]) ? "n" : ""), pbolt.name.c_str(), - monster->name(DESC_NOCAP_A).c_str()); - } - - pbolt.aux_source = throw_buff; - - // Add everything up. - pbolt.hit = baseHit + random2avg(exHitBonus, 2) + ammoHitBonus; - pbolt.damage = - dice_def(1, baseDam + random2avg(exDamBonus, 2) + ammoDamBonus); - - if (projected == LRET_LAUNCHED) - { - pbolt.damage.size += lnchDamBonus; - pbolt.hit += lnchHitBonus; - } - pbolt.damage.size = diceMult * pbolt.damage.size / 100; - - if (monster->has_ench(ENCH_BATTLE_FRENZY)) - { - const mon_enchant ench = monster->get_ench(ENCH_BATTLE_FRENZY); - -#ifdef DEBUG_DIAGNOSTICS - const dice_def orig_damage = pbolt.damage; -#endif - - pbolt.damage.size = pbolt.damage.size * (115 + ench.degree * 15) / 100; - -#ifdef DEBUG_DIAGNOSTICS - mprf(MSGCH_DIAGNOSTICS, "%s frenzy damage: %dd%d -> %dd%d", - monster->name(DESC_PLAIN).c_str(), - orig_damage.num, orig_damage.size, - pbolt.damage.num, pbolt.damage.size); -#endif - } - - // Skilled archers get better to-hit and damage. - if (skilled) - { - pbolt.hit = pbolt.hit * 120 / 100; - pbolt.damage.size = pbolt.damage.size * 120 / 100; - } - - scale_dice(pbolt.damage); - - // decrease inventory - bool really_returns; - if (returning && !one_chance_in(mons_power(monster->type) + 3)) - really_returns = true; - else - really_returns = false; - - pbolt.drop_item = !really_returns; - - // Redraw the screen before firing, in case the monster just - // came into view and the screen hasn't been updated yet. - viewwindow(true, false); - pbolt.fire(); - - // The item can be destroyed before returning. - if (really_returns && thrown_object_destroyed(&item, pbolt.target, true)) - { - really_returns = false; - } - - if (really_returns) - { - // Fire beam in reverse. - pbolt.setup_retrace(); - viewwindow(true, false); - pbolt.fire(); - msg::stream << "The weapon returns " - << (you.can_see(monster)? - ("to " + monster->name(DESC_NOCAP_THE)) - : "whence it came from") - << "!" << std::endl; - - // Player saw the item return. - if (!is_artefact(item)) - { - // Since this only happens for non-artefacts, also mark properties - // as known. - set_ident_flags(mitm[hand_used], - ISFLAG_KNOW_TYPE | ISFLAG_KNOW_PROPERTIES); - } - } - else if (dec_mitm_item_quantity(hand_used, 1)) - monster->inv[returning ? slot : MSLOT_MISSILE] = NON_ITEM; - - if (pbolt.special_explosion != NULL) - delete pbolt.special_explosion; - - return (true); -} - -static void _scale_draconian_breath(bolt& beam, int drac_type) -{ - int scaling = 100; - switch (drac_type) - { - case MONS_RED_DRACONIAN: - beam.name = "searing blast"; - beam.aux_source = "blast of searing breath"; - scaling = 65; - break; - - case MONS_WHITE_DRACONIAN: - beam.name = "chilling blast"; - beam.aux_source = "blast of chilling breath"; - beam.short_name = "frost"; - scaling = 65; - break; - - case MONS_PLAYER_GHOST: // draconians only - beam.name = "blast of negative energy"; - beam.aux_source = "blast of draining breath"; - beam.flavour = BEAM_NEG; - beam.colour = DARKGREY; - scaling = 65; - break; - } - beam.damage.size = scaling * beam.damage.size / 100; -} - -static spell_type _draco_type_to_breath(int drac_type) -{ - switch (drac_type) - { - case MONS_BLACK_DRACONIAN: return SPELL_LIGHTNING_BOLT; - case MONS_MOTTLED_DRACONIAN: return SPELL_STICKY_FLAME_SPLASH; - case MONS_YELLOW_DRACONIAN: return SPELL_ACID_SPLASH; - case MONS_GREEN_DRACONIAN: return SPELL_POISONOUS_CLOUD; - case MONS_PURPLE_DRACONIAN: return SPELL_ISKENDERUNS_MYSTIC_BLAST; - case MONS_RED_DRACONIAN: return SPELL_FIRE_BREATH; - case MONS_WHITE_DRACONIAN: return SPELL_COLD_BREATH; - case MONS_PALE_DRACONIAN: return SPELL_STEAM_BALL; - - // Handled later. - case MONS_PLAYER_GHOST: return SPELL_DRACONIAN_BREATH; - - default: - DEBUGSTR("Invalid monster using draconian breath spell"); - break; - } - - return (SPELL_DRACONIAN_BREATH); -} - - -bolt mons_spells( monsters *mons, spell_type spell_cast, int power ) -{ - ASSERT(power > 0); - - bolt beam; - - // Initialise to some bogus values so we can catch problems. - beam.name = "****"; - beam.colour = 1000; - beam.hit = -1; - beam.damage = dice_def( 1, 0 ); - beam.ench_power = -1; - beam.type = 0; - beam.flavour = BEAM_NONE; - beam.thrower = KILL_MISC; - beam.is_beam = false; - beam.is_explosion = false; - - // Sandblast is different, and gets range updated later - if (spell_cast != SPELL_SANDBLAST) - beam.range = spell_range(spell_cast, power, true, false); - - const int drac_type = (mons_genus(mons->type) == MONS_DRACONIAN) - ? draco_subspecies(mons) : mons->type; - - spell_type real_spell = spell_cast; - - if (spell_cast == SPELL_DRACONIAN_BREATH) - real_spell = _draco_type_to_breath(drac_type); - - beam.type = dchar_glyph(DCHAR_FIRED_ZAP); // default - beam.thrower = KILL_MON_MISSILE; - - // FIXME: this should use the zap_data[] struct from beam.cc! - switch (real_spell) - { - case SPELL_MAGIC_DART: - beam.colour = LIGHTMAGENTA; - beam.name = "magic dart"; - beam.damage = dice_def( 3, 4 + (power / 100) ); - beam.hit = AUTOMATIC_HIT; - beam.flavour = BEAM_MMISSILE; - break; - - case SPELL_THROW_FLAME: - beam.colour = RED; - beam.name = "puff of flame"; - beam.damage = dice_def( 3, 5 + (power / 40) ); - beam.hit = 25 + power / 40; - beam.flavour = BEAM_FIRE; - break; - - case SPELL_THROW_FROST: - beam.colour = WHITE; - beam.name = "puff of frost"; - beam.damage = dice_def( 3, 5 + (power / 40) ); - beam.hit = 25 + power / 40; - beam.flavour = BEAM_COLD; - break; - - case SPELL_SANDBLAST: - beam.colour = BROWN; - beam.name = "rocky blast"; - beam.damage = dice_def( 3, 5 + (power / 40) ); - beam.hit = 20 + power / 40; - beam.flavour = BEAM_FRAG; - beam.range = 2; // spell_range() is wrong here - break; - - case SPELL_DISPEL_UNDEAD: - beam.flavour = BEAM_DISPEL_UNDEAD; - beam.damage = dice_def( 3, std::min(6 + power / 10, 40) ); - beam.is_beam = true; - break; - - case SPELL_PARALYSE: - beam.flavour = BEAM_PARALYSIS; - beam.is_beam = true; - break; - - case SPELL_SLOW: - beam.flavour = BEAM_SLOW; - beam.is_beam = true; - break; - - case SPELL_HASTE: // (self) - beam.flavour = BEAM_HASTE; - break; - - case SPELL_BACKLIGHT: - beam.flavour = BEAM_BACKLIGHT; - beam.is_beam = true; - break; - - case SPELL_CONFUSE: - beam.flavour = BEAM_CONFUSION; - beam.is_beam = true; - break; - - case SPELL_SLEEP: - beam.flavour = BEAM_SLEEP; - beam.is_beam = true; - break; - - case SPELL_POLYMORPH_OTHER: - beam.flavour = BEAM_POLYMORPH; - beam.is_beam = true; - // Be careful with this one. - // Having allies mutate you is infuriating. - beam.foe_ratio = 1000; - break; - - case SPELL_VENOM_BOLT: - beam.name = "bolt of poison"; - beam.damage = dice_def( 3, 6 + power / 13 ); - beam.colour = LIGHTGREEN; - beam.flavour = BEAM_POISON; - beam.hit = 19 + power / 20; - beam.is_beam = true; - break; - - case SPELL_POISON_ARROW: - beam.name = "poison arrow"; - beam.damage = dice_def( 3, 7 + power / 12 ); - beam.colour = LIGHTGREEN; - beam.type = dchar_glyph(DCHAR_FIRED_MISSILE); - beam.flavour = BEAM_POISON_ARROW; - beam.hit = 20 + power / 25; - break; - - case SPELL_BOLT_OF_MAGMA: - beam.name = "bolt of magma"; - beam.damage = dice_def( 3, 8 + power / 11 ); - beam.colour = RED; - beam.flavour = BEAM_LAVA; - beam.hit = 17 + power / 25; - beam.is_beam = true; - break; - - case SPELL_BOLT_OF_FIRE: - beam.name = "bolt of fire"; - beam.damage = dice_def( 3, 8 + power / 11 ); - beam.colour = RED; - beam.flavour = BEAM_FIRE; - beam.hit = 17 + power / 25; - beam.is_beam = true; - break; - - case SPELL_FLING_ICICLE: - beam.name = "shard of ice"; - beam.damage = dice_def( 3, 8 + power / 11 ); - beam.colour = WHITE; - beam.flavour = BEAM_ICE; - beam.hit = 17 + power / 25; - beam.is_beam = true; - break; - - case SPELL_BOLT_OF_COLD: - beam.name = "bolt of cold"; - beam.damage = dice_def( 3, 8 + power / 11 ); - beam.colour = WHITE; - beam.flavour = BEAM_COLD; - beam.hit = 17 + power / 25; - beam.is_beam = true; - break; - - case SPELL_FREEZING_CLOUD: - beam.name = "freezing blast"; - beam.damage = dice_def( 2, 9 + power / 11 ); - beam.colour = WHITE; - beam.flavour = BEAM_COLD; - beam.hit = 17 + power / 25; - beam.is_beam = true; - beam.is_big_cloud = true; - break; - - case SPELL_SHOCK: - beam.name = "zap"; - beam.damage = dice_def( 1, 8 + (power / 20) ); - beam.colour = LIGHTCYAN; - beam.flavour = BEAM_ELECTRICITY; - beam.hit = 17 + power / 20; - beam.is_beam = true; - break; - - case SPELL_LIGHTNING_BOLT: - beam.name = "bolt of lightning"; - beam.damage = dice_def( 3, 10 + power / 17 ); - beam.colour = LIGHTCYAN; - beam.flavour = BEAM_ELECTRICITY; - beam.hit = 16 + power / 40; - beam.is_beam = true; - break; - - case SPELL_INVISIBILITY: - beam.flavour = BEAM_INVISIBILITY; - break; - - case SPELL_FIREBALL: - beam.colour = RED; - beam.name = "fireball"; - beam.damage = dice_def( 3, 7 + power / 10 ); - beam.hit = 40; - beam.flavour = BEAM_FIRE; - beam.foe_ratio = 60; - beam.is_explosion = true; - break; - - case SPELL_FIRE_STORM: - setup_fire_storm(mons, power / 2, beam); - beam.foe_ratio = random_range(40, 55); - break; - - case SPELL_ICE_STORM: - beam.name = "great blast of cold"; - beam.colour = BLUE; - beam.damage = calc_dice( 10, 18 + power / 2 ); - beam.hit = 20 + power / 10; // 50: 25 100: 30 - beam.ench_power = power; // used for radius - beam.flavour = BEAM_ICE; // half resisted - beam.is_explosion = true; - beam.foe_ratio = random_range(40, 55); - break; - - case SPELL_HELLFIRE_BURST: - beam.aux_source = "burst of hellfire"; - beam.name = "burst of hellfire"; - beam.ex_size = 1; - beam.flavour = BEAM_HELLFIRE; - beam.is_explosion = true; - beam.colour = RED; - beam.aux_source.clear(); - beam.is_tracer = false; - beam.hit = 20; - beam.damage = mons_foe_is_mons(mons) ? dice_def(5, 7) - : dice_def(3, 20); - break; - - case SPELL_MINOR_HEALING: - beam.flavour = BEAM_HEALING; - beam.hit = 25 + (power / 5); - break; - - case SPELL_TELEPORT_SELF: - beam.flavour = BEAM_TELEPORT; - break; - - case SPELL_TELEPORT_OTHER: - beam.flavour = BEAM_TELEPORT; - beam.is_beam = true; - break; - - case SPELL_LEHUDIBS_CRYSTAL_SPEAR: // was splinters - beam.name = "crystal spear"; - beam.damage = dice_def( 3, 16 + power / 10 ); - beam.colour = WHITE; - beam.type = dchar_glyph(DCHAR_FIRED_MISSILE); - beam.flavour = BEAM_MMISSILE; - beam.hit = 22 + power / 20; - break; - - case SPELL_DIG: - beam.flavour = BEAM_DIGGING; - beam.is_beam = true; - break; - - case SPELL_BOLT_OF_DRAINING: // negative energy - beam.name = "bolt of negative energy"; - beam.damage = dice_def( 3, 6 + power / 13 ); - beam.colour = DARKGREY; - beam.flavour = BEAM_NEG; - beam.hit = 16 + power / 35; - beam.is_beam = true; - break; - - case SPELL_ISKENDERUNS_MYSTIC_BLAST: // mystic blast - beam.colour = LIGHTMAGENTA; - beam.name = "orb of energy"; - beam.short_name = "energy"; - beam.damage = dice_def( 3, 7 + (power / 14) ); - beam.hit = 20 + (power / 20); - beam.flavour = BEAM_MMISSILE; - break; - - case SPELL_STEAM_BALL: - beam.colour = LIGHTGREY; - beam.name = "ball of steam"; - beam.damage = dice_def( 3, 7 + (power / 15) ); - beam.hit = 20 + power / 20; - beam.flavour = BEAM_STEAM; - break; - - case SPELL_PAIN: - beam.flavour = BEAM_PAIN; - beam.damage = dice_def( 1, 7 + (power / 20) ); - beam.ench_power = std::max(50, 8 * mons->hit_dice); - beam.is_beam = true; - break; - - case SPELL_STICKY_FLAME_SPLASH: - case SPELL_STICKY_FLAME: - beam.colour = RED; - beam.name = "sticky flame"; - beam.damage = dice_def( 3, 3 + power / 50 ); - beam.hit = 18 + power / 15; - beam.flavour = BEAM_FIRE; - break; - - case SPELL_POISONOUS_CLOUD: - beam.name = "blast of poison"; - beam.damage = dice_def( 3, 3 + power / 25 ); - beam.colour = LIGHTGREEN; - beam.flavour = BEAM_POISON; - beam.hit = 18 + power / 25; - beam.is_beam = true; - beam.is_big_cloud = true; - break; - - case SPELL_ENERGY_BOLT: // eye of devastation - beam.colour = YELLOW; - beam.name = "bolt of energy"; - beam.short_name = "energy"; - beam.damage = dice_def( 3, 20 ); - beam.hit = 15 + power / 30; - beam.flavour = BEAM_NUKE; // a magical missile which destroys walls - beam.is_beam = true; - break; - - case SPELL_STING: // sting - beam.colour = GREEN; - beam.name = "sting"; - beam.damage = dice_def( 1, 6 + power / 25 ); - beam.hit = 60; - beam.flavour = BEAM_POISON; - break; - - case SPELL_IRON_SHOT: - beam.colour = LIGHTCYAN; - beam.name = "iron shot"; - beam.damage = dice_def( 3, 8 + (power / 9) ); - beam.hit = 20 + (power / 25); - beam.type = dchar_glyph(DCHAR_FIRED_MISSILE); - beam.flavour = BEAM_MMISSILE; // similarly unresisted thing - break; - - case SPELL_STONE_ARROW: - beam.colour = LIGHTGREY; - beam.name = "stone arrow"; - beam.damage = dice_def( 3, 5 + (power / 10) ); - beam.hit = 14 + power / 35; - beam.type = dchar_glyph(DCHAR_FIRED_MISSILE); - beam.flavour = BEAM_MMISSILE; // similarly unresisted thing - break; - - case SPELL_POISON_SPLASH: - beam.colour = GREEN; - beam.name = "splash of poison"; - beam.damage = dice_def( 1, 4 + power / 10 ); - beam.hit = 16 + power / 20; - beam.flavour = BEAM_POISON; - break; - - case SPELL_ACID_SPLASH: - beam.colour = YELLOW; - beam.name = "splash of acid"; - beam.damage = dice_def( 3, 7 ); - beam.hit = 20 + (3 * mons->hit_dice); - beam.flavour = BEAM_ACID; - break; - - case SPELL_DISINTEGRATE: - beam.flavour = BEAM_DISINTEGRATION; - beam.ench_power = 50; - beam.damage = dice_def( 1, 30 + (power / 10) ); - beam.is_beam = true; - break; - - case SPELL_MEPHITIC_CLOUD: // swamp drake, player ghost - beam.name = "foul vapour"; - beam.damage = dice_def(1,0); - beam.colour = GREEN; - // Well, it works, even if the name isn't quite intuitive. - beam.flavour = BEAM_POTION_STINKING_CLOUD; - beam.hit = 14 + power / 30; - beam.ench_power = power; // probably meaningless - beam.is_explosion = true; - beam.is_big_cloud = true; - break; - - case SPELL_MIASMA: // death drake - beam.name = "foul vapour"; - beam.damage = dice_def( 3, 5 + power / 24 ); - beam.colour = DARKGREY; - beam.flavour = BEAM_MIASMA; - beam.hit = 17 + power / 20; - beam.is_beam = true; - beam.is_big_cloud = true; - break; - - case SPELL_QUICKSILVER_BOLT: // Quicksilver dragon - beam.colour = random_colour(); - beam.name = "bolt of energy"; - beam.short_name = "energy"; - beam.damage = dice_def( 3, 25 ); - beam.hit = 16 + power / 25; - beam.flavour = BEAM_MMISSILE; - break; - - case SPELL_HELLFIRE: // fiend's hellfire - beam.name = "blast of hellfire"; - beam.aux_source = "blast of hellfire"; - beam.colour = RED; - beam.damage = dice_def( 3, 25 ); - beam.hit = 24; - beam.flavour = BEAM_HELLFIRE; - beam.is_beam = true; - beam.is_explosion = true; - break; - - case SPELL_METAL_SPLINTERS: - beam.name = "spray of metal splinters"; - beam.short_name = "metal splinters"; - beam.damage = dice_def( 3, 20 + power / 20 ); - beam.colour = CYAN; - beam.flavour = BEAM_FRAG; - beam.hit = 19 + power / 30; - beam.is_beam = true; - break; - - case SPELL_BANISHMENT: - beam.flavour = BEAM_BANISH; - beam.is_beam = true; - break; - - case SPELL_BLINK_OTHER: - beam.flavour = BEAM_BLINK; - beam.is_beam = true; - break; - - case SPELL_FIRE_BREATH: - beam.name = "blast of flame"; - beam.aux_source = "blast of fiery breath"; - beam.damage = dice_def( 3, (mons->hit_dice * 2) ); - beam.colour = RED; - beam.hit = 30; - beam.flavour = BEAM_FIRE; - beam.is_beam = true; - break; - - case SPELL_COLD_BREATH: - beam.name = "blast of cold"; - beam.aux_source = "blast of icy breath"; - beam.short_name = "frost"; - beam.damage = dice_def( 3, (mons->hit_dice * 2) ); - beam.colour = WHITE; - beam.hit = 30; - beam.flavour = BEAM_COLD; - beam.is_beam = true; - break; - - case SPELL_DRACONIAN_BREATH: - beam.damage = dice_def( 3, (mons->hit_dice * 2) ); - beam.hit = 30; - beam.is_beam = true; - break; - - case SPELL_PORKALATOR: - beam.name = "porkalator"; - beam.type = 0; - beam.flavour = BEAM_PORKALATOR; - beam.thrower = KILL_MON_MISSILE; - beam.is_beam = true; - break; - - default: - if (!is_valid_spell(real_spell)) - DEBUGSTR("Invalid spell #%d cast by %s", (int) real_spell, - mons->name(DESC_PLAIN, true).c_str()); - - DEBUGSTR("Unknown monster spell '%s' cast by %s", - spell_title(real_spell), - mons->name(DESC_PLAIN, true).c_str()); - - return (beam); - } - - if (beam.is_enchantment()) - { - beam.type = dchar_glyph(DCHAR_SPACE); - beam.name = "0"; - } - - if (spell_cast == SPELL_DRACONIAN_BREATH) - _scale_draconian_breath(beam, drac_type); - - // Accuracy is lowered by one quarter if the dragon is attacking - // a target that is wielding a weapon of dragon slaying (which - // makes the dragon/draconian avoid looking at the foe). - // FIXME: This effect is not yet implemented for player draconians - // or characters in dragon form breathing at monsters wielding a - // weapon with this brand. - if (is_dragonkind(mons)) - { - if (actor *foe = mons->get_foe()) - { - if (const item_def *weapon = foe->weapon()) - { - if (get_weapon_brand(*weapon) == SPWPN_DRAGON_SLAYING) - { - beam.hit *= 3; - beam.hit /= 4; - } - } - } - } - - return (beam); -} - -static int _monster_abjure_square(const coord_def &pos, - int pow, int actual, - int wont_attack) -{ - monsters *target = monster_at(pos); - if (target == NULL) - return (0); - - if (!target->alive() - || ((bool)wont_attack == mons_wont_attack_real(target))) - { - return (0); - } - - int duration; - - if (!target->is_summoned(&duration)) - return (0); - - pow = std::max(20, fuzz_value(pow, 40, 25)); - - if (!actual) - return (pow > 40 || pow >= duration); - - // TSO and Trog's abjuration protection. - bool shielded = false; - if (you.religion == GOD_SHINING_ONE) - { - pow = pow * (30 - target->hit_dice) / 30; - if (pow < duration) - { - simple_god_message(" protects your fellow warrior from evil " - "magic!"); - shielded = true; - } - } - else if (you.religion == GOD_TROG) - { - pow = pow * 4 / 5; - if (pow < duration) - { - simple_god_message(" shields your ally from puny magic!"); - shielded = true; - } - } - else if (is_sanctuary(target->pos())) - { - pow = 0; - mpr("Zin's power protects your fellow warrior from evil magic!", - MSGCH_GOD); - shielded = true; - } - -#ifdef DEBUG_DIAGNOSTICS - mprf(MSGCH_DIAGNOSTICS, "Abj: dur: %d, pow: %d, ndur: %d", - duration, pow, duration - pow); -#endif - - mon_enchant abj = target->get_ench(ENCH_ABJ); - if (!target->lose_ench_duration(abj, pow)) - { - if (!shielded) - simple_monster_message(target, " shudders."); - return (1); - } - - return (0); -} - -static int _apply_radius_around_square( const coord_def &c, int radius, - int (*fn)(const coord_def &, int, int, int), - int pow, int par1, int par2) -{ - int res = 0; - for (int yi = -radius; yi <= radius; ++yi) - { - const coord_def c1(c.x - radius, c.y + yi); - const coord_def c2(c.x + radius, c.y + yi); - if (in_bounds(c1)) - res += fn(c1, pow, par1, par2); - if (in_bounds(c2)) - res += fn(c2, pow, par1, par2); - } - - for (int xi = -radius + 1; xi < radius; ++xi) - { - const coord_def c1(c.x + xi, c.y - radius); - const coord_def c2(c.x + xi, c.y + radius); - if (in_bounds(c1)) - res += fn(c1, pow, par1, par2); - if (in_bounds(c2)) - res += fn(c2, pow, par1, par2); - } - return (res); -} - -static int _monster_abjuration(const monsters *caster, bool actual) -{ - const bool wont_attack = mons_wont_attack_real(caster); - int maffected = 0; - - if (actual) - mpr("Send 'em back where they came from!"); - - int pow = std::min(caster->hit_dice * 90, 2500); - - // Abjure radius. - for (int rad = 1; rad < 5 && pow >= 30; ++rad) - { - int number_hit = - _apply_radius_around_square(caster->pos(), rad, - _monster_abjure_square, - pow, actual, wont_attack); - - maffected += number_hit; - - // Each affected monster drops power. - // - // We could further tune this by the actual amount of abjuration - // damage done to each summon, but the player will probably never - // notice. :-) - while (number_hit-- > 0) - pow = pow * 90 / 100; - - pow /= 2; - } - return (maffected); -} - -bool silver_statue_effects(monsters *mons) -{ - actor *foe = mons->get_foe(); - if (foe && mons->can_see(foe) && !one_chance_in(3)) - { - const std::string msg = - "'s eyes glow " + weird_glowing_colour() + '.'; - simple_monster_message(mons, msg.c_str(), MSGCH_WARN); - - create_monster( - mgen_data( - summon_any_demon((coinflip() ? DEMON_COMMON - : DEMON_LESSER)), - SAME_ATTITUDE(mons), 5, 0, foe->pos(), mons->foe)); - return (true); - } - return (false); -} - -bool orange_statue_effects(monsters *mons) -{ - actor *foe = mons->get_foe(); - if (foe && mons->can_see(foe) && !one_chance_in(3)) - { - if (you.can_see(foe)) - { - if (foe == &you) - mprf(MSGCH_WARN, "A hostile presence attacks your mind!"); - else if (you.can_see(mons)) - mprf(MSGCH_WARN, "%s fixes %s piercing gaze on %s.", - mons->name(DESC_CAP_THE).c_str(), - mons->pronoun(PRONOUN_NOCAP_POSSESSIVE).c_str(), - foe->name(DESC_NOCAP_THE).c_str()); - } - - MiscastEffect(foe, monster_index(mons), SPTYP_DIVINATION, - random2(15), random2(150), - "an orange crystal statue"); - return (true); - } - - return (false); -} - -bool ugly_thing_mutate(monsters *ugly, bool proximity) -{ - bool success = false; - - std::string src = ""; - - unsigned char mon_colour = BLACK; - - if (!proximity) - success = true; - else if (one_chance_in(8)) - { - int you_mutate_chance = 0; - int mon_mutate_chance = 0; - - for (adjacent_iterator ri(ugly->pos()); ri; ++ri) - { - if (you.pos() == *ri) - you_mutate_chance = get_contamination_level(); - else - { - monsters *ugly_near = monster_at(*ri); - - if (!ugly_near - || ugly_near->type != MONS_UGLY_THING - && ugly_near->type != MONS_VERY_UGLY_THING) - { - continue; - } - - for (int i = 0; i < 2; ++i) - { - if (coinflip()) - { - mon_mutate_chance++; - - if (coinflip()) - { - const int ugly_colour = - make_low_colour(ugly->colour); - const int ugly_near_colour = - make_low_colour(ugly_near->colour); - - if (ugly_colour != ugly_near_colour) - mon_colour = ugly_near_colour; - } - } - - if (ugly_near->type != MONS_VERY_UGLY_THING) - break; - } - } - } - - you_mutate_chance = std::min(16, you_mutate_chance); - mon_mutate_chance = std::min(16, mon_mutate_chance); - - if (!one_chance_in(you_mutate_chance + mon_mutate_chance + 1)) - { - const bool proximity_you = - (you_mutate_chance > mon_mutate_chance) ? true : - (you_mutate_chance == mon_mutate_chance) ? coinflip() - : false; - - src = proximity_you ? " from you" : " from its kin"; - - success = true; - } - } - - if (success) - { - simple_monster_message(ugly, - make_stringf(" basks in the mutagenic energy%s and changes!", - src.c_str()).c_str()); - - ugly->uglything_mutate(mon_colour); - - return (true); - } - - return (false); -} - -// Inflict any enchantments the parent slime has on its offspring, -// leaving durations unchanged, I guess. -cao -static void _split_ench_durations(monsters *initial_slime, monsters *split_off) -{ - mon_enchant_list::iterator i; - - for (i = initial_slime->enchantments.begin(); - i != initial_slime->enchantments.end(); ++i) - { - split_off->add_ench(i->second); - } - -} - -// What to do about any enchantments these two slimes may have? For -// now, we are averaging the durations. -cao -static void _merge_ench_durations(monsters *initial_slime, monsters *merge_to) -{ - mon_enchant_list::iterator i; - - int initial_count = initial_slime->number; - int merge_to_count = merge_to->number; - int total_count = initial_count + merge_to_count; - - for (i = initial_slime->enchantments.begin(); - i != initial_slime->enchantments.end(); ++i) - { - // Does the other slime have this enchantment as well? - mon_enchant temp = merge_to->get_ench(i->first); - // If not, use duration 0 for their part of the average. - int duration = temp.ench == ENCH_NONE ? 0 : temp.duration; - - i->second.duration = (i->second.duration * initial_count - + duration * merge_to_count)/total_count; - - if (!i->second.duration) - i->second.duration = 1; - - merge_to->add_ench(i->second); - } - - for (i = merge_to->enchantments.begin(); - i != merge_to->enchantments.end(); ++i) - { - if (initial_slime->enchantments.find(i->first) - != initial_slime->enchantments.end() - && i->second.duration > 1) - { - i->second.duration = (merge_to_count * i->second.duration) - / total_count; - - merge_to->update_ench(i->second); - } - } -} - -// Calculate slime creature hp based on how many are merged. -static void _stats_from_blob_count(monsters *slime, float hp_per_blob) -{ - slime->max_hit_points = (int)(slime->number * hp_per_blob); - slime->hit_points = slime->max_hit_points; -} - -// Create a new slime creature at 'target', and split 'thing''s hp and -// merge count with the new monster. -static bool _do_split(monsters *thing, coord_def & target) -{ - // Create a new slime. - int slime_idx = create_monster(mgen_data(MONS_SLIME_CREATURE, - thing->behaviour, - 0, - 0, - target, - thing->foe, - MG_FORCE_PLACE)); - - if (slime_idx == -1) - return (false); - - monsters *new_slime = &env.mons[slime_idx]; - - // Inflict the new slime with any enchantments on the parent. - _split_ench_durations(thing, new_slime); - new_slime->attitude = thing->attitude; - new_slime->flags = thing->flags; - - if (!new_slime) - return (false); - - if (you.can_see(thing)) - mprf("%s splits.", thing->name(DESC_CAP_A).c_str()); - - int split_off = thing->number / 2; - float hp_per_blob = thing->max_hit_points / float(thing->number); - - thing->number -= split_off; - new_slime->number = split_off; - - new_slime->hit_dice = thing->hit_dice; - - _stats_from_blob_count(thing, hp_per_blob); - _stats_from_blob_count(new_slime, hp_per_blob); - - return (true); -} - -// Actually merge two slime creature, pooling their hp, etc. -// initial_slime is the one that gets killed off by this process. -static bool _do_merge(monsters *initial_slime, monsters *merge_to) -{ - // Combine enchantment durations. - _merge_ench_durations(initial_slime, merge_to); - - merge_to->number += initial_slime->number; - merge_to->max_hit_points += initial_slime->max_hit_points; - merge_to->hit_points += initial_slime->max_hit_points; - - // Merge monster flags (mostly so that MF_CREATED_NEUTRAL, etc. are - // passed on if the merged slime subsequently splits. Hopefully - // this won't do anything weird. - merge_to->flags |= initial_slime->flags; - - // Merging costs the combined slime some energy. - monsterentry* entry = get_monster_data(merge_to->type); - merge_to->speed_increment -= entry->energy_usage.move; - - // This is dumb. With that said, the idea is that if 2 slimes merge - // you can gain a space by moving away the turn after (maybe this is - // too nice but there will probably be a lot of complaints about the - // damage on higher level slimes). So we subtracted some energy - // above, but if merge_to hasn't moved yet this turn, that will just - // cancel its turn in this round of world_reacts(). So we are going - // to see if merge_to has gone already by checking its mindex (this - // works because handle_monsters just iterates over env.mons in - // ascending order). - if (initial_slime->mindex() < merge_to->mindex()) - merge_to->speed_increment -= entry->energy_usage.move; - - // Overwrite the state of the slime getting merged into, because it - // might have been resting or something. - merge_to->behaviour = initial_slime->behaviour; - merge_to->foe = initial_slime->foe; - - behaviour_event(merge_to, ME_EVAL); - - // Messaging. - if (you.can_see(merge_to)) - { - if (you.can_see(initial_slime)) - { - mprf("Two slime creatures merge to form %s.", - merge_to->name(DESC_NOCAP_A).c_str()); - } - else - { - mprf("A slime creature suddenly becomes %s.", - merge_to->name(DESC_NOCAP_A).c_str()); - } - - you.flash_colour = LIGHTGREEN; - viewwindow(true, false); - - int flash_delay = 150; - // Scale delay to match change in arena_delay. - if (crawl_state.arena) - { - flash_delay *= Options.arena_delay; - flash_delay /= 600; - } - - delay(flash_delay); - } - else if (you.can_see(initial_slime)) - mpr("A slime creature suddenly disappears!"); - - // Have to 'kill' the slime doing the merging. - monster_die(initial_slime, KILL_MISC, NON_MONSTER, true); - - return (true); -} - -// Slime creatures can split but not merge under these conditions. -static bool _unoccupied_slime(monsters *thing) -{ - return (thing->asleep() - || mons_is_wandering(thing) - || thing->foe == MHITNOT); -} - -// Slime creatures cannot split or merge under these conditions. -static bool _disabled_slime(monsters *thing) -{ - return (!thing - || mons_is_fleeing(thing) - || mons_is_confused(thing) - || mons_is_paralysed(thing)); -} - -// See if there are any appropriate adjacent slime creatures for 'thing' -// to merge with. If so, carry out the merge. -static bool _slime_merge(monsters *thing) -{ - if (!thing || _disabled_slime(thing) || _unoccupied_slime(thing)) - return (false); - - int max_slime_merge = 5; - int compass_idx[8] = {0, 1, 2, 3, 4, 5, 6, 7}; - std::random_shuffle(compass_idx, compass_idx + 8); - coord_def origin = thing->pos(); - - // Check for adjacent slime creatures. - for (int i = 0; i < 8; ++i) - { - coord_def target = origin + Compass[compass_idx[i]]; - monsters *other_thing = monster_at(target); - - // We found an adjacent monster. Is it another slime creature - // we can consider merging with? - if (other_thing - && other_thing->mons_species() == MONS_SLIME_CREATURE - && other_thing->attitude == thing->attitude - && other_thing->is_summoned() == thing->is_summoned() - && !mons_is_shapeshifter(other_thing) - && !_disabled_slime(other_thing)) - { - // We can actually merge if doing so won't take us over the - // merge cap and the 'movement' would bring us closer to our - // target. - int new_blob_count = other_thing->number + thing->number; - if (new_blob_count <= max_slime_merge - && grid_distance(thing->target, thing->pos()) > - grid_distance(thing->target, target)) - { - return (_do_merge(thing, other_thing)); - } - } - } - - // No adjacent slime creatures we could merge with. - return (false); -} - -// See if slime creature 'thing' can split, and carry out the split if -// we can find a square to place the new slime creature on. -static bool _slime_split(monsters *thing) -{ - if (!thing - || !_unoccupied_slime(thing) - || _disabled_slime(thing) - || thing->number <= 1) - { - return (false); - } - - int compass_idx[] = {0, 1, 2, 3, 4, 5, 6, 7}; - std::random_shuffle(compass_idx, compass_idx + 8); - coord_def origin = thing->pos(); - - // Anywhere we can place an offspring? - for (int i = 0; i < 8; ++i) - { - coord_def target = origin + Compass[compass_idx[i]]; - - if (mons_class_can_pass(MONS_SLIME_CREATURE, env.grid(target)) - && !actor_at(target)) - { - // This can fail if placing a new monster fails. That - // probably means we have too many monsters on the level, - // so just return in that case. - return (_do_split(thing, target)); - } - } - - // No free squares. - return (false); -} - -// See if a given slime creature can split or merge. -bool slime_split_merge(monsters *thing) -{ - // No merging/splitting shapeshifters. - if (!thing - || mons_is_shapeshifter(thing) - || thing->mons_species() != MONS_SLIME_CREATURE) - { - return (false); - } - - if (_slime_split(thing)) - return (true); - - return (_slime_merge(thing)); -} - -bool orc_battle_cry(monsters *chief) -{ - const actor *foe = chief->get_foe(); - int affected = 0; - - if (foe - && (foe != &you || !mons_friendly(chief)) - && !silenced(chief->pos()) - && chief->can_see(foe) - && coinflip()) - { - const int boss_index = monster_index(chief); - const int level = chief->hit_dice > 12? 2 : 1; - std::vector seen_affected; - for (int i = 0; i < MAX_MONSTERS; ++i) - { - monsters *mon = &menv[i]; - if (mon != chief - && mon->alive() - && mons_species(mon->type) == MONS_ORC - && mons_aligned(boss_index, i) - && mon->hit_dice < chief->hit_dice - && !mon->has_ench(ENCH_BERSERK) - && !mon->has_ench(ENCH_MIGHT) - && !mon->cannot_move() - && !mon->confused() - && chief->can_see(mon)) - { - mon_enchant ench = mon->get_ench(ENCH_BATTLE_FRENZY); - if (ench.ench == ENCH_NONE || ench.degree < level) - { - const int dur = - random_range(12, 20) * speed_to_duration(mon->speed); - - if (ench.ench != ENCH_NONE) - { - ench.degree = level; - ench.duration = std::max(ench.duration, dur); - mon->update_ench(ench); - } - else - { - mon->add_ench(mon_enchant(ENCH_BATTLE_FRENZY, level, - KC_OTHER, dur)); - } - - affected++; - if (you.can_see(mon)) - seen_affected.push_back(mon); - - if (mon->asleep()) - behaviour_event(mon, ME_DISTURB, MHITNOT, chief->pos()); - } - } - } - - if (affected) - { - if (you.can_see(chief) && player_can_hear(chief->pos())) - { - mprf(MSGCH_SOUND, "%s roars a battle-cry!", - chief->name(DESC_CAP_THE).c_str()); - } - - // The yell happens whether you happen to see it or not. - noisy(15, chief->pos(), chief->mindex()); - - // Disabling detailed frenzy announcement because it's so spammy. - const msg_channel_type channel = - mons_friendly_real(chief) ? MSGCH_MONSTER_ENCHANT - : MSGCH_FRIEND_ENCHANT; - - if (!seen_affected.empty()) - { - std::string who; - if (seen_affected.size() == 1) - { - who = seen_affected[0]->name(DESC_CAP_THE); - mprf(channel, "%s goes into a battle-frenzy!", who.c_str()); - } - else - { - int type = seen_affected[0]->type; - for (unsigned int i = 0; i < seen_affected.size(); i++) - { - if (seen_affected[i]->type != type) - { - // just mention plain orcs - type = MONS_ORC; - break; - } - } - who = get_monster_data(type)->name; - - mprf(channel, "%s %s go into a battle-frenzy!", - mons_friendly(chief) ? "Your" : "The", - pluralise(who).c_str()); - } - } - } - } - // Orc battle cry doesn't cost the monster an action. - return (false); -} - -static bool _make_monster_angry(const monsters *mon, monsters *targ) -{ - if (mons_friendly_real(mon) != mons_friendly_real(targ)) - return (false); - - // targ is guaranteed to have a foe (needs_berserk checks this). - // Now targ needs to be closer to *its* foe than mon is (otherwise - // mon might be in the way). - - coord_def victim; - if (targ->foe == MHITYOU) - victim = you.pos(); - else if (targ->foe != MHITNOT) - { - const monsters *vmons = &menv[targ->foe]; - if (!vmons->alive()) - return (false); - victim = vmons->pos(); - } - else - { - // Should be impossible. needs_berserk should find this case. - ASSERT(false); - return (false); - } - - // If mon may be blocking targ from its victim, don't try. - if (victim.distance_from(targ->pos()) > victim.distance_from(mon->pos())) - return (false); - - if (you.can_see(mon)) - { - mprf("%s goads %s on!", mon->name(DESC_CAP_THE).c_str(), - targ->name(DESC_NOCAP_THE).c_str()); - } - - targ->go_berserk(false); - - return (true); -} - -bool moth_incite_monsters(const monsters *mon) -{ - if (is_sanctuary(you.pos()) || is_sanctuary(mon->pos())) - return false; - - int goaded = 0; - for (int i = 0; i < MAX_MONSTERS; ++i) - { - monsters *targ = &menv[i]; - if (targ == mon || !targ->alive() || !targ->needs_berserk()) - continue; - - if (mon->pos().distance_from(targ->pos()) > 3) - continue; - - if (is_sanctuary(targ->pos())) - continue; - - // Cannot goad other moths of wrath! - if (targ->type == MONS_MOTH_OF_WRATH) - continue; - - if (_make_monster_angry(mon, targ) && !one_chance_in(3 * ++goaded)) - return (true); - } - - return (false); -} - -void mons_clear_trapping_net(monsters *mon) -{ - if (!mons_is_caught(mon)) - return; - - const int net = get_trapping_net(mon->pos()); - if (net != NON_ITEM) - remove_item_stationary(mitm[net]); - - mon->del_ench(ENCH_HELD, true); -} - -bool mons_clonable(const monsters* mon, bool needs_adjacent) -{ - // No uniques or ghost demon monsters. Also, figuring out the name - // for the clone of a named monster isn't worth it. - if (mons_is_unique(mon->type) - || mons_is_ghost_demon(mon->type) - || mon->is_named()) - { - return (false); - } - - if (needs_adjacent) - { - // Is there space for the clone? - bool square_found = false; - for (int i = 0; i < 8; i++) - { - const coord_def p = mon->pos() + Compass[i]; - - if (in_bounds(p) - && !actor_at(p) - && monster_habitable_grid(mon, grd(p))) - { - square_found = true; - break; - } - } - if (!square_found) - return (false); - } - - // Is the monster carrying an artefact? - for (int i = 0; i < NUM_MONSTER_SLOTS; i++) - { - const int index = mon->inv[i]; - - if (index == NON_ITEM) - continue; - - if (is_artefact(mitm[index])) - return (false); - } - - return (true); -} - -int clone_mons(const monsters* orig, bool quiet, bool* obvious, - coord_def pos) -{ - // Is there an open slot in menv? - int midx = NON_MONSTER; - for (int i = 0; i < MAX_MONSTERS; i++) - if (menv[i].type == MONS_NO_MONSTER) - { - midx = i; - break; - } - - if (midx == NON_MONSTER) - return (NON_MONSTER); - - if (!in_bounds(pos)) - { - // Find an adjacent square. - int squares = 0; - for (int i = 0; i < 8; i++) - { - const coord_def p = orig->pos() + Compass[i]; - - if (in_bounds(p) - && !actor_at(p) - && monster_habitable_grid(orig, grd(p))) - { - if (one_chance_in(++squares)) - pos = p; - } - } - - if (squares == 0) - return (NON_MONSTER); - } - - ASSERT( !actor_at(pos) ); - - monsters &mon(menv[midx]); - - mon = *orig; - mon.position = pos; - mgrd(pos) = midx; - - // Duplicate objects, or unequip them if they can't be duplicated. - for (int i = 0; i < NUM_MONSTER_SLOTS; i++) - { - const int old_index = orig->inv[i]; - - if (old_index == NON_ITEM) - continue; - - const int new_index = get_item_slot(0); - if (new_index == NON_ITEM) - { - mon.unequip(mitm[old_index], i, 0, true); - mon.inv[i] = NON_ITEM; - continue; - } - - mon.inv[i] = new_index; - mitm[new_index] = mitm[old_index]; - mitm[new_index].set_holding_monster(midx); - } - - bool _obvious; - if (obvious == NULL) - obvious = &_obvious; - *obvious = false; - - if (you.can_see(orig) && you.can_see(&mon)) - { - if (!quiet) - simple_monster_message(orig, " is duplicated!"); - *obvious = true; - } - - mark_interesting_monst(&mon, mon.behaviour); - if (you.can_see(&mon)) - { - handle_seen_interrupt(&mon); - viewwindow(true, false); - } - - if (crawl_state.arena) - arena_placed_monster(&mon); - - return (midx); -} - -std::string summoned_poof_msg(const monsters* monster, bool plural) -{ - int summon_type = 0; - bool valid_mon = false; - if (monster != NULL && !invalid_monster(monster)) - { - (void) monster->is_summoned(NULL, &summon_type); - valid_mon = true; - } - - std::string msg = "disappear%s in a puff of smoke"; - bool no_chaos = false; - - switch (summon_type) - { - case SPELL_SHADOW_CREATURES: - msg = "dissolve%s into shadows"; - no_chaos = true; - break; - - case MON_SUMM_CHAOS: - msg = "degenerate%s into a cloud of primal chaos"; - break; - - case MON_SUMM_WRATH: - case MON_SUMM_AID: - if (valid_mon && is_good_god(monster->god)) - { - msg = "dissolve%s into sparkling lights"; - no_chaos = true; - } - break; - } - - if (valid_mon) - { - if (monster->god == GOD_XOM && !no_chaos && one_chance_in(10) - || monster->type == MONS_CHAOS_SPAWN) - { - msg = "degenerate%s into a cloud of primal chaos"; - } - - if (mons_is_holy(monster) && summon_type != SPELL_SHADOW_CREATURES - && summon_type != MON_SUMM_CHAOS) - { - msg = "dissolve%s into sparkling lights"; - } - } - - // Conjugate. - msg = make_stringf(msg.c_str(), plural ? "" : "s"); - - return (msg); -} - -std::string summoned_poof_msg(const int midx, const item_def &item) -{ - if (midx == NON_MONSTER) - return summoned_poof_msg(static_cast(NULL), item); - else - return summoned_poof_msg(&menv[midx], item); -} - -std::string summoned_poof_msg(const monsters* monster, const item_def &item) -{ - ASSERT(item.flags & ISFLAG_SUMMONED); - - return summoned_poof_msg(monster, item.quantity > 1); -} diff --git a/crawl-ref/source/mstuff2.h b/crawl-ref/source/mstuff2.h index 4f0a5ba3c8..848dacbf7c 100644 --- a/crawl-ref/source/mstuff2.h +++ b/crawl-ref/source/mstuff2.h @@ -14,33 +14,4 @@ struct bolt; -bolt mons_spells(monsters *mons, spell_type spell_cast, int power); -void mons_cast(monsters *monster, bolt &pbolt, spell_type spell_cast, - bool do_noise = true); -void mons_cast_noise(monsters *monster, bolt &pbolt, spell_type spell_cast); -void setup_mons_cast(monsters *monster, bolt &pbolt, spell_type spell_cast); -bool mons_throw(monsters *monster, bolt &pbolt, int hand_used); -void setup_generic_throw(monsters *monster, bolt &pbolt); -void mons_trap(monsters *monster); -bool monster_random_space(const monsters *monster, coord_def& target, - bool forbid_sanctuary = false); -bool monster_random_space(monster_type mon, coord_def& target, - bool forbid_sanctuary = false); -void monster_teleport(monsters *monster, bool instan, bool silent = false); -bool ugly_thing_mutate(monsters *ugly, bool proximity = false); -bool slime_split_merge(monsters *thing); -bool orc_battle_cry(monsters *chief); -bool orange_statue_effects(monsters *mons); -bool silver_statue_effects(monsters *mons); -bool moth_incite_monsters(const monsters *mon); -void mons_clear_trapping_net(monsters *mon); -void mons_cast_haunt(monsters *monster); - -bool mons_clonable(const monsters* orig, bool needs_adjacent = true); -int clone_mons(const monsters* orig, bool quiet = false, - bool* obvious = NULL, coord_def pos = coord_def(0, 0) ); - -std::string summoned_poof_msg(const monsters* monster, bool plural = false); -std::string summoned_poof_msg(const int midx, const item_def &item); -std::string summoned_poof_msg(const monsters* monster, const item_def &item); #endif diff --git a/crawl-ref/source/religion.cc b/crawl-ref/source/religion.cc index f19fe4203b..a1a87c7508 100644 --- a/crawl-ref/source/religion.cc +++ b/crawl-ref/source/religion.cc @@ -50,10 +50,10 @@ #include "makeitem.h" #include "message.h" #include "misc.h" +#include "mon-behv.h" #include "mon-util.h" #include "monplace.h" #include "monstuff.h" -#include "mstuff2.h" #include "mutation.h" #include "newgame.h" #include "notes.h" diff --git a/crawl-ref/source/spells2.cc b/crawl-ref/source/spells2.cc index bc5ce39e56..3a9741b49f 100644 --- a/crawl-ref/source/spells2.cc +++ b/crawl-ref/source/spells2.cc @@ -33,6 +33,7 @@ #include "los.h" #include "message.h" #include "misc.h" +#include "mon-behv.h" #include "monplace.h" #include "monstuff.h" #include "mon-util.h" diff --git a/crawl-ref/source/spells3.cc b/crawl-ref/source/spells3.cc index 15397a1f77..3dc8033cd2 100644 --- a/crawl-ref/source/spells3.cc +++ b/crawl-ref/source/spells3.cc @@ -33,6 +33,7 @@ #include "los.h" #include "message.h" #include "misc.h" +#include "mon-behv.h" #include "monplace.h" #include "monstuff.h" #include "mon-util.h" diff --git a/crawl-ref/source/spells4.cc b/crawl-ref/source/spells4.cc index 160fee75b1..dd452c1fbd 100644 --- a/crawl-ref/source/spells4.cc +++ b/crawl-ref/source/spells4.cc @@ -32,10 +32,10 @@ #include "makeitem.h" #include "message.h" #include "misc.h" +#include "mon-behv.h" #include "monplace.h" #include "monstuff.h" #include "mon-util.h" -#include "mstuff2.h" #include "ouch.h" #include "player.h" #include "quiver.h" diff --git a/crawl-ref/source/spl-cast.cc b/crawl-ref/source/spl-cast.cc index fd6b110c00..79fefc24f6 100644 --- a/crawl-ref/source/spl-cast.cc +++ b/crawl-ref/source/spl-cast.cc @@ -32,8 +32,8 @@ #include "menu.h" #include "misc.h" #include "message.h" +#include "mon-cast.h" #include "monstuff.h" -#include "mstuff2.h" #include "mutation.h" #include "ouch.h" #include "player.h" diff --git a/crawl-ref/source/spl-util.cc b/crawl-ref/source/spl-util.cc index d588fb62e7..df0e76626e 100644 --- a/crawl-ref/source/spl-util.cc +++ b/crawl-ref/source/spl-util.cc @@ -26,6 +26,7 @@ #include "stuff.h" #include "los.h" #include "macro.h" +#include "mon-behv.h" #include "monstuff.h" #include "notes.h" #include "player.h" diff --git a/crawl-ref/source/terrain.cc b/crawl-ref/source/terrain.cc index 86b4e9cf14..a3b1e48ff3 100644 --- a/crawl-ref/source/terrain.cc +++ b/crawl-ref/source/terrain.cc @@ -22,8 +22,8 @@ #include "message.h" #include "misc.h" #include "monplace.h" +#include "monstuff.h" #include "mon-util.h" -#include "mstuff2.h" #include "ouch.h" #include "overmap.h" #include "player.h" diff --git a/crawl-ref/source/tutorial.cc b/crawl-ref/source/tutorial.cc index 3d9a177e72..32010e4dfa 100644 --- a/crawl-ref/source/tutorial.cc +++ b/crawl-ref/source/tutorial.cc @@ -35,9 +35,9 @@ #include "menu.h" #include "message.h" #include "misc.h" +#include "mon-behv.h" #include "mon-pick.h" #include "mon-util.h" -#include "monstuff.h" #include "mutation.h" #include "newgame.h" #include "jobs.h" diff --git a/crawl-ref/source/view.cc b/crawl-ref/source/view.cc index 42b6254e90..245aaee4e0 100644 --- a/crawl-ref/source/view.cc +++ b/crawl-ref/source/view.cc @@ -42,6 +42,7 @@ #include "macro.h" #include "message.h" #include "misc.h" +#include "mon-behv.h" #include "monplace.h" #include "monstuff.h" #include "mon-util.h" diff --git a/crawl-ref/source/xom.cc b/crawl-ref/source/xom.cc index 34e879f41e..a79672a555 100644 --- a/crawl-ref/source/xom.cc +++ b/crawl-ref/source/xom.cc @@ -23,6 +23,7 @@ #include "makeitem.h" #include "message.h" #include "misc.h" +#include "mon-behv.h" #include "mon-util.h" #include "monplace.h" #include "monstuff.h" -- cgit v1.2.3-54-g00ecf