/* * 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 "env.h" #include "fprop.h" #include "exclude.h" #include "mon-iter.h" #include "mon-movetarget.h" #include "mon-pathfind.h" #include "mon-stuff.h" #include "mon-util.h" #include "random.h" #include "state.h" #include "terrain.h" #include "traps.h" #include "tutorial.h" #include "view.h" #include "shout.h" static void _set_nearest_monster_foe(monsters *monster); static void _guess_invis_foe_pos(monsters *mon, bool strict = true) { const actor* foe = mon->get_foe(); const int guess_radius = mons_sense_invis(mon) ? 3 : 2; std::vector possibilities; for (radius_iterator ri(mon->pos(), guess_radius, C_ROUND); ri; ++ri) { // NOTE: This depends on mon_see_cell() ignoring clouds, // so that cells hidden by opaque clouds are included // as a possibility for the foe's location. if (!strict || foe->is_habitable(*ri) && mon->mon_see_cell(*ri)) possibilities.push_back(*ri); } // If being strict (monster must see possible cell, foe must be // able to live there) gives no possibilites, then find *some* // cell near the foe. if (strict && possibilities.empty()) { _guess_invis_foe_pos(mon, false); return; } if (!possibilities.empty()) mon->target = possibilities[random2(possibilities.size())]; } //--------------------------------------------------------------- // // 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) { // Test spawners should always be BEH_SEEK against a foe, since // their only purpose is to spew out monsters for testing // purposes. if (mon->type == MONS_TEST_SPAWNER) { for (monster_iterator mi; mi; ++mi) { if (mon->attitude != mi->attitude) { mon->foe = mi->mindex(); mon->target = mi->pos(); mon->behaviour = BEH_SEEK; return; } } } bool changed = true; bool isFriendly = mon->friendly(); bool isNeutral = mon->neutral(); bool wontAttack = mon->wont_attack(); // 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 = you.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 && !mon->wont_attack()) 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 = mon->pacified(); 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; } // Make sure monsters are not targetting the player in arena mode. ASSERT(!(crawl_state.arena && mon->foe == MHITYOU)); 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 (foe_monster.friendly() == 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->berserk() && mon->type != MONS_GIANT_SPORE) { mon->foe = you.pet_target; } // Instead, berserkers attack nearest monsters. if ((mon->berserk() || mon->type == 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 == mon->mindex()) 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 && menv[mon->foe].wont_attack()) { mon->foe = MHITNOT; } // Neutral monsters prefer not to attack players, or other neutrals. if (isNeutral && mon->foe != MHITNOT && (mon->foe == MHITYOU || menv[mon->foe].neutral())) { 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->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 (foe_monster.friendly() == 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 || mon->type == MONS_GIANT_SPORE) { new_beh = BEH_WANDER; } else { new_foe = MHITYOU; mon->target = you.pos(); } break; } // Foe gone out of LOS? if (!proxFoe) { // Maybe the foe is just invisible. if (mon->target.origin() && afoe && mon->near_foe()) { _guess_invis_foe_pos(mon); if (mon->target.origin()) { // Having a seeking mon with a foe who's target is // (0, 0) can lead to asserts, so lets try to // avoid that. _set_nearest_monster_foe(mon); if (mon->foe == MHITNOT) { new_beh = BEH_WANDER; break; } mon->target = mon->get_foe()->pos(); } } 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 && you.beheld_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 (mon->strict_neutral() && 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 && !crawl_state.arena) { 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 (!in_bounds(p)) return (false); if (p == you.pos()) { // The player: We don't return true here because // otherwise wandering monsters will always // attack the player. return (false); } if (monsters *foe = monster_at(p)) { if (foe != mon && mon->can_see(foe) && !mons_is_projectile(foe->type) && (friendly || !is_sanctuary(p)) && (foe->friendly() != friendly || (neutral && !foe->neutral()))) { return (true); } } return (false); } // Choose random nearest monster as a foe. void _set_nearest_monster_foe(monsters *mon) { const bool friendly = mon->friendly(); const bool neutral = mon->neutral(); 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()); if (mons_is_projectile(mon->type)) return; // projectiles have no AI const beh_type old_behaviour = mon->behaviour; bool isSmart = (mons_intel(mon) > I_ANIMAL); bool wontAttack = mon->wont_attack(); bool sourceWontAttack = false; bool setTarget = false; bool breakCharm = false; bool was_sleeping = mon->asleep(); if (src == MHITYOU) sourceWontAttack = true; else if (src != MHITNOT) sourceWontAttack = menv[src].wont_attack(); 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 Fedhas, 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 (mon->friendly() && mon->is_patrolling() && !mon->asleep()) { break; } // Avoid moving friendly giant spores out of BEH_WANDER. if (mon->friendly() && mon->type == MONS_GIANT_SPORE) 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() && !you.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->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 (mon->friendly()) { breakCharm = true; mon->foe = MHITNOT; set_random_target(mon); } else setTarget = true; } else if (mon->friendly() && !crawl_state.arena) mon->foe = MHITYOU; if (you.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 (mon->pacified()) break; // Just set behaviour... foe doesn't change. if (!mons_is_cornered(mon)) { if (mon->friendly() && !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 = 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 && !mon->wont_attack()) { handle_monster_shouts(mon); } const bool wasLurking = (old_behaviour == BEH_LURK && !mons_is_lurking(mon)); const bool isPacified = mon->pacified(); 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); }