/* * File: shout.cc * Summary: Stealth, noise, shouting. */ #include "AppHdr.h" #include "shout.h" #include "cluautil.h" #include "coord.h" #include "database.h" #include "dlua.h" #include "env.h" #include "ghost.h" #include "jobs.h" #include "message.h" #include "misc.h" #include "mon-behv.h" #include "mon-iter.h" #include "mon-place.h" #include "mon-pathfind.h" #include "monster.h" #include "mon-stuff.h" #include "player.h" #include "random.h" #include "skills.h" #include "state.h" #include "stuff.h" #include "areas.h" #include "tutorial.h" #include "view.h" #include extern int stealth; // defined in main.cc void handle_monster_shouts(monsters* monster, bool force) { if (!force && x_chance_in_y(you.skills[SK_STEALTH], 30)) return; // Friendly or neutral monsters don't shout. if (!force && (monster->friendly() || monster->neutral())) return; // Get it once, since monster might be S_RANDOM, in which case // mons_shouts() will return a different value every time. // Demon lords will insult you as a greeting, but later we'll // choose a random verb and loudness for them. shout_type s_type = mons_shouts(monster->type, false); // Silent monsters can give noiseless "visual shouts" if the // player can see them, in which case silence isn't checked for. if (s_type == S_SILENT && !monster->visible_to(&you) || s_type != S_SILENT && !player_can_hear(monster->pos())) { return; } mon_acting mact(monster); std::string default_msg_key = ""; switch (s_type) { case S_SILENT: // No default message. break; case S_SHOUT: default_msg_key = "__SHOUT"; break; case S_BARK: default_msg_key = "__BARK"; break; case S_SHOUT2: default_msg_key = "__TWO_SHOUTS"; break; case S_ROAR: default_msg_key = "__ROAR"; break; case S_SCREAM: default_msg_key = "__SCREAM"; break; case S_BELLOW: default_msg_key = "__BELLOW"; break; case S_SCREECH: default_msg_key = "__SCREECH"; break; case S_BUZZ: default_msg_key = "__BUZZ"; break; case S_MOAN: default_msg_key = "__MOAN"; break; case S_GURGLE: default_msg_key = "__GURGLE"; break; case S_WHINE: default_msg_key = "__WHINE"; break; case S_CROAK: default_msg_key = "__CROAK"; break; case S_GROWL: default_msg_key = "__GROWL"; break; case S_HISS: default_msg_key = "__HISS"; break; case S_DEMON_TAUNT: default_msg_key = "__DEMON_TAUNT"; break; default: default_msg_key = "__BUGGY"; // S_LOUD, S_VERY_SOFT, etc. (loudness) } // Now that we have the message key, get a random verb and noise level // for pandemonium lords. if (s_type == S_DEMON_TAUNT) s_type = mons_shouts(monster->type, true); std::string msg, suffix; std::string key = mons_type_name(monster->type, DESC_PLAIN); // Pandemonium demons have random names, so use "pandemonium lord" if (monster->type == MONS_PANDEMONIUM_DEMON) key = "pandemonium lord"; // Search for player ghost shout by the ghost's class. else if (monster->type == MONS_PLAYER_GHOST) { const ghost_demon &ghost = *(monster->ghost); std::string ghost_class = get_class_name(ghost.job); key = ghost_class + " player ghost"; default_msg_key = "player ghost"; } // Tries to find an entry for "name seen" or "name unseen", // and if no such entry exists then looks simply for "name". // We don't use "you.can_see(monster)" here since that would return // false for submerged monsters, but submerged monsters will be forced // to surface before they shout, thus removing that source of // non-visibility. if (mons_near(monster) && (!monster->invisible() || you.can_see_invisible())) suffix = " seen"; else suffix = " unseen"; if (monster->props.exists("shout_func")) { lua_stack_cleaner clean(dlua); dlua_chunk &chunk = monster->props["shout_func"]; if (!chunk.load(dlua)) { push_monster(dlua, monster); clua_pushcxxstring(dlua, suffix); dlua.callfn(NULL, 2, 1); dlua.fnreturns(">s", &msg); // __NONE means to be silent, and __NEXT or __DEFAULT means to try // the next method of getting a shout message. if (msg == "__NONE") return; if (msg == "__DEFAULT" || msg == "__NEXT") msg.clear(); } else { mprf(MSGCH_ERROR, "Lua shout function for monster '%s' didn't load: %s", monster->full_name(DESC_PLAIN).c_str(), dlua.error.c_str()); } } if (msg.empty()) msg = getShoutString(key, suffix); if (msg == "__DEFAULT" || msg == "__NEXT") msg = getShoutString(default_msg_key, suffix); else if (msg.empty()) { // NOTE: Use the hardcoded glyph rather than that returned // by mons_char(), since the result of mons_char() can be // changed by user settings. char mchar = get_monster_data(monster->type)->showchar; // See if there's a shout for all monsters using the // same glyph/symbol std::string glyph_key = "'"; // Database keys are case-insensitve. if (isupper(mchar)) glyph_key += "cap-"; glyph_key += mchar; glyph_key += "'"; msg = getShoutString(glyph_key, suffix); if (msg.empty() || msg == "__DEFAULT") msg = getShoutString(default_msg_key, suffix); } if (default_msg_key == "__BUGGY") { msg::streams(MSGCH_SOUND) << "You hear something buggy!" << std::endl; } else if (s_type == S_SILENT && (msg.empty() || msg == "__NONE")) { ; // No "visual shout" defined for silent monster, do nothing. } else if (msg.empty()) // Still nothing found? { msg::streams(MSGCH_DIAGNOSTICS) << "No shout entry for default shout type '" << default_msg_key << "'" << std::endl; msg::streams(MSGCH_SOUND) << "You hear something buggy!" << std::endl; } else if (msg == "__NONE") { msg::streams(MSGCH_DIAGNOSTICS) << "__NONE returned as shout for non-silent monster '" << default_msg_key << "'" << std::endl; msg::streams(MSGCH_SOUND) << "You hear something buggy!" << std::endl; } else { msg_channel_type channel = MSGCH_TALK; std::string param = ""; std::string::size_type pos = msg.find(":"); if (pos != std::string::npos) { param = msg.substr(0, pos); msg = msg.substr(pos + 1); } if (s_type == S_SILENT || param == "VISUAL") channel = MSGCH_TALK_VISUAL; else if (param == "SOUND") channel = MSGCH_SOUND; // Monster must come up from being submerged if it wants to shout. if (monster->submerged()) { if (!monster->del_ench(ENCH_SUBMERGED)) { // Couldn't unsubmerge. return; } if (you.can_see(monster)) { if (monster->type == MONS_AIR_ELEMENTAL) monster->seen_context = "thin air"; else if (monster->type == MONS_TRAPDOOR_SPIDER) monster->seen_context = "leaps out"; else if (!monster_habitable_grid(monster, DNGN_FLOOR)) monster->seen_context = "bursts forth shouting"; else monster->seen_context = "surfaces"; // Give interrupt message before shout message. handle_seen_interrupt(monster); } } if (channel != MSGCH_TALK_VISUAL || you.can_see(monster)) { msg = do_mon_str_replacements(msg, monster, s_type); msg::streams(channel) << msg << std::endl; // Otherwise it can move away with no feedback. if (you.can_see(monster)) { if (!(monster->flags & MF_WAS_IN_VIEW)) handle_seen_interrupt(monster); seen_monster(monster); } } } const int noise_level = get_shout_noise_level(s_type); const bool heard = noisy(noise_level, monster->pos(), monster->mindex()); if (Tutorial.tutorial_left && (heard || you.can_see(monster))) learned_something_new(TUT_MONSTER_SHOUT, monster->pos()); } #ifdef WIZARD void force_monster_shout(monsters* monster) { handle_monster_shouts(monster, true); } #endif bool check_awaken(monsters* monster) { // Monsters put to sleep by ensorcelled hibernation will sleep // at least one turn. if (monster->has_ench(ENCH_SLEEPY)) return (false); // Berserkers aren't really concerned about stealth. if (you.berserk()) return (true); // I assume that creatures who can sense invisible are very perceptive. int mons_perc = 10 + (mons_intel(monster) * 4) + monster->hit_dice + mons_sense_invis(monster) * 5; bool unnatural_stealthy = false; // "stealthy" only because of invisibility? // Critters that are wandering but still have MHITYOU as their foe are // still actively on guard for the player, even if they can't see you. // Give them a large bonus -- handle_behaviour() will nuke 'foe' after // a while, removing this bonus. if (mons_is_wandering(monster) && monster->foe == MHITYOU) mons_perc += 15; if (!you.visible_to(monster)) { mons_perc -= 75; unnatural_stealthy = true; } if (monster->asleep()) { if (monster->holiness() == MH_NATURAL) { // Monster is "hibernating"... reduce chance of waking. if (monster->has_ench(ENCH_SLEEP_WARY)) mons_perc -= 10; } else // unnatural creature { // Unnatural monsters don't actually "sleep", they just // haven't noticed an intruder yet... we'll assume that // they're diligently on guard. mons_perc += 10; } } // If you've been tagged with Corona or are Glowing, the glow // makes you extremely unstealthy. if (you.backlit() && you.visible_to(monster)) mons_perc += 50; if (mons_perc < 0) mons_perc = 0; if (x_chance_in_y(mons_perc + 1, stealth)) return (true); // Oops, the monster wakes up! // You didn't wake the monster! if (player_light_armour(true) && you.can_see(monster) // to avoid leaking information && you.burden_state == BS_UNENCUMBERED && !you.attribute[ATTR_SHADOWS] && !monster->wont_attack() && !mons_class_flag(monster->type, M_NO_EXP_GAIN) // If invisible, training happens much more rarely. && (!unnatural_stealthy && one_chance_in(25) || one_chance_in(100))) { exercise(SK_STEALTH, 1); } return (false); } // Noisy now has a messenging service for giving messages to the // player is appropriate. // // Returns true if the PC heard the noise. bool noisy(int loudness, const coord_def& where, const char *msg, int who, bool mermaid) { bool ret = false; if (loudness <= 0) return (false); // If the origin is silenced there is no noise. if (silenced(where)) return (false); const int dist = loudness * loudness; const int player_distance = distance( you.pos(), where ); // Message the player. if (player_distance <= dist && player_can_hear( where )) { if (msg) mpr( msg, MSGCH_SOUND ); you.check_awaken(dist - player_distance); if (!mermaid) you.beholders_check_noise(loudness); ret = true; } for (monster_iterator mi; mi; ++mi) { if (!mi->alive()) continue; // Monsters arent' affected by their own noise. We don't check // where == mi->pos() since it might be caused by the // Projected Noise spell. if (mi->mindex() == who) continue; if (distance(mi->pos(), where) <= dist && !silenced(mi->pos())) { // If the noise came from the character, any nearby monster // will be jumping on top of them. if (where == you.pos()) behaviour_event(*mi, ME_ALERT, MHITYOU, you.pos()); else if (mermaid && mons_primary_habitat(*mi) == HT_WATER && !mi->friendly()) { // Mermaids/sirens call (hostile) aquatic monsters. behaviour_event(*mi, ME_ALERT, MHITNOT, where); } else behaviour_event(*mi, ME_DISTURB, MHITNOT, where); } } return (ret); } bool noisy(int loudness, const coord_def& where, int who, bool mermaid) { return noisy(loudness, where, NULL, who, mermaid); } static const char* _player_vampire_smells_blood(int dist) { // non-thirsty vampires get no clear indication of how close the // smell is if (you.hunger_state >= HS_SATIATED) return ""; if (dist < 16) // 4*4 return " near-by"; if (you.hunger_state <= HS_NEAR_STARVING && dist > get_los_radius_sq()) return " in the distance"; return ""; } void blood_smell(int strength, const coord_def& where) { const int range = strength * strength; dprf("blood stain at (%d, %d), range of smell = %d", where.x, where.y, range); // Of the player species, only Vampires can smell blood. if (you.species == SP_VAMPIRE) { // Whether they actually do so, depends on their hunger state. int vamp_strength = strength - 2 * (you.hunger_state - 1); if (vamp_strength > 0) { int vamp_range = vamp_strength * vamp_strength; const int player_distance = distance(you.pos(), where); if (player_distance <= vamp_range) { dprf("Player smells blood, pos: (%d, %d), dist = %d)", you.pos().x, you.pos().y, player_distance); you.check_awaken(range - player_distance); // Don't message if you can see the square. if (!you.see_cell(where)) { mprf("You smell fresh blood%s.", _player_vampire_smells_blood(player_distance)); } } } } circle_def c(where, range, C_CIRCLE); for (monster_iterator mi(&c); mi; ++mi) { if (!mons_class_flag(mi->type, M_BLOOD_SCENT)) continue; // Let sleeping hounds lie. if (mi->asleep() && mons_species(mi->type) != MONS_VAMPIRE && mi->type != MONS_SHARK) { // 33% chance of sleeping on // 33% of being disturbed (start BEH_WANDER) // 33% of being alerted (start BEH_SEEK) if (!one_chance_in(3)) { if (coinflip()) { dprf("disturbing %s (%d, %d)", mi->name(DESC_PLAIN).c_str(), mi->pos().x, mi->pos().y); behaviour_event(*mi, ME_DISTURB, MHITNOT, where); } continue; } } dprf("alerting %s (%d, %d)", mi->name(DESC_PLAIN).c_str(), mi->pos().x, mi->pos().y); behaviour_event(*mi, ME_ALERT, MHITNOT, where); if (mi->type == MONS_SHARK) { // Sharks go into a battle frenzy if they smell blood. monster_pathfind mp; if (mp.init_pathfind(*mi, where)) { mon_enchant ench = mi->get_ench(ENCH_BATTLE_FRENZY); const int dist = 15 - (mi->pos() - where).rdist(); const int dur = random_range(dist, dist*2) * speed_to_duration(mi->speed); if (ench.ench != ENCH_NONE) { int level = ench.degree; if (level < 4 && one_chance_in(2*level)) ench.degree++; ench.duration = std::max(ench.duration, dur); mi->update_ench(ench); } else { mi->add_ench(mon_enchant(ENCH_BATTLE_FRENZY, 1, KC_OTHER, dur)); simple_monster_message(*mi, " is consumed with " "blood-lust!"); } } } } }