1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303
|
/**
* @file
* @brief Stealth, noise, shouting.
**/
#include "AppHdr.h"
#include "shout.h"
#include <sstream>
#include "act-iter.h"
#include "areas.h"
#include "art-enum.h"
#include "artefact.h"
#include "branch.h"
#include "database.h"
#include "directn.h"
#include "english.h"
#include "env.h"
#include "exercise.h"
#include "ghost.h"
#include "god-passive.h"
#include "hints.h"
#include "item-status-flag-type.h"
#include "jobs.h"
#include "libutil.h"
#include "macro.h"
#include "message.h"
#include "mon-behv.h"
#include "mon-place.h"
#include "mon-poly.h"
#include "prompt.h"
#include "religion.h"
#include "state.h"
#include "stringutil.h"
#include "terrain.h"
#include "view.h"
#include "viewchar.h"
static noise_grid _noise_grid;
static void _monster_apply_noise(monster *mons,
const coord_def &apparent_source,
int noise_intensity_millis);
/// By default, what database lookup key corresponds to each shout type?
static const map<shout_type, string> default_msg_keys = {
{ S_SILENT, "" },
{ S_SHOUT, "__SHOUT" },
{ S_BARK, "__BARK" },
{ S_HOWL, "__HOWL" },
{ S_SHOUT2, "__TWO_SHOUTS" },
{ S_ROAR, "__ROAR" },
{ S_SCREAM, "__SCREAM" },
{ S_BELLOW, "__BELLOW" },
{ S_BLEAT, "__BLEAT" },
{ S_TRUMPET, "__TRUMPET" },
{ S_SCREECH, "__SCREECH" },
{ S_BUZZ, "__BUZZ" },
{ S_MOAN, "__MOAN" },
{ S_GURGLE, "__GURGLE" },
{ S_CROAK, "__CROAK" },
{ S_GROWL, "__GROWL" },
{ S_HISS, "__HISS" },
{ S_SKITTER, "__SKITTER" },
{ S_FAINT_SKITTER, "__FAINT_SKITTER" },
{ S_DEMON_TAUNT, "__DEMON_TAUNT" },
{ S_CHERUB, "__CHERUB" },
{ S_SQUEAL, "__SQUEAL" },
{ S_LOUD_ROAR, "__LOUD_ROAR" },
{ S_RUSTLE, "__RUSTLE" },
{ S_SQUEAK, "__SQUEAK" },
{ S_CAW, "__CAW" },
{ S_LAUGH, "__LAUGH" },
};
/**
* What's the appropriate DB lookup key for a given monster's shouts?
*
* @param mons The monster in question.
* @return A name for the monster; e.g. "orc", "Kirke", "pandemonium
* lord", "Fire Elementalist player ghost".
*/
static string _shout_key(const monster &mons)
{
// Pandemonium demons have random names, so use "pandemonium lord"
if (mons.type == MONS_PANDEMONIUM_LORD)
return "pandemonium lord";
// Search for player ghost shout by the ghost's job.
if (mons.type == MONS_PLAYER_GHOST)
{
const ghost_demon &ghost = *(mons.ghost);
const string ghost_job = get_job_name(ghost.job);
return ghost_job + " player ghost";
}
// everything else just goes by name.
return mons_type_name(mons.type, DESC_PLAIN);
}
/**
* Let a monster consider whether or not it wants to shout, and, if so, shout.
*
* @param mon The monster in question.
*/
void monster_consider_shouting(monster &mon)
{
if (one_chance_in(5))
return;
// Friendly or neutral monsters don't shout.
// XXX: redundant with one of two uses (mon-behv.cc)
if (mon.friendly() || mon.neutral())
return;
monster_attempt_shout(mon);
}
/**
* If it's at all possible for a monster to shout, have it do so.
*
* @param mon The monster in question.
* @return Whether a shout occurred.
*/
bool monster_attempt_shout(monster &mon)
{
if (mon.cannot_act() || mon.asleep() || mon.has_ench(ENCH_DUMB))
return false;
const shout_type shout = mons_shouts(mon.type, false);
// Silent monsters can give noiseless "visual shouts" if the
// player can see them, in which case silence isn't checked for.
// Muted & silenced monsters can't shout at all.
if (shout == S_SILENT && !mon.visible_to(&you)
|| shout != S_SILENT && mon.is_silenced())
{
return false;
}
monster_shout(mon, shout);
return true;
}
/**
* Have a monster perform a specific shout.
*
* @param mons The monster in question.
* @param shout The shout_type to use.
*/
void monster_shout(monster &mons, int shout)
{
shout_type s_type = static_cast<shout_type>(shout);
mon_acting mact(&mons);
// less specific, more specific.
const string default_msg_key
= mons.type == MONS_PLAYER_GHOST ?
"player ghost" :
lookup(default_msg_keys, s_type, "__BUGGY");
const string key = _shout_key(mons);
// 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(mons.type, true);
// Tries to find an entry for "name seen" or "name unseen",
// and if no such entry exists then looks simply for "name".
const bool seen = you.can_see(mons);
const string suffix = seen ? " seen" : " unseen";
string message = getShoutString(key, suffix);
if (message == "__DEFAULT" || message == "__NEXT")
message = getShoutString(default_msg_key, suffix);
else if (message.empty())
{
char mchar = mons_base_char(mons.type);
// See if there's a shout for all monsters using the
// same glyph/symbol
string glyph_key = "'";
// Database keys are case-insensitive.
if (isaupper(mchar))
glyph_key += "cap-";
glyph_key += mchar;
glyph_key += "'";
message = getShoutString(glyph_key, suffix);
if (message.empty() || message == "__DEFAULT")
message = getShoutString(default_msg_key, suffix);
}
if (default_msg_key == "__BUGGY")
{
msg::streams(MSGCH_SOUND) << "You hear something buggy!"
<< endl;
}
else if (s_type == S_SILENT && (message.empty() || message == "__NONE"))
; // No "visual shout" defined for silent monster, do nothing.
else if (message.empty()) // Still nothing found?
{
msg::streams(MSGCH_DIAGNOSTICS)
<< "No shout entry for default shout type '"
<< default_msg_key << "'" << endl;
msg::streams(MSGCH_SOUND) << "You hear something buggy!"
<< endl;
}
else if (message == "__NONE")
{
msg::streams(MSGCH_DIAGNOSTICS)
<< "__NONE returned as shout for non-silent monster '"
<< default_msg_key << "'" << endl;
msg::streams(MSGCH_SOUND) << "You hear something buggy!"
<< endl;
}
else if (s_type == S_SILENT || !silenced(you.pos()))
{
msg_channel_type channel = MSGCH_TALK;
if (s_type == S_SILENT)
channel = MSGCH_TALK_VISUAL;
strip_channel_prefix(message, channel);
if (channel != MSGCH_TALK_VISUAL || seen)
{
message = do_mon_str_replacements(message, mons, s_type);
msg::streams(channel) << message << endl;
}
}
const int noise_level = get_shout_noise_level(s_type);
const bool heard = noisy(noise_level, mons.pos(), mons.mid);
if (crawl_state.game_is_hints() && (heard || you.can_see(mons)))
learned_something_new(HINT_MONSTER_SHOUT, mons.pos());
}
int monster_perception(monster* mons)
{
if (!you.visible_to(mons))
return 5;
return monster_perception(mons->get_hit_dice(), mons_intel(*mons), mons->asleep());
}
int monster_perception(int HD, mon_intel_type intel, bool is_asleep)
{
// Intelligent monsters are better at noticing the player, and those who are
// awake are significantly moreso.
static const int intel_factor[] = {15, 20, 30};
const int perc_mult = intel_factor[intel] + (!is_asleep ? 15 : 0);
const int perc = (5 + HD * 3 / 2) * perc_mult / 20;
// Very low HD enemies still have a minimum perception.
return max(12, perc);
}
bool check_awaken(monster* mons, int stealth)
{
// Usually redundant because we iterate over player LOS.
// Maybe can be removed now that xray_vision is gone?
if (!mons->see_cell(you.pos()))
return false;
// Monsters forcibly put to sleep should never wake up of their own accord
// until it wears off.
if (mons_is_deep_asleep(*mons))
return false;
// Berserkers aren't really concerned about stealth.
if (you.berserk())
return true;
// If you've sacrificed stealth, you always alert monsters.
if (you.get_mutation_level(MUT_NO_STEALTH))
return true;
int mons_perc = monster_perception(mons);
if (x_chance_in_y(mons_perc, stealth))
return true; // Oops, the monster wakes up!
// You didn't wake the monster!
if (you.can_see(*mons) // to avoid leaking information
&& !mons->wont_attack()
&& !mons->neutral() // include pacified monsters
&& mons_class_gives_xp(mons->type))
{
practise_sneaking();
}
return false;
}
void item_noise(const item_def &item, actor &act, string msg, int loudness)
{
// TODO: messaging for cases where act != you. (This doesn't come up right
// now.)
if (is_unrandom_artefact(item))
{
// "Your Singing Sword" sounds disrespectful
// (as if there could be more than one!)
msg = replace_all(msg, "@Your_weapon@", "@The_weapon@");
msg = replace_all(msg, "@your_weapon@", "@the_weapon@");
}
// Set appropriate channel (will usually be TALK).
msg_channel_type channel = MSGCH_TALK;
string param;
const string::size_type pos = msg.find(":");
if (pos != string::npos)
param = msg.substr(0, pos);
if (!param.empty())
{
bool match = true;
if (param == "DANGER")
channel = MSGCH_DANGER;
else if (param == "WARN")
channel = MSGCH_WARN;
else if (param == "SOUND")
channel = MSGCH_SOUND;
else if (param == "PLAIN")
channel = MSGCH_PLAIN;
else if (param == "SPELL")
channel = MSGCH_FRIEND_SPELL;
else if (param == "ENCHANT")
channel = MSGCH_FRIEND_ENCHANT;
else if (param == "VISUAL")
channel = MSGCH_TALK_VISUAL;
else if (param != "TALK")
match = false;
if (match)
msg = msg.substr(pos + 1);
}
if (msg.empty()) // give default noises
{
channel = MSGCH_SOUND;
msg = "You hear a strange noise.";
}
// Replace weapon references. Can't use DESC_THE because that includes
// pluses etc. and we want just the basename.
msg = replace_all(msg, "@The_weapon@", "The @weapon@");
msg = replace_all(msg, "@the_weapon@", "the @weapon@");
msg = replace_all(msg, "@Your_weapon@", "Your @weapon@");
msg = replace_all(msg, "@your_weapon@", "your @weapon@");
msg = replace_all(msg, "@weapon@", item.name(DESC_BASENAME));
// replace references to player name and god
msg = replace_all(msg, "@player_name@", you.your_name);
msg = replace_all(msg, "@player_god@",
you_worship(GOD_NO_GOD) ? "atheism"
: god_name(you.religion, coinflip()));
msg = replace_all(msg, "@player_genus@",
species::name(you.species, species::SPNAME_GENUS));
msg = replace_all(msg, "@a_player_genus@",
article_a(species::name(you.species, species::SPNAME_GENUS)));
msg = replace_all(msg, "@player_genus_plural@",
pluralise(species::name(you.species, species::SPNAME_GENUS)));
msg = maybe_pick_random_substring(msg);
msg = maybe_capitalise_substring(msg);
mprf(channel, "%s", msg.c_str());
if (channel != MSGCH_TALK_VISUAL)
noisy(loudness, act.pos());
}
// TODO: Let artefacts besides weapons generate noise.
void noisy_equipment(const item_def &weapon)
{
if (silenced(you.pos()) || !one_chance_in(20))
return;
string msg;
if (is_unrandom_artefact(weapon))
{
string name = weapon.name(DESC_QUALNAME, false, true, false, false);
msg = getSpeakString(name);
if (msg == "NONE")
return;
}
if (msg.empty())
msg = getSpeakString("noisy weapon");
item_noise(weapon, you, msg, 20);
}
// Berserking monsters cannot be ordered around.
static bool _follows_orders(monster* mon)
{
return mon->friendly()
&& !mon->berserk_or_frenzied()
&& !mon->is_peripheral()
&& !mon->has_ench(ENCH_HAUNTING)
&& !mon->has_ench(ENCH_VEXED);
}
// Sets foe target of friendly monsters.
// If allow_patrol is true, patrolling monsters get MHITNOT instead.
static void _set_friendly_foes(bool allow_patrol = false)
{
for (monster_near_iterator mi(you.pos()); mi; ++mi)
{
if (!_follows_orders(*mi))
continue;
if (you.pet_target != MHITNOT && mi->behaviour == BEH_WITHDRAW)
{
mi->behaviour = BEH_SEEK;
mi->patrol_point = coord_def(0, 0);
}
mi->foe = (allow_patrol && mi->is_patrolling() ? int{MHITNOT}
: you.pet_target);
}
}
static void _set_allies_patrol_point(bool clear = false)
{
for (monster_near_iterator mi(you.pos()); mi; ++mi)
{
if (!_follows_orders(*mi))
continue;
mi->patrol_point = (clear ? coord_def(0, 0) : mi->pos());
if (!clear)
mi->behaviour = BEH_WANDER;
else
mi->behaviour = BEH_SEEK;
mi->travel_path.clear();
}
}
static void _set_allies_withdraw(const coord_def &target)
{
coord_def delta = target - you.pos();
float mult = float(LOS_DEFAULT_RANGE * 3) / (float)max(abs(delta.x), abs(delta.y));
coord_def rally_point = clamp_in_bounds(coord_def(delta.x * mult, delta.y * mult) + you.pos());
for (monster_near_iterator mi(you.pos()); mi; ++mi)
{
if (!_follows_orders(*mi))
continue;
if (mons_class_flag(mi->type, M_STATIONARY))
continue;
mi->behaviour = BEH_WITHDRAW;
mi->target = clamp_in_bounds(target);
mi->patrol_point = rally_point;
mi->foe = MHITNOT;
mi->props.erase(LAST_POS_KEY);
mi->props.erase(BLOCKED_DEADLINE_KEY);
}
}
/// Prompt the player to issue orders. Returns the key pressed.
static int _issue_orders_prompt()
{
mprf(MSGCH_PROMPT, "What are your orders?");
if (!you.cannot_speak())
{
string cap_shout = you.shout_verb(false);
cap_shout[0] = toupper_safe(cap_shout[0]);
mprf(" t - %s!", cap_shout.c_str());
}
if (!you.berserk() && !you.confused())
{
mpr("Orders for allies: a - Attack new target.");
mpr(" r - Retreat! s - Stop attacking.");
mpr(" g - Guard the area. f - Follow me.");
}
mpr(" Anything else - Cancel.");
flush_prev_message(); // buffer doesn't get flushed otherwise
const int keyn = get_ch();
clear_messages();
return keyn;
}
/// If the monster is invisible, can at least one of your allies see them?
static bool _allies_can_see(const monster &mon)
{
if (!mon.invisible())
return true; // XXX: even if we have no allies?
for (monster_near_iterator mi(you.pos()); mi; ++mi)
if (_follows_orders(*mi) && mi->can_see_invisible())
return true;
return false;
}
/**
* Issue the order specified by the given key.
*
* @param keyn The key the player just pressed.
* @param mons_targd[out] Who the player's allies should be targeting as a
* result of this command.
* @return Whether a command actually executed (and the value
* of mons_targd should be used).
*/
static bool _issue_order(int keyn, int &mons_targd)
{
if (you.berserk() || you.confused())
{
canned_msg(MSG_OK);
return false;
}
switch (keyn)
{
case 'f':
case 's':
mons_targd = MHITYOU;
if (keyn == 'f')
{
// Don't reset patrol points for 'Stop fighting!'
_set_allies_patrol_point(true);
mpr("Follow me!");
}
else
mpr("Stop fighting!");
break;
case 'g':
mpr("Guard this area!");
mons_targd = MHITNOT;
_set_allies_patrol_point();
break;
case 'a':
{
direction_chooser_args args;
args.restricts = DIR_TARGET;
args.mode = TARG_HOSTILE;
args.needs_path = false;
args.top_prompt = "Gang up on whom?";
dist targ;
direction(targ, args);
if (targ.isCancel)
{
canned_msg(MSG_OK);
return false;
}
if (!targ.isValid)
{
canned_msg(MSG_NOTHING_THERE);
return false;
}
const monster* m = monster_at(targ.target);
if (!m || !you.can_see(*m))
{
canned_msg(MSG_NOTHING_THERE);
return false;
}
if (!_allies_can_see(*m))
{
mprf("%s is invisible, and you have no allies that can see %s.",
m->name(DESC_THE).c_str(),
m->pronoun(PRONOUN_OBJECTIVE).c_str());
return false;
}
mons_targd = m->mindex();
}
break;
case 'r':
{
direction_chooser_args args;
args.restricts = DIR_TARGET;
args.mode = TARG_NON_ACTOR;
args.needs_path = false;
args.top_prompt = "Retreat in which direction?";
dist targ;
direction(targ, args);
if (targ.isCancel)
{
canned_msg(MSG_OK);
return false;
}
if (targ.isValid)
{
mpr("Fall back!");
mons_targd = MHITNOT;
}
_set_allies_withdraw(targ.target);
}
break;
default:
canned_msg(MSG_OK);
return false;
}
return true;
}
static string _allies_who_cant_see_invis()
{
// We assume that at least some of your allies can see the target, since we
// forbid giving attack orders for a target none of your allies can see.
monster *non_sinv_ally = nullptr;
for (monster_near_iterator mi(you.pos()); mi; ++mi)
{
if (!_follows_orders(*mi) || mi->can_see_invisible())
continue;
if (non_sinv_ally)
return "some of your allies";
non_sinv_ally = *mi;
}
if (non_sinv_ally)
return non_sinv_ally->name(DESC_YOUR);
return "";
}
static void _check_unseen_target(int mindex)
{
const monster &target = env.mons[mindex];
if (!target.invisible())
return;
const string allies = _allies_who_cant_see_invis();
if (allies.empty())
return;
mprf("%s is invisible, and %s can't see %s.",
target.name(DESC_THE).c_str(),
allies.c_str(),
target.pronoun(PRONOUN_OBJECTIVE).c_str());
}
/**
* Prompt the player to either change their allies' orders or to shout.
*
* XXX: it'd be nice if shouting was a separate command.
* XXX: this should maybe be in another file.
*/
void issue_orders()
{
ASSERT(!crawl_state.game_is_arena());
if (you.cannot_speak() && you.berserk())
{
mpr("You're too berserk to give orders, and you can't shout!");
return;
}
const int keyn = _issue_orders_prompt();
if (keyn == '!' || keyn == 't') // '!' for [very] old keyset
{
yell();
you.turn_is_over = true;
return;
}
int mons_targd = MHITNOT; // XXX: just use you.pet_target directly?
if (!_issue_order(keyn, mons_targd))
return;
you.turn_is_over = true;
you.pet_target = mons_targd;
// Allow patrolling for "Stop fighting!"
_set_friendly_foes(keyn == 's');
if (mons_targd != MHITNOT && mons_targd != MHITYOU)
{
mpr("Attack!");
_check_unseen_target(mons_targd);
}
}
/**
* Make the player yell, either at a monster, themselves, or at nothing in particular.
*
* @target The target to yell at; may be null.
*/
void yell(const actor* target)
{
ASSERT(!crawl_state.game_is_arena());
const string shout_verb = you.shout_verb(target != nullptr);
const int noise_level = you.shout_volume();
if (you.cannot_speak())
{
if (target)
{
if (silenced(you.pos()))
{
mprf("You feel %s rip itself from your throat, "
"but you make no sound!",
article_a(shout_verb).c_str());
}
else
{
mprf("You feel a strong urge to %s, but "
"you are unable to make a sound!",
shout_verb.c_str());
}
}
else
mpr("You are unable to make a sound!");
return;
}
if (target)
{
mprf("You %s%s at %s!",
shout_verb.c_str(),
you.duration[DUR_RECITE] ? " your recitation" : "",
target && target->is_player() ? "yourself"
: target->name(DESC_THE).c_str());
}
else
{
const char *fugue_suff = you.duration[DUR_FUGUE] ?
", and the damned howl along" : "";
mprf(MSGCH_SOUND, "You %s%s%s!",
shout_verb.c_str(),
you.berserk() ? " wildly" : " for attention",
fugue_suff);
}
noisy(noise_level, you.pos());
}
void apply_noises()
{
// [ds] This copying isn't awesome, but we cannot otherwise handle
// the case where one set of noises wakes up monsters who then let
// out yips of their own, modifying _noise_grid while it is in the
// middle of propagate_noise().
if (_noise_grid.dirty())
{
noise_grid copy = _noise_grid;
// Reset the main grid.
_noise_grid.reset();
copy.propagate_noise();
}
}
// noisy() has a messaging service for giving messages to the player
// as appropriate.
bool noisy(int original_loudness, const coord_def& where,
const char *msg, mid_t who, bool fake_noise)
{
ASSERT_IN_BOUNDS(where);
if (original_loudness <= 0)
return false;
// high ambient noise makes sounds harder to hear
const int ambient = ambient_noise();
const int loudness =
ambient < 0 ? original_loudness + random2avg(abs(ambient), 3)
: original_loudness - random2avg(abs(ambient), 3);
int adj_loudness = loudness;
if (you.see_cell(where))
{
if (have_passive(passive_t::dampen_noise))
adj_loudness = div_rand_round(adj_loudness, 2);
if (you.unrand_equipped(UNRAND_THIEF))
adj_loudness = div_rand_round(adj_loudness, 2);
}
dprf(DIAG_NOISE, "Noise %d (orig: %d; ambient: %d) at pos(%d,%d)",
adj_loudness, original_loudness, ambient, where.x, where.y);
if (adj_loudness <= 0)
return false;
// If the origin is silenced there is no noise, unless we're
// faking it.
if (silenced(where) && !fake_noise)
return false;
// [ds] Reduce noise propagation for Sprint.
const int scaled_loudness =
crawl_state.game_is_sprint()? max(1, div_rand_round(adj_loudness, 2))
: adj_loudness;
// The multiplier converts to milli-auns which are used internally
// by noise propagation.
const int multiplier = 1000;
// Add +1 to scaled_loudness so that all squares adjacent to a
// sound of loudness 1 will hear the sound.
const string noise_msg(msg ? msg : "");
_noise_grid.register_noise(
noise_t(where, noise_msg, (scaled_loudness + 1) * multiplier, who,
fake_noise));
// Some users of noisy() want an immediate answer to whether the
// player heard the noise. The deferred noise system also means
// that soft noises can be drowned out by loud noises. For both
// these reasons, use the simple old noise system to check if the
// player heard the noise:
const int dist = loudness;
const int player_distance = grid_distance(you.pos(), where);
// Message the player.
if (player_distance <= dist && player_can_hear(where))
{
if (msg && !fake_noise)
mprf(MSGCH_SOUND, "%s", msg);
return true;
}
return false;
}
bool noisy(int loudness, const coord_def& where, mid_t who)
{
return noisy(loudness, where, nullptr, who);
}
// This fakes noise even through silence.
bool fake_noisy(int loudness, const coord_def& where)
{
return noisy(loudness, where, nullptr, MID_NOBODY, true);
}
//////////////////////////////////////////////////////////////////////////////
// noise machinery
// Currently noise attenuation depends solely on the feature in question.
// Permarock walls are assumed to completely kill noise.
static int _noise_attenuation_millis(const coord_def &pos)
{
const dungeon_feature_type feat = env.grid(pos);
if (feat_is_permarock(feat))
return NOISE_ATTENUATION_COMPLETE;
return BASE_NOISE_ATTENUATION_MILLIS *
(feat_is_wall(feat) ? 12 :
feat_is_closed_door(feat) ? 8 :
feat_is_tree(feat) ? 3 :
feat_is_statuelike(feat) ? 2 :
1);
}
noise_cell::noise_cell()
: neighbour_delta(0, 0), noise_id(-1), noise_intensity_millis(0),
noise_travel_distance(0)
{
}
bool noise_cell::can_apply_noise(int _noise_intensity_millis) const
{
return noise_intensity_millis < _noise_intensity_millis;
}
bool noise_cell::apply_noise(int _noise_intensity_millis,
int _noise_id,
int _noise_travel_distance,
const coord_def &_neighbour_delta)
{
if (can_apply_noise(_noise_intensity_millis))
{
noise_id = _noise_id;
noise_intensity_millis = _noise_intensity_millis;
noise_travel_distance = _noise_travel_distance;
neighbour_delta = _neighbour_delta;
return true;
}
return false;
}
int noise_cell::turn_angle(const coord_def &next_delta) const
{
if (neighbour_delta.origin())
return 0;
// Going in reverse?
if (next_delta.x == -neighbour_delta.x
&& next_delta.y == -neighbour_delta.y)
{
return 4;
}
const int xdiff = abs(neighbour_delta.x - next_delta.x);
const int ydiff = abs(neighbour_delta.y - next_delta.y);
return xdiff + ydiff;
}
noise_grid::noise_grid()
: cells(), noises(), affected_actor_count(0)
{
}
void noise_grid::reset()
{
cells.init(noise_cell());
noises.clear();
affected_actor_count = 0;
}
void noise_grid::register_noise(const noise_t &noise)
{
noise_cell &target_cell(cells(noise.noise_source));
if (target_cell.can_apply_noise(noise.noise_intensity_millis))
{
const int noise_index = noises.size();
noises.push_back(noise);
noises[noise_index].noise_id = noise_index;
cells(noise.noise_source).apply_noise(noise.noise_intensity_millis,
noise_index,
0,
coord_def(0, 0));
}
}
void noise_grid::propagate_noise()
{
if (noises.empty())
return;
#ifdef DEBUG_NOISE_PROPAGATION
dprf(DIAG_NOISE, "noise_grid: %u noises to apply",
(unsigned int)noises.size());
#endif
vector<coord_def> noise_perimeter[2];
int circ_index = 0;
for (const noise_t &noise : noises)
noise_perimeter[circ_index].push_back(noise.noise_source);
int travel_distance = 0;
while (!noise_perimeter[circ_index].empty())
{
const vector<coord_def> &perimeter(noise_perimeter[circ_index]);
vector<coord_def> &next_perimeter(noise_perimeter[!circ_index]);
++travel_distance;
for (const coord_def &p : perimeter)
{
const noise_cell &cell(cells(p));
if (!cell.silent())
{
apply_noise_effects(p,
cell.noise_intensity_millis,
noises[cell.noise_id]);
const int attenuation = _noise_attenuation_millis(p);
// If the base noise attenuation kills the noise, go no farther:
if (noise_is_audible(cell.noise_intensity_millis - attenuation))
{
// [ds] Not using adjacent iterator which has
// unnecessary overhead for the tight loop here.
for (int xi = -1; xi <= 1; ++xi)
{
for (int yi = -1; yi <= 1; ++yi)
{
if (xi || yi)
{
const coord_def next_position(p.x + xi,
p.y + yi);
if (in_bounds(next_position))
{
if (propagate_noise_to_neighbour(
attenuation,
travel_distance,
cell, p,
next_position))
{
next_perimeter.push_back(next_position);
}
}
}
}
}
}
}
}
noise_perimeter[circ_index].clear();
circ_index = !circ_index;
}
#ifdef DEBUG_NOISE_PROPAGATION
if (affected_actor_count)
{
mprf(MSGCH_WARN, "Writing noise grid with %d noise sources",
(int) noises.size());
dump_noise_grid("noise-grid.html");
}
#endif
}
bool noise_grid::propagate_noise_to_neighbour(int base_attenuation,
int travel_distance,
const noise_cell &cell,
const coord_def ¤t_pos,
const coord_def &next_pos)
{
noise_cell &neighbour(cells(next_pos));
if (!neighbour.can_apply_noise(cell.noise_intensity_millis
- base_attenuation))
{
return false;
}
const int noise_turn_angle = cell.turn_angle(next_pos - current_pos);
const int turn_attenuation =
noise_turn_angle? (base_attenuation * (100 + noise_turn_angle * 25)
/ 100)
: base_attenuation;
const int attenuated_noise_intensity =
cell.noise_intensity_millis - turn_attenuation;
if (noise_is_audible(attenuated_noise_intensity))
{
const int neighbour_old_distance = neighbour.noise_travel_distance;
if (neighbour.apply_noise(attenuated_noise_intensity,
cell.noise_id,
travel_distance,
next_pos - current_pos))
// Return true only if we hadn't already registered this
// cell as a neighbour (presumably with a lower volume).
return neighbour_old_distance != travel_distance;
}
return false;
}
void noise_grid::apply_noise_effects(const coord_def &pos,
int noise_intensity_millis,
const noise_t &noise)
{
// Real noises don't have any effect in silenced squares.
if (silenced(pos) && !noise.fake_noise)
return;
if (you.pos() == pos)
{
// This stores noise heard at the player's position for
// display in the HUD. A more interesting (and much more complicated)
// way of doing this might be to sample from the noise grid at
// selected distances from the player. Dealing with terrain is a bit
// nightmarish for this alternative, though, so I'm going to keep it
// simple.
you.los_noise_level = max(you.los_noise_level, noise_intensity_millis);
}
if (monster *mons = monster_at(pos))
{
if (mons->alive()
&& !mons_is_deep_asleep(*mons)
&& mons->mid != noise.noise_producer_mid)
{
const coord_def perceived_position =
noise_perceived_position(mons, pos, noise);
_monster_apply_noise(mons, perceived_position,
noise_intensity_millis);
++affected_actor_count;
}
}
}
// Given an actor at affected_pos and a given noise, calculates where
// the actor might think the noise originated.
//
// [ds] Let's keep this brutally simple, since the player will
// probably not notice even if we get very clever:
//
// - If the cells can see each other, return the actual source position.
//
// - If the cells cannot see each other, calculate a noise source as follows:
//
// Calculate a noise centroid between the noise source and the observer,
// weighted to the noise source if the noise has travelled in a straight line,
// weighted toward the observer the more the noise has deviated from a
// straight line.
//
// Fuzz the centroid by the extra distance the noise has travelled over
// the straight line distance. This is where the observer will think the
// noise originated.
//
// Thus, if the noise has travelled in a straight line, the observer
// will know the exact origin, 100% of the time, even if the
// observer is all the way across the level.
coord_def noise_grid::noise_perceived_position(actor *act,
const coord_def &affected_pos,
const noise_t &noise) const
{
const int noise_travel_distance = cells(affected_pos).noise_travel_distance;
if (!noise_travel_distance)
return noise.noise_source;
const int cell_grid_distance =
grid_distance(affected_pos, noise.noise_source);
if (cell_grid_distance <= LOS_RADIUS)
{
if (act->see_cell(noise.noise_source))
return noise.noise_source;
}
const int extra_distance_covered =
noise_travel_distance - cell_grid_distance;
const int source_weight = 200;
const int target_weight =
extra_distance_covered?
75 * extra_distance_covered / cell_grid_distance
: 0;
const coord_def noise_centroid =
target_weight?
(noise.noise_source * source_weight + affected_pos * target_weight)
/ (source_weight + target_weight)
: noise.noise_source;
const int fuzz = extra_distance_covered;
coord_def fuzz_rnd;
fuzz_rnd.x = random_range(-fuzz, fuzz, 2);
fuzz_rnd.y = random_range(-fuzz, fuzz, 2);
const coord_def perceived_point =
fuzz?
noise_centroid + fuzz_rnd
: noise_centroid;
const coord_def final_perceived_point =
!in_bounds(perceived_point)?
clamp_in_bounds(perceived_point)
: perceived_point;
#ifdef DEBUG_NOISE_PROPAGATION
dprf(DIAG_NOISE, "[NOISE] Noise perceived by %s at (%d,%d) "
"centroid (%d,%d) source (%d,%d) "
"heard at (%d,%d), distance: %d (travelled %d)",
act->name(DESC_PLAIN, true).c_str(),
final_perceived_point.x, final_perceived_point.y,
noise_centroid.x, noise_centroid.y,
noise.noise_source.x, noise.noise_source.y,
affected_pos.x, affected_pos.y,
cell_grid_distance, noise_travel_distance);
#endif
return final_perceived_point;
}
#ifdef DEBUG_NOISE_PROPAGATION
#include <cmath>
// Return HTML RGB triple given a hue and assuming chroma of 0.86 (220)
static string _hue_rgb(int hue)
{
const int chroma = 220;
const double hue2 = hue / 60.0;
const int x = chroma * (1.0 - fabs(hue2 - floor(hue2 / 2) * 2 - 1));
int red = 0, green = 0, blue = 0;
if (hue2 < 1)
red = chroma, green = x;
else if (hue2 < 2)
red = x, green = chroma;
else if (hue2 < 3)
green = chroma, blue = x;
else if (hue2 < 4)
green = x, blue = chroma;
else
red = x, blue = chroma;
// Other hues are not generated, so skip them.
return make_stringf("%02x%02x%02x", red, green, blue);
}
static string _noise_intensity_styles()
{
// Hi-intensity sound will be red (HSV 0), low intensity blue (HSV 240).
const int hi_hue = 0;
const int lo_hue = 240;
const int huespan = lo_hue - hi_hue;
const int max_intensity = 25;
string styles;
for (int intensity = 1; intensity <= max_intensity; ++intensity)
{
const int hue = lo_hue - intensity * huespan / max_intensity;
styles += make_stringf(".i%d { background: #%s }\n",
intensity, _hue_rgb(hue).c_str());
}
return styles;
}
static void _write_noise_grid_css(FILE *outf)
{
fprintf(outf,
"<style type='text/css'>\n"
"body { font-family: monospace; padding: 0; margin: 0; "
"line-height: 100%% }\n"
"%s\n"
"</style>",
_noise_intensity_styles().c_str());
}
void noise_grid::write_cell(FILE *outf, coord_def p, int ch) const
{
const int intensity = min(25, cells(p).noise_intensity_millis / 1000);
if (intensity)
fprintf(outf, "<span class='i%d'>&#%d;</span>", intensity, ch);
else
fprintf(outf, "<span>&#%d;</span>", ch);
}
void noise_grid::write_noise_grid(FILE *outf) const
{
// Duplicate the screenshot() trick.
FixedVector<char32_t, NUM_DCHAR_TYPES> char_table_bk;
char_table_bk = Options.char_table;
init_char_table(CSET_ASCII);
init_show_table();
fprintf(outf, "<div>\n");
// Write the whole map out without checking for mappedness. Handy
// for debugging level-generation issues.
for (int y = 0; y < GYM; ++y)
{
for (int x = 0; x < GXM; ++x)
{
coord_def p(x, y);
if (you.pos() == coord_def(x, y))
write_cell(outf, p, '@');
else
write_cell(outf, p, get_feature_def(env.grid[x][y]).symbol());
}
fprintf(outf, "<br>\n");
}
fprintf(outf, "</div>\n");
}
void noise_grid::dump_noise_grid(const string &filename) const
{
FILE *outf = fopen_u(filename.c_str(), "w");
fprintf(outf, "<!DOCTYPE html><html><head>");
_write_noise_grid_css(outf);
fprintf(outf, "</head>\n<body>\n");
write_noise_grid(outf);
fprintf(outf, "</body></html>\n");
fclose(outf);
}
#endif
static void _monster_apply_noise(monster *mons,
const coord_def &apparent_source,
int noise_intensity_millis)
{
#ifdef DEBUG_NOISE_PROPAGATION
dprf(DIAG_NOISE, "[NOISE] Actor %s (%d,%d) perceives noise (%d) "
"from (%d,%d), real source (%d,%d), distance: %d, noise travelled: %d",
act->name(DESC_PLAIN, true).c_str(),
act->pos().x, act->pos().y,
noise_intensity_millis,
apparent_source.x, apparent_source.y,
noise.noise_source.x, noise.noise_source.y,
grid_distance(act->pos(), noise.noise_source),
noise_travel_distance);
#else
UNUSED(noise_intensity_millis);
#endif
// If the perceived noise came from within the player's LOS, any
// monster that hears it will have a chance to guess that the
// noise was triggered by the player, depending on how close it was to
// the player. This happens independently of actual noise source.
//
// The probability is calculated as p(x) = (-90/R)x + 100, which is
// linear from p(0) = 100 to p(R) = 10. This replaces a version that
// was 100% from 0 to 3, and 0% outward.
//
// behaviour around the old breakpoint for R=8: p(3) = 66, p(4) = 55.
const int player_distance = grid_distance(apparent_source, you.pos());
const int alert_prob = max(player_distance * -90 / LOS_RADIUS + 100, 0);
if (x_chance_in_y(alert_prob, 100))
behaviour_event(mons, ME_ALERT, &you, apparent_source); // shout + set you as foe
else
behaviour_event(mons, ME_DISTURB, 0, apparent_source); // wake
}
|