1 /*
2 * Monsters.c
3 * Brogue
4 *
5 * Created by Brian Walker on 1/13/09.
6 * Copyright 2012. All rights reserved.
7 *
8 * This file is part of Brogue.
9 *
10 * This program is free software: you can redistribute it and/or modify
11 * it under the terms of the GNU Affero General Public License as
12 * published by the Free Software Foundation, either version 3 of the
13 * License, or (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU Affero General Public License for more details.
19 *
20 * You should have received a copy of the GNU Affero General Public License
21 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 */
23
24 #include "Rogue.h"
25 #include "IncludeGlobals.h"
26
mutateMonster(creature * monst,short mutationIndex)27 void mutateMonster(creature *monst, short mutationIndex) {
28 monst->mutationIndex = mutationIndex;
29 const mutation *theMut = &(mutationCatalog[mutationIndex]);
30 monst->info.flags |= theMut->monsterFlags;
31 monst->info.abilityFlags |= theMut->monsterAbilityFlags;
32 monst->info.maxHP = monst->info.maxHP * theMut->healthFactor / 100;
33 monst->info.movementSpeed = monst->info.movementSpeed * theMut->moveSpeedFactor / 100;
34 monst->info.attackSpeed = monst->info.attackSpeed * theMut->attackSpeedFactor / 100;
35 monst->info.defense = monst->info.defense * theMut->defenseFactor / 100;
36 if (monst->info.damage.lowerBound > 0) {
37 monst->info.damage.lowerBound = monst->info.damage.lowerBound * theMut->damageFactor / 100;
38 monst->info.damage.lowerBound = max(monst->info.damage.lowerBound, 1);
39 }
40 if (monst->info.damage.upperBound > 0) {
41 monst->info.damage.upperBound = monst->info.damage.upperBound * theMut->damageFactor / 100;
42 monst->info.damage.upperBound = max(monst->info.damage.upperBound, (monst->info.abilityFlags & MA_POISONS) ? 2 : 1);
43 }
44 if (theMut->DFChance >= 0) {
45 monst->info.DFChance = theMut->DFChance;
46 }
47 if (theMut->DFType > 0) {
48 monst->info.DFType = theMut->DFType;
49 }
50 }
51
52 // Allocates space, generates a creature of the given type,
53 // prepends it to the list of creatures, and returns a pointer to that creature. Note that the creature
54 // is not given a map location here!
55 // TODO: generateMonster is convenient, but probably it should not add the monster to
56 // any global lists. The caller can do this, to avoid needlessly moving them elsewhere.
generateMonster(short monsterID,boolean itemPossible,boolean mutationPossible)57 creature *generateMonster(short monsterID, boolean itemPossible, boolean mutationPossible) {
58 short itemChance, mutationChance, i, mutationAttempt;
59
60 // 1.17^x * 10, with x from 1 to 13:
61 const int POW_DEEP_MUTATION[] = {11, 13, 16, 18, 21, 25, 30, 35, 41, 48, 56, 65, 76};
62
63 creature *monst = calloc(1, sizeof(creature));
64 clearStatus(monst);
65 monst->info = monsterCatalog[monsterID];
66
67 monst->mutationIndex = -1;
68 if (mutationPossible
69 && !(monst->info.flags & MONST_NEVER_MUTATED)
70 && !(monst->info.abilityFlags & MA_NEVER_MUTATED)
71 && rogue.depthLevel > 10) {
72
73
74 if (rogue.depthLevel <= AMULET_LEVEL) {
75 mutationChance = clamp(rogue.depthLevel - 10, 1, 10);
76 } else {
77 mutationChance = POW_DEEP_MUTATION[min(rogue.depthLevel - AMULET_LEVEL, 12)];
78 mutationChance = min(mutationChance, 75);
79 }
80
81 if (rand_percent(mutationChance)) {
82 mutationAttempt = rand_range(0, NUMBER_MUTATORS - 1);
83 if (!(monst->info.flags & mutationCatalog[mutationAttempt].forbiddenFlags)
84 && !(monst->info.abilityFlags & mutationCatalog[mutationAttempt].forbiddenAbilityFlags)) {
85
86 mutateMonster(monst, mutationAttempt);
87 }
88 }
89 }
90
91 prependCreature(monsters, monst);
92 monst->xLoc = monst->yLoc = 0;
93 monst->depth = rogue.depthLevel;
94 monst->bookkeepingFlags = 0;
95 monst->mapToMe = NULL;
96 monst->safetyMap = NULL;
97 monst->leader = NULL;
98 monst->carriedMonster = NULL;
99 monst->creatureState = (((monst->info.flags & MONST_NEVER_SLEEPS) || rand_percent(25))
100 ? MONSTER_TRACKING_SCENT : MONSTER_SLEEPING);
101 monst->creatureMode = MODE_NORMAL;
102 monst->currentHP = monst->info.maxHP;
103 monst->spawnDepth = rogue.depthLevel;
104 monst->ticksUntilTurn = monst->info.movementSpeed;
105 monst->info.turnsBetweenRegen *= 1000; // tracked as thousandths to prevent rounding errors
106 monst->turnsUntilRegen = monst->info.turnsBetweenRegen;
107 monst->regenPerTurn = 0;
108 monst->movementSpeed = monst->info.movementSpeed;
109 monst->attackSpeed = monst->info.attackSpeed;
110 monst->turnsSpentStationary = 0;
111 monst->xpxp = 0;
112 monst->machineHome = 0;
113 monst->newPowerCount = monst->totalPowerCount = 0;
114 monst->targetCorpseLoc[0] = monst->targetCorpseLoc[1] = 0;
115 monst->lastSeenPlayerAt[0] = monst->lastSeenPlayerAt[1] = -1;
116 monst->targetWaypointIndex = -1;
117 for (i=0; i < MAX_WAYPOINT_COUNT; i++) {
118 monst->waypointAlreadyVisited[i] = rand_range(0, 1);
119 }
120
121 if (monst->info.flags & MONST_FIERY) {
122 monst->status[STATUS_BURNING] = monst->maxStatus[STATUS_BURNING] = 1000; // won't decrease
123 }
124 if (monst->info.flags & MONST_FLIES) {
125 monst->status[STATUS_LEVITATING] = monst->maxStatus[STATUS_LEVITATING] = 1000; // won't decrease
126 }
127 if (monst->info.flags & MONST_IMMUNE_TO_FIRE) {
128 monst->status[STATUS_IMMUNE_TO_FIRE] = monst->maxStatus[STATUS_IMMUNE_TO_FIRE] = 1000; // won't decrease
129 }
130 if (monst->info.flags & MONST_INVISIBLE) {
131 monst->status[STATUS_INVISIBLE] = monst->maxStatus[STATUS_INVISIBLE] = 1000; // won't decrease
132 }
133 monst->status[STATUS_NUTRITION] = monst->maxStatus[STATUS_NUTRITION] = 1000;
134
135 if (monst->info.flags & MONST_CARRY_ITEM_100) {
136 itemChance = 100;
137 } else if (monst->info.flags & MONST_CARRY_ITEM_25) {
138 itemChance = 25;
139 } else {
140 itemChance = 0;
141 }
142
143 if (ITEMS_ENABLED
144 && itemPossible
145 && (rogue.depthLevel <= AMULET_LEVEL)
146 && monsterItemsHopper->nextItem
147 && rand_percent(itemChance)) {
148
149 monst->carriedItem = monsterItemsHopper->nextItem;
150 monsterItemsHopper->nextItem = monsterItemsHopper->nextItem->nextItem;
151 monst->carriedItem->nextItem = NULL;
152 monst->carriedItem->originDepth = rogue.depthLevel;
153 } else {
154 monst->carriedItem = NULL;
155 }
156
157 initializeGender(monst);
158
159 if (!(monst->info.flags & MONST_INANIMATE) && !monst->status[STATUS_LIFESPAN_REMAINING]) {
160 monst->bookkeepingFlags |= MB_HAS_SOUL;
161 }
162
163 return monst;
164 }
165
monsterRevealed(creature * monst)166 boolean monsterRevealed(creature *monst) {
167 if (monst == &player) {
168 return false;
169 } else if (monst->bookkeepingFlags & MB_TELEPATHICALLY_REVEALED) {
170 return true;
171 } else if (monst->status[STATUS_ENTRANCED]) {
172 return true;
173 } else if (player.status[STATUS_TELEPATHIC] && !(monst->info.flags & MONST_INANIMATE)) {
174 return true;
175 }
176 return false;
177 }
178
monsterHiddenBySubmersion(const creature * monst,const creature * observer)179 boolean monsterHiddenBySubmersion(const creature *monst, const creature *observer) {
180 if (monst->bookkeepingFlags & MB_SUBMERGED) {
181 if (observer
182 && (terrainFlags(observer->xLoc, observer->yLoc) & T_IS_DEEP_WATER)
183 && !observer->status[STATUS_LEVITATING]) {
184 // observer is in deep water, so target is not hidden by water
185 return false;
186 } else {
187 // submerged and the observer is not in deep water.
188 return true;
189 }
190 }
191 return false;
192 }
193
monsterIsHidden(const creature * monst,const creature * observer)194 boolean monsterIsHidden(const creature *monst, const creature *observer) {
195 if (monst->bookkeepingFlags & MB_IS_DORMANT) {
196 return true;
197 }
198 if (observer && monstersAreTeammates(monst, observer)) {
199 // Teammates can always see each other.
200 return false;
201 }
202 if ((monst->status[STATUS_INVISIBLE] && !pmap[monst->xLoc][monst->yLoc].layers[GAS])) {
203 // invisible and not in gas
204 return true;
205 }
206 if (monsterHiddenBySubmersion(monst, observer)) {
207 return true;
208 }
209 return false;
210 }
211
canSeeMonster(creature * monst)212 boolean canSeeMonster(creature *monst) {
213 if (monst == &player) {
214 return true;
215 }
216 if (!monsterIsHidden(monst, &player)
217 && (playerCanSee(monst->xLoc, monst->yLoc) || monsterRevealed(monst))) {
218 return true;
219 }
220 return false;
221 }
222
223 // This is different from canSeeMonster() in that it counts only physical sight -- not clairvoyance or telepathy.
canDirectlySeeMonster(creature * monst)224 boolean canDirectlySeeMonster(creature *monst) {
225 if (monst == &player) {
226 return true;
227 }
228 if (playerCanDirectlySee(monst->xLoc, monst->yLoc) && !monsterIsHidden(monst, &player)) {
229 return true;
230 }
231 return false;
232 }
233
monsterName(char * buf,creature * monst,boolean includeArticle)234 void monsterName(char *buf, creature *monst, boolean includeArticle) {
235 short oldRNG;
236
237 if (monst == &player) {
238 strcpy(buf, "you");
239 return;
240 }
241 if (canSeeMonster(monst) || rogue.playbackOmniscience) {
242 if (player.status[STATUS_HALLUCINATING] && !rogue.playbackOmniscience) {
243
244 oldRNG = rogue.RNG;
245 rogue.RNG = RNG_COSMETIC;
246 //assureCosmeticRNG;
247 sprintf(buf, "%s%s", (includeArticle ? "the " : ""),
248 monsterCatalog[rand_range(1, NUMBER_MONSTER_KINDS - 1)].monsterName);
249 restoreRNG;
250
251 return;
252 }
253 sprintf(buf, "%s%s", (includeArticle ? (monst->creatureState == MONSTER_ALLY ? "your " : "the ") : ""),
254 monst->info.monsterName);
255 //monsterText[monst->info.monsterID].name);
256 return;
257 } else {
258 strcpy(buf, "something");
259 return;
260 }
261 }
262
monsterIsInClass(const creature * monst,const short monsterClass)263 boolean monsterIsInClass(const creature *monst, const short monsterClass) {
264 short i;
265 for (i = 0; monsterClassCatalog[monsterClass].memberList[i] != 0; i++) {
266 if (monsterClassCatalog[monsterClass].memberList[i] == monst->info.monsterID) {
267 return true;
268 }
269 }
270 return false;
271 }
272
273 // Don't attack a revenant if you're not magical.
274 // Don't attack a monster embedded in obstruction crystal.
275 // Etc.
attackWouldBeFutile(const creature * attacker,const creature * defender)276 boolean attackWouldBeFutile(const creature *attacker, const creature *defender) {
277 if (cellHasTerrainFlag(defender->xLoc, defender->yLoc, T_OBSTRUCTS_PASSABILITY)
278 && !(defender->info.flags & MONST_ATTACKABLE_THRU_WALLS)) {
279 return true;
280 }
281 if (attacker == &player) {
282 // Let the player do what she wants, if it's possible.
283 return false;
284 }
285 if ((attacker->info.flags & MONST_RESTRICTED_TO_LIQUID)
286 && !(attacker->status[STATUS_LEVITATING])
287 && defender->status[STATUS_LEVITATING]) {
288 return true;
289 }
290 if (defender->info.flags & MONST_INVULNERABLE) {
291 return true;
292 }
293 if (defender->info.flags & MONST_IMMUNE_TO_WEAPONS
294 && !(attacker->info.abilityFlags & MA_POISONS)) {
295 return true;
296 }
297 return false;
298 }
299
300 // This is a specific kind of willingness, bordering on ability.
301 // Intuition: if it swung an axe from that position, should it
302 // hit the defender? Or silently pass through it, as it does for
303 // allies?
monsterWillAttackTarget(const creature * attacker,const creature * defender)304 boolean monsterWillAttackTarget(const creature *attacker, const creature *defender) {
305 if (attacker == defender || (defender->bookkeepingFlags & MB_IS_DYING)) {
306 return false;
307 }
308 if (attacker == &player
309 && defender->creatureState == MONSTER_ALLY) {
310
311 return false;
312 }
313 if (attacker->status[STATUS_ENTRANCED]
314 && defender->creatureState != MONSTER_ALLY) {
315
316 return true;
317 }
318 if (attacker->creatureState == MONSTER_ALLY
319 && attacker != &player
320 && defender->status[STATUS_ENTRANCED]) {
321
322 return false;
323 }
324 if (defender->bookkeepingFlags & MB_CAPTIVE) {
325 return false;
326 }
327 if (attacker->status[STATUS_DISCORDANT]
328 || defender->status[STATUS_DISCORDANT]
329 || attacker->status[STATUS_CONFUSED]) {
330
331 return true;
332 }
333 if (monstersAreEnemies(attacker, defender)
334 && !monstersAreTeammates(attacker, defender)) {
335 return true;
336 }
337 return false;
338 }
339
monstersAreTeammates(const creature * monst1,const creature * monst2)340 boolean monstersAreTeammates(const creature *monst1, const creature *monst2) {
341 // if one follows the other, or the other follows the one, or they both follow the same
342 return ((((monst1->bookkeepingFlags & MB_FOLLOWER) && monst1->leader == monst2)
343 || ((monst2->bookkeepingFlags & MB_FOLLOWER) && monst2->leader == monst1)
344 || (monst1->creatureState == MONSTER_ALLY && monst2 == &player)
345 || (monst1 == &player && monst2->creatureState == MONSTER_ALLY)
346 || (monst1->creatureState == MONSTER_ALLY && monst2->creatureState == MONSTER_ALLY)
347 || ((monst1->bookkeepingFlags & MB_FOLLOWER) && (monst2->bookkeepingFlags & MB_FOLLOWER)
348 && monst1->leader == monst2->leader)) ? true : false);
349 }
350
monstersAreEnemies(const creature * monst1,const creature * monst2)351 boolean monstersAreEnemies(const creature *monst1, const creature *monst2) {
352 if ((monst1->bookkeepingFlags | monst2->bookkeepingFlags) & MB_CAPTIVE) {
353 return false;
354 }
355 if (monst1 == monst2) {
356 return false; // Can't be enemies with yourself, even if discordant.
357 }
358 if (monst1->status[STATUS_DISCORDANT] || monst2->status[STATUS_DISCORDANT]) {
359 return true;
360 }
361 // eels and krakens attack anything in deep water
362 if (((monst1->info.flags & MONST_RESTRICTED_TO_LIQUID)
363 && !(monst2->info.flags & MONST_IMMUNE_TO_WATER)
364 && !(monst2->status[STATUS_LEVITATING])
365 && cellHasTerrainFlag(monst2->xLoc, monst2->yLoc, T_IS_DEEP_WATER))
366
367 || ((monst2->info.flags & MONST_RESTRICTED_TO_LIQUID)
368 && !(monst1->info.flags & MONST_IMMUNE_TO_WATER)
369 && !(monst1->status[STATUS_LEVITATING])
370 && cellHasTerrainFlag(monst1->xLoc, monst1->yLoc, T_IS_DEEP_WATER))) {
371
372 return true;
373 }
374 return ((monst1->creatureState == MONSTER_ALLY || monst1 == &player)
375 != (monst2->creatureState == MONSTER_ALLY || monst2 == &player));
376 }
377
378
initializeGender(creature * monst)379 void initializeGender(creature *monst) {
380 if ((monst->info.flags & MONST_MALE) && (monst->info.flags & MONST_FEMALE)) {
381 monst->info.flags &= ~(rand_percent(50) ? MONST_MALE : MONST_FEMALE);
382 }
383 }
384
385 // Returns true if either string has a null terminator before they otherwise disagree.
stringsMatch(const char * str1,const char * str2)386 boolean stringsMatch(const char *str1, const char *str2) {
387 short i;
388
389 for (i=0; str1[i] && str2[i]; i++) {
390 if (str1[i] != str2[i]) {
391 return false;
392 }
393 }
394 return true;
395 }
396
397 // Genders:
398 // 0 = [character escape sequence]
399 // 1 = you
400 // 2 = male
401 // 3 = female
402 // 4 = neuter
resolvePronounEscapes(char * text,creature * monst)403 void resolvePronounEscapes(char *text, creature *monst) {
404 short pronounType, gender, i;
405 char *insert, *scan;
406 boolean capitalize;
407 // Note: Escape sequences MUST be longer than EACH of the possible replacements.
408 // That way, the string only contracts, and we don't need a buffer.
409 const char pronouns[4][5][20] = {
410 {"$HESHE", "you", "he", "she", "it"},
411 {"$HIMHER", "you", "him", "her", "it"},
412 {"$HISHER", "your", "his", "her", "its"},
413 {"$HIMSELFHERSELF", "yourself", "himself", "herself", "itself"}};
414
415 if (monst == &player) {
416 gender = 1;
417 } else if (!canSeeMonster(monst) && !rogue.playbackOmniscience) {
418 gender = 4;
419 } else if (monst->info.flags & MONST_MALE) {
420 gender = 2;
421 } else if (monst->info.flags & MONST_FEMALE) {
422 gender = 3;
423 } else {
424 gender = 4;
425 }
426
427 capitalize = false;
428
429 for (insert = scan = text; *scan;) {
430 if (scan[0] == '$') {
431 for (pronounType=0; pronounType<4; pronounType++) {
432 if (stringsMatch(pronouns[pronounType][0], scan)) {
433 strcpy(insert, pronouns[pronounType][gender]);
434 if (capitalize) {
435 upperCase(insert);
436 capitalize = false;
437 }
438 scan += strlen(pronouns[pronounType][0]);
439 insert += strlen(pronouns[pronounType][gender]);
440 break;
441 }
442 }
443 if (pronounType == 4) {
444 // Started with a '$' but didn't match an escape sequence; just copy the character and move on.
445 *(insert++) = *(scan++);
446 }
447 } else if (scan[0] == COLOR_ESCAPE) {
448 for (i=0; i<4; i++) {
449 *(insert++) = *(scan++);
450 }
451 } else { // Didn't match any of the escape sequences; copy the character instead.
452 if (*scan == '.') {
453 capitalize = true;
454 } else if (*scan != ' ') {
455 capitalize = false;
456 }
457
458 *(insert++) = *(scan++);
459 }
460 }
461 *insert = '\0';
462 }
463
464 /*
465 Returns a random horde, weighted by spawn frequency, which has all requiredFlags
466 and does not have any forbiddenFlags. If summonerType is 0, all hordes valid on
467 the given depth are considered. (Depth 0 means current depth.) Otherwise, all
468 hordes with summonerType as a leader are considered.
469 */
pickHordeType(short depth,enum monsterTypes summonerType,unsigned long forbiddenFlags,unsigned long requiredFlags)470 short pickHordeType(short depth, enum monsterTypes summonerType, unsigned long forbiddenFlags, unsigned long requiredFlags) {
471 short i, index, possCount = 0;
472
473 if (depth <= 0) {
474 depth = rogue.depthLevel;
475 }
476
477 for (i=0; i<NUMBER_HORDES; i++) {
478 if (!(hordeCatalog[i].flags & forbiddenFlags)
479 && !(~(hordeCatalog[i].flags) & requiredFlags)
480 && ((!summonerType && hordeCatalog[i].minLevel <= depth && hordeCatalog[i].maxLevel >= depth)
481 || (summonerType && (hordeCatalog[i].flags & HORDE_IS_SUMMONED) && hordeCatalog[i].leaderType == summonerType))) {
482 possCount += hordeCatalog[i].frequency;
483 }
484 }
485
486 if (possCount == 0) {
487 return -1;
488 }
489
490 index = rand_range(1, possCount);
491
492 for (i=0; i<NUMBER_HORDES; i++) {
493 if (!(hordeCatalog[i].flags & forbiddenFlags)
494 && !(~(hordeCatalog[i].flags) & requiredFlags)
495 && ((!summonerType && hordeCatalog[i].minLevel <= depth && hordeCatalog[i].maxLevel >= depth)
496 || (summonerType && (hordeCatalog[i].flags & HORDE_IS_SUMMONED) && hordeCatalog[i].leaderType == summonerType))) {
497 if (index <= hordeCatalog[i].frequency) {
498 return i;
499 }
500 index -= hordeCatalog[i].frequency;
501 }
502 }
503 return 0; // should never happen
504 }
505
empowerMonster(creature * monst)506 void empowerMonster(creature *monst) {
507 char theMonsterName[100], buf[200];
508 monst->info.maxHP += 12;
509 monst->info.defense += 10;
510 monst->info.accuracy += 10;
511 monst->info.damage.lowerBound += max(1, monst->info.damage.lowerBound / 10);
512 monst->info.damage.upperBound += max(1, monst->info.damage.upperBound / 10);
513 monst->newPowerCount++;
514 monst->totalPowerCount++;
515 heal(monst, 100, true);
516
517 if (canSeeMonster(monst)) {
518 monsterName(theMonsterName, monst, true);
519 sprintf(buf, "%s looks stronger", theMonsterName);
520 combatMessage(buf, &advancementMessageColor);
521 }
522 }
523
524 // If placeClone is false, the clone won't get a location
525 // and won't set any HAS_MONSTER flags or cause any refreshes;
526 // it's just generated and inserted into the chains.
cloneMonster(creature * monst,boolean announce,boolean placeClone)527 creature *cloneMonster(creature *monst, boolean announce, boolean placeClone) {
528 char buf[DCOLS], monstName[DCOLS];
529 short jellyCount;
530
531 creature *newMonst = generateMonster(monst->info.monsterID, false, false);
532 *newMonst = *monst; // boink!
533
534 newMonst->carriedMonster = NULL; // Temporarily remove anything it's carrying.
535
536 initializeGender(newMonst);
537 newMonst->bookkeepingFlags &= ~(MB_LEADER | MB_CAPTIVE | MB_HAS_SOUL);
538 newMonst->bookkeepingFlags |= MB_FOLLOWER;
539 newMonst->mapToMe = NULL;
540 newMonst->safetyMap = NULL;
541 newMonst->carriedItem = NULL;
542 if (monst->carriedMonster) {
543 creature *parentMonst = cloneMonster(monst->carriedMonster, false, false); // Also clone the carriedMonster
544 removeCreature(monsters, parentMonst); // The cloned create will be added to the world, which we immediately undo.
545 removeCreature(dormantMonsters, parentMonst); // in case it's added as a dormant creature? TODO: is this possible?
546 }
547 newMonst->ticksUntilTurn = 101;
548 if (!(monst->creatureState == MONSTER_ALLY)) {
549 newMonst->bookkeepingFlags &= ~MB_TELEPATHICALLY_REVEALED;
550 }
551 if (monst->leader) {
552 newMonst->leader = monst->leader;
553 } else {
554 newMonst->leader = monst;
555 monst->bookkeepingFlags |= MB_LEADER;
556 }
557
558 if (monst->bookkeepingFlags & MB_CAPTIVE) {
559 // If you clone a captive, the clone will be your ally.
560 becomeAllyWith(newMonst);
561 }
562
563 if (placeClone) {
564 // getQualifyingLocNear(loc, monst->xLoc, monst->yLoc, true, 0, forbiddenFlagsForMonster(&(monst->info)), (HAS_PLAYER | HAS_MONSTER), false, false);
565 // newMonst->xLoc = loc[0];
566 // newMonst->yLoc = loc[1];
567 getQualifyingPathLocNear(&(newMonst->xLoc), &(newMonst->yLoc), monst->xLoc, monst->yLoc, true,
568 T_DIVIDES_LEVEL & avoidedFlagsForMonster(&(newMonst->info)), HAS_PLAYER,
569 avoidedFlagsForMonster(&(newMonst->info)), (HAS_PLAYER | HAS_MONSTER | HAS_STAIRS), false);
570 pmap[newMonst->xLoc][newMonst->yLoc].flags |= HAS_MONSTER;
571 refreshDungeonCell(newMonst->xLoc, newMonst->yLoc);
572 if (announce && canSeeMonster(newMonst)) {
573 monsterName(monstName, newMonst, false);
574 sprintf(buf, "another %s appears!", monstName);
575 message(buf, 0);
576 }
577 }
578
579 if (monst == &player) { // Player managed to clone himself.
580 newMonst->info.foreColor = &gray;
581 newMonst->info.damage.lowerBound = 1;
582 newMonst->info.damage.upperBound = 2;
583 newMonst->info.damage.clumpFactor = 1;
584 newMonst->info.defense = 0;
585 strcpy(newMonst->info.monsterName, "clone");
586 newMonst->creatureState = MONSTER_ALLY;
587 }
588
589 if (monst->creatureState == MONSTER_ALLY
590 && (monst->info.abilityFlags & MA_CLONE_SELF_ON_DEFEND)
591 && !rogue.featRecord[FEAT_JELLYMANCER]) {
592
593 jellyCount = 0;
594 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
595 creature *nextMonst = nextCreature(&it);
596 if (nextMonst->creatureState == MONSTER_ALLY
597 && (nextMonst->info.abilityFlags & MA_CLONE_SELF_ON_DEFEND)) {
598
599 jellyCount++;
600 }
601 }
602 if (jellyCount >= 90) {
603 rogue.featRecord[FEAT_JELLYMANCER] = true;
604 }
605 }
606 return newMonst;
607 }
608
forbiddenFlagsForMonster(creatureType * monsterType)609 unsigned long forbiddenFlagsForMonster(creatureType *monsterType) {
610 unsigned long flags;
611
612 flags = T_PATHING_BLOCKER;
613 if (monsterType->flags & MONST_INVULNERABLE) {
614 flags &= ~(T_LAVA_INSTA_DEATH | T_SPONTANEOUSLY_IGNITES | T_IS_FIRE);
615 }
616 if (monsterType->flags & (MONST_IMMUNE_TO_FIRE | MONST_FLIES)) {
617 flags &= ~T_LAVA_INSTA_DEATH;
618 }
619 if (monsterType->flags & MONST_IMMUNE_TO_FIRE) {
620 flags &= ~(T_SPONTANEOUSLY_IGNITES | T_IS_FIRE);
621 }
622 if (monsterType->flags & (MONST_IMMUNE_TO_WATER | MONST_FLIES)) {
623 flags &= ~T_IS_DEEP_WATER;
624 }
625 if (monsterType->flags & (MONST_FLIES)) {
626 flags &= ~(T_AUTO_DESCENT | T_IS_DF_TRAP);
627 }
628 return flags;
629 }
630
avoidedFlagsForMonster(creatureType * monsterType)631 unsigned long avoidedFlagsForMonster(creatureType *monsterType) {
632 unsigned long flags;
633
634 flags = forbiddenFlagsForMonster(monsterType) | T_HARMFUL_TERRAIN | T_SACRED;
635
636 if (monsterType->flags & MONST_INVULNERABLE) {
637 flags &= ~(T_HARMFUL_TERRAIN | T_IS_DF_TRAP);
638 }
639 if (monsterType->flags & MONST_INANIMATE) {
640 flags &= ~(T_CAUSES_POISON | T_CAUSES_DAMAGE | T_CAUSES_PARALYSIS | T_CAUSES_CONFUSION);
641 }
642 if (monsterType->flags & MONST_IMMUNE_TO_FIRE) {
643 flags &= ~T_IS_FIRE;
644 }
645 if (monsterType->flags & MONST_FLIES) {
646 flags &= ~T_CAUSES_POISON;
647 }
648 return flags;
649 }
650
monsterCanSubmergeNow(creature * monst)651 boolean monsterCanSubmergeNow(creature *monst) {
652 return ((monst->info.flags & MONST_SUBMERGES)
653 && cellHasTMFlag(monst->xLoc, monst->yLoc, TM_ALLOWS_SUBMERGING)
654 && !cellHasTerrainFlag(monst->xLoc, monst->yLoc, T_OBSTRUCTS_PASSABILITY)
655 && !(monst->bookkeepingFlags & (MB_SEIZING | MB_SEIZED | MB_CAPTIVE))
656 && ((monst->info.flags & (MONST_IMMUNE_TO_FIRE | MONST_INVULNERABLE))
657 || monst->status[STATUS_IMMUNE_TO_FIRE]
658 || !cellHasTerrainFlag(monst->xLoc, monst->yLoc, T_LAVA_INSTA_DEATH)));
659 }
660
661 // Returns true if at least one minion spawned.
spawnMinions(short hordeID,creature * leader,boolean summoned,boolean itemPossible)662 boolean spawnMinions(short hordeID, creature *leader, boolean summoned, boolean itemPossible) {
663 short iSpecies, iMember, count;
664 unsigned long forbiddenTerrainFlags;
665 hordeType *theHorde;
666 creature *monst;
667 short x, y;
668 short failsafe;
669 boolean atLeastOneMinion = false;
670
671 x = leader->xLoc;
672 y = leader->yLoc;
673
674 theHorde = &hordeCatalog[hordeID];
675
676 for (iSpecies = 0; iSpecies < theHorde->numberOfMemberTypes; iSpecies++) {
677 count = randClump(theHorde->memberCount[iSpecies]);
678
679 forbiddenTerrainFlags = forbiddenFlagsForMonster(&(monsterCatalog[theHorde->memberType[iSpecies]]));
680 if (hordeCatalog[hordeID].spawnsIn) {
681 forbiddenTerrainFlags &= ~(tileCatalog[hordeCatalog[hordeID].spawnsIn].flags);
682 }
683
684 for (iMember = 0; iMember < count; iMember++) {
685 monst = generateMonster(theHorde->memberType[iSpecies], itemPossible, !summoned);
686 failsafe = 0;
687 do {
688 getQualifyingPathLocNear(&(monst->xLoc), &(monst->yLoc), x, y, summoned,
689 T_DIVIDES_LEVEL & forbiddenTerrainFlags, (HAS_PLAYER | HAS_STAIRS),
690 forbiddenTerrainFlags, HAS_MONSTER, false);
691 } while (theHorde->spawnsIn && !cellHasTerrainType(monst->xLoc, monst->yLoc, theHorde->spawnsIn) && failsafe++ < 20);
692 if (failsafe >= 20) {
693 // abort
694 killCreature(monst, true);
695 break;
696 }
697 if (monsterCanSubmergeNow(monst)) {
698 monst->bookkeepingFlags |= MB_SUBMERGED;
699 }
700 brogueAssert(!(pmap[monst->xLoc][monst->yLoc].flags & HAS_MONSTER));
701 pmap[monst->xLoc][monst->yLoc].flags |= HAS_MONSTER;
702 monst->bookkeepingFlags |= (MB_FOLLOWER | MB_JUST_SUMMONED);
703 monst->leader = leader;
704 monst->creatureState = leader->creatureState;
705 monst->mapToMe = NULL;
706 if (theHorde->flags & HORDE_DIES_ON_LEADER_DEATH) {
707 monst->bookkeepingFlags |= MB_BOUND_TO_LEADER;
708 }
709 if (hordeCatalog[hordeID].flags & HORDE_ALLIED_WITH_PLAYER) {
710 becomeAllyWith(monst);
711 }
712 atLeastOneMinion = true;
713 }
714 }
715
716 if (atLeastOneMinion && !(theHorde->flags & HORDE_DIES_ON_LEADER_DEATH)) {
717 leader->bookkeepingFlags |= MB_LEADER;
718 }
719
720 return atLeastOneMinion;
721 }
722
drawManacle(short x,short y,enum directions dir)723 boolean drawManacle(short x, short y, enum directions dir) {
724 enum tileType manacles[8] = {MANACLE_T, MANACLE_B, MANACLE_L, MANACLE_R, MANACLE_TL, MANACLE_BL, MANACLE_TR, MANACLE_BR};
725 short newX = x + nbDirs[dir][0];
726 short newY = y + nbDirs[dir][1];
727 if (coordinatesAreInMap(newX, newY)
728 && pmap[newX][newY].layers[DUNGEON] == FLOOR
729 && pmap[newX][newY].layers[LIQUID] == NOTHING) {
730
731 pmap[x + nbDirs[dir][0]][y + nbDirs[dir][1]].layers[SURFACE] = manacles[dir];
732 return true;
733 }
734 return false;
735 }
736
drawManacles(short x,short y)737 void drawManacles(short x, short y) {
738 enum directions fallback[4][3] = {{UPLEFT, UP, LEFT}, {DOWNLEFT, DOWN, LEFT}, {UPRIGHT, UP, RIGHT}, {DOWNRIGHT, DOWN, RIGHT}};
739 short i, j;
740 for (i = 0; i < 4; i++) {
741 for (j = 0; j < 3 && !drawManacle(x, y, fallback[i][j]); j++);
742 }
743 }
744
745 // If hordeID is 0, it's randomly assigned based on the depth, with a 10% chance of an out-of-depth spawn from 1-5 levels deeper.
746 // If x is negative, location is random.
747 // Returns a pointer to the leader.
spawnHorde(short hordeID,short x,short y,unsigned long forbiddenFlags,unsigned long requiredFlags)748 creature *spawnHorde(short hordeID, short x, short y, unsigned long forbiddenFlags, unsigned long requiredFlags) {
749 short loc[2];
750 short i, failsafe, depth;
751 hordeType *theHorde;
752 creature *leader, *preexistingMonst;
753 boolean tryAgain;
754
755 if (rogue.depthLevel > 1 && rand_percent(10)) {
756 depth = rogue.depthLevel + rand_range(1, min(5, rogue.depthLevel / 2));
757 if (depth > AMULET_LEVEL) {
758 depth = max(rogue.depthLevel, AMULET_LEVEL);
759 }
760 forbiddenFlags |= HORDE_NEVER_OOD;
761 } else {
762 depth = rogue.depthLevel;
763 }
764
765 if (hordeID <= 0) {
766 failsafe = 50;
767 do {
768 tryAgain = false;
769 hordeID = pickHordeType(depth, 0, forbiddenFlags, requiredFlags);
770 if (hordeID < 0) {
771 return NULL;
772 }
773 if (x >= 0 && y >= 0) {
774 if (cellHasTerrainFlag(x, y, T_PATHING_BLOCKER)
775 && (!hordeCatalog[hordeID].spawnsIn || !cellHasTerrainType(x, y, hordeCatalog[hordeID].spawnsIn))) {
776
777 // don't spawn a horde in special terrain unless it's meant to spawn there
778 tryAgain = true;
779 }
780 if (hordeCatalog[hordeID].spawnsIn && !cellHasTerrainType(x, y, hordeCatalog[hordeID].spawnsIn)) {
781 // don't spawn a horde on normal terrain if it's meant for special terrain
782 tryAgain = true;
783 }
784 }
785 } while (--failsafe && tryAgain);
786 }
787
788 failsafe = 50;
789
790 if (x < 0 || y < 0) {
791 i = 0;
792 do {
793 while (!randomMatchingLocation(&(loc[0]), &(loc[1]), FLOOR, NOTHING, (hordeCatalog[hordeID].spawnsIn ? hordeCatalog[hordeID].spawnsIn : -1))
794 || passableArcCount(loc[0], loc[1]) > 1) {
795 if (!--failsafe) {
796 return NULL;
797 }
798 hordeID = pickHordeType(depth, 0, forbiddenFlags, 0);
799
800 if (hordeID < 0) {
801 return NULL;
802 }
803 }
804 x = loc[0];
805 y = loc[1];
806 i++;
807
808 // This "while" condition should contain IN_FIELD_OF_VIEW, since that is specifically
809 // calculated from the entry stairs when the level is generated, and will prevent monsters
810 // from spawning within FOV of the entry stairs.
811 } while (i < 25 && (pmap[x][y].flags & (ANY_KIND_OF_VISIBLE | IN_FIELD_OF_VIEW)));
812 }
813
814 // if (hordeCatalog[hordeID].spawnsIn == DEEP_WATER && pmap[x][y].layers[LIQUID] != DEEP_WATER) {
815 // message("Waterborne monsters spawned on land!", REQUIRE_ACKNOWLEDGMENT);
816 // }
817
818 theHorde = &hordeCatalog[hordeID];
819
820 if (theHorde->machine > 0) {
821 // Build the accompanying machine (e.g. a goblin encampment)
822 buildAMachine(theHorde->machine, x, y, 0, NULL, NULL, NULL);
823 }
824
825 leader = generateMonster(theHorde->leaderType, true, true);
826 leader->xLoc = x;
827 leader->yLoc = y;
828
829 if (hordeCatalog[hordeID].flags & HORDE_LEADER_CAPTIVE) {
830 leader->bookkeepingFlags |= MB_CAPTIVE;
831 leader->creatureState = MONSTER_WANDERING;
832 if (leader->info.turnsBetweenRegen > 0) {
833 leader->currentHP = leader->info.maxHP / 4 + 1;
834 }
835
836 // Draw the manacles unless the horde spawns in weird terrain (e.g. cages).
837 if (!hordeCatalog[hordeID].spawnsIn) {
838 drawManacles(x, y);
839 }
840 } else if (hordeCatalog[hordeID].flags & HORDE_ALLIED_WITH_PLAYER) {
841 becomeAllyWith(leader);
842 }
843
844 if (hordeCatalog[hordeID].flags & HORDE_SACRIFICE_TARGET) {
845 leader->bookkeepingFlags |= MB_MARKED_FOR_SACRIFICE;
846 leader->info.intrinsicLightType = SACRIFICE_MARK_LIGHT;
847 }
848
849 if ((theHorde->flags & HORDE_MACHINE_THIEF)) {
850 leader->safetyMap = allocGrid(); // Keep thieves from fleeing before they see the player
851 fillGrid(leader->safetyMap, 0);
852 }
853
854 preexistingMonst = monsterAtLoc(x, y);
855 if (preexistingMonst) {
856 killCreature(preexistingMonst, true); // If there's already a monster here, quietly bury the body.
857 }
858
859 brogueAssert(!(pmap[x][y].flags & HAS_MONSTER));
860
861 pmap[x][y].flags |= HAS_MONSTER;
862 if (playerCanSeeOrSense(x, y)) {
863 refreshDungeonCell(x, y);
864 }
865 if (monsterCanSubmergeNow(leader)) {
866 leader->bookkeepingFlags |= MB_SUBMERGED;
867 }
868
869 spawnMinions(hordeID, leader, false, true);
870
871 return leader;
872 }
873
fadeInMonster(creature * monst)874 void fadeInMonster(creature *monst) {
875 color fColor, bColor;
876 enum displayGlyph displayChar;
877 getCellAppearance(monst->xLoc, monst->yLoc, &displayChar, &fColor, &bColor);
878 flashMonster(monst, &bColor, 100);
879 }
880
createCreatureList()881 creatureList createCreatureList() {
882 creatureList list;
883 list.head = NULL;
884 return list;
885 }
iterateCreatures(creatureList * list)886 creatureIterator iterateCreatures(creatureList *list) {
887 creatureIterator iter;
888 iter.list = list;
889 iter.next = list->head;
890 return iter;
891 }
hasNextCreature(creatureIterator iter)892 boolean hasNextCreature(creatureIterator iter) {
893 return iter.next != NULL;
894 }
nextCreature(creatureIterator * iter)895 creature *nextCreature(creatureIterator *iter) {
896 if (iter->next == NULL) {
897 return NULL;
898 }
899 creature *result = iter->next->creature;
900 iter->next = iter->next->nextCreature;
901 return result;
902 }
restartIterator(creatureIterator * iter)903 void restartIterator(creatureIterator *iter) {
904 iter->next = iter->list->head;
905 }
prependCreature(creatureList * list,creature * add)906 void prependCreature(creatureList *list, creature *add) {
907 creatureListNode *node = calloc(1, sizeof(creatureListNode));
908 node->creature = add;
909 node->nextCreature = list->head;
910 list->head = node;
911 }
removeCreature(creatureList * list,creature * remove)912 boolean removeCreature(creatureList *list, creature *remove) {
913 creatureListNode **node = &list->head;
914 while (*node != NULL) {
915 if ((*node)->creature == remove) {
916 creatureListNode *removeNode = *node;
917 *node = removeNode->nextCreature;
918 free(removeNode);
919 return true;
920 }
921 node = &(*node)->nextCreature;
922 }
923 return false;
924 }
firstCreature(creatureList * list)925 creature *firstCreature(creatureList *list) {
926 if (list->head == NULL) {
927 return NULL;
928 }
929 return list->head->creature;
930 }
freeCreatureList(creatureList * list)931 void freeCreatureList(creatureList *list) {
932 creatureListNode *nextMonst;
933 for (creatureListNode *monstNode = list->head; monstNode != NULL; monstNode = nextMonst) {
934 nextMonst = monstNode->nextCreature;
935 freeCreature(monstNode->creature);
936 free(monstNode);
937 }
938 list->head = NULL;
939 }
940
summonMinions(creature * summoner)941 boolean summonMinions(creature *summoner) {
942 enum monsterTypes summonerType = summoner->info.monsterID;
943 const short hordeID = pickHordeType(0, summonerType, 0, 0);
944 short seenMinionCount = 0, x, y;
945 boolean atLeastOneMinion = false;
946 char buf[DCOLS];
947 char monstName[DCOLS];
948 short **grid;
949
950 if (hordeID < 0) {
951 return false;
952 }
953
954 if (summoner->info.abilityFlags & MA_ENTER_SUMMONS) {
955 pmap[summoner->xLoc][summoner->yLoc].flags &= ~HAS_MONSTER;
956 removeCreature(monsters, summoner);
957 }
958
959 atLeastOneMinion = spawnMinions(hordeID, summoner, true, false);
960
961 if (hordeCatalog[hordeID].flags & HORDE_SUMMONED_AT_DISTANCE) {
962 // Create a grid where "1" denotes a valid summoning location: within DCOLS/2 pathing distance,
963 // not in harmful terrain, and outside of the player's field of view.
964 grid = allocGrid();
965 fillGrid(grid, 0);
966 calculateDistances(grid, summoner->xLoc, summoner->yLoc, (T_PATHING_BLOCKER | T_SACRED), NULL, true, true);
967 findReplaceGrid(grid, 1, DCOLS/2, 1);
968 findReplaceGrid(grid, 2, 30000, 0);
969 getTerrainGrid(grid, 0, (T_PATHING_BLOCKER | T_HARMFUL_TERRAIN), (IN_FIELD_OF_VIEW | CLAIRVOYANT_VISIBLE | HAS_PLAYER | HAS_MONSTER));
970 } else {
971 grid = NULL;
972 }
973
974 creature *host = NULL;
975 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
976 creature *monst = nextCreature(&it);
977 if (monst != summoner && monstersAreTeammates(monst, summoner)
978 && (monst->bookkeepingFlags & MB_JUST_SUMMONED)) {
979
980 if (hordeCatalog[hordeID].flags & HORDE_SUMMONED_AT_DISTANCE) {
981 x = y = -1;
982 randomLocationInGrid(grid, &x, &y, 1);
983 teleport(monst, x, y, true);
984 if (x != -1 && y != -1 && grid != NULL) {
985 grid[x][y] = 0;
986 }
987 }
988
989 monst->bookkeepingFlags &= ~MB_JUST_SUMMONED;
990 if (canSeeMonster(monst)) {
991 seenMinionCount++;
992 refreshDungeonCell(monst->xLoc, monst->yLoc);
993 }
994 monst->ticksUntilTurn = 101;
995 monst->leader = summoner;
996
997 fadeInMonster(monst);
998 host = monst;
999 }
1000 }
1001
1002 if (canSeeMonster(summoner)) {
1003 monsterName(monstName, summoner, true);
1004 if (monsterText[summoner->info.monsterID].summonMessage[0]) {
1005 sprintf(buf, "%s %s", monstName, monsterText[summoner->info.monsterID].summonMessage);
1006 } else {
1007 sprintf(buf, "%s incants darkly!", monstName);
1008 }
1009 message(buf, 0);
1010 }
1011
1012 if (summoner->info.abilityFlags & MA_ENTER_SUMMONS) {
1013 removeCreature(monsters, summoner);
1014 if (atLeastOneMinion && host) {
1015 host->carriedMonster = summoner;
1016 demoteMonsterFromLeadership(summoner);
1017 refreshDungeonCell(summoner->xLoc, summoner->yLoc);
1018 } else {
1019 pmap[summoner->xLoc][summoner->yLoc].flags |= HAS_MONSTER;
1020 // TODO: why move to the beginning?
1021 prependCreature(monsters, summoner);
1022 }
1023 } else if (atLeastOneMinion) {
1024 summoner->bookkeepingFlags |= MB_LEADER;
1025 }
1026 createFlare(summoner->xLoc, summoner->yLoc, SUMMONING_FLASH_LIGHT);
1027
1028 if (grid) {
1029 freeGrid(grid);
1030 }
1031
1032 return atLeastOneMinion;
1033 }
1034
1035 // Generates and places monsters for the level.
populateMonsters()1036 void populateMonsters() {
1037 if (!MONSTERS_ENABLED) {
1038 return;
1039 }
1040
1041 short i, numberOfMonsters = min(20, 6 + 3 * max(0, rogue.depthLevel - AMULET_LEVEL)); // almost always 6.
1042
1043 while (rand_percent(60)) {
1044 numberOfMonsters++;
1045 }
1046 for (i=0; i<numberOfMonsters; i++) {
1047 spawnHorde(0, -1, -1, (HORDE_IS_SUMMONED | HORDE_MACHINE_ONLY), 0); // random horde type, random location
1048 }
1049 }
1050
getRandomMonsterSpawnLocation(short * x,short * y)1051 boolean getRandomMonsterSpawnLocation(short *x, short *y) {
1052 short **grid;
1053
1054 grid = allocGrid();
1055 fillGrid(grid, 0);
1056 calculateDistances(grid, player.xLoc, player.yLoc, T_DIVIDES_LEVEL, NULL, true, true);
1057 getTerrainGrid(grid, 0, (T_PATHING_BLOCKER | T_HARMFUL_TERRAIN), (HAS_PLAYER | HAS_MONSTER | HAS_STAIRS | IN_FIELD_OF_VIEW));
1058 findReplaceGrid(grid, -30000, DCOLS/2-1, 0);
1059 findReplaceGrid(grid, 30000, 30000, 0);
1060 findReplaceGrid(grid, DCOLS/2, 30000-1, 1);
1061 randomLocationInGrid(grid, x, y, 1);
1062 if (*x < 0 || *y < 0) {
1063 fillGrid(grid, 1);
1064 getTerrainGrid(grid, 0, (T_PATHING_BLOCKER | T_HARMFUL_TERRAIN), (HAS_PLAYER | HAS_MONSTER | HAS_STAIRS | IN_FIELD_OF_VIEW | IS_IN_MACHINE));
1065 randomLocationInGrid(grid, x, y, 1);
1066 }
1067 // DEBUG {
1068 // dumpLevelToScreen();
1069 // hiliteGrid(grid, &orange, 50);
1070 // plotCharWithColor('X', mapToWindowX(x), mapToWindowY(y), &black, &white);
1071 // temporaryMessage("Horde spawn location possibilities:", REQUIRE_ACKNOWLEDGMENT);
1072 // }
1073 freeGrid(grid);
1074 if (*x < 0 || *y < 0) {
1075 return false;
1076 }
1077 return true;
1078 }
1079
spawnPeriodicHorde()1080 void spawnPeriodicHorde() {
1081 creature *monst;
1082 short x, y;
1083
1084 if (!MONSTERS_ENABLED) {
1085 return;
1086 }
1087
1088 if (getRandomMonsterSpawnLocation(&x, &y)) {
1089 monst = spawnHorde(0, x, y, (HORDE_IS_SUMMONED | HORDE_LEADER_CAPTIVE | HORDE_NO_PERIODIC_SPAWN | HORDE_MACHINE_ONLY), 0);
1090 if (monst) {
1091 monst->creatureState = MONSTER_WANDERING;
1092 for (creatureIterator it2 = iterateCreatures(monsters); hasNextCreature(it2);) {
1093 creature *monst2 = nextCreature(&it2);
1094 if (monst2->leader == monst) {
1095 monst2->creatureState = MONSTER_WANDERING;
1096 }
1097 }
1098 }
1099 }
1100 }
1101
1102 // Instantally disentangles the player/creature. Useful for magical displacement like teleport and blink.
disentangle(creature * monst)1103 void disentangle(creature *monst) {
1104 if (monst == &player && monst->status[STATUS_STUCK]) {
1105 message("you break free!", false);
1106 }
1107 monst->status[STATUS_STUCK] = 0;
1108 }
1109
1110 // x and y are optional.
teleport(creature * monst,short x,short y,boolean respectTerrainAvoidancePreferences)1111 void teleport(creature *monst, short x, short y, boolean respectTerrainAvoidancePreferences) {
1112 short **grid, i, j;
1113 char monstFOV[DCOLS][DROWS];
1114
1115 if (!coordinatesAreInMap(x, y)) {
1116 zeroOutGrid(monstFOV);
1117 getFOVMask(monstFOV, monst->xLoc, monst->yLoc, DCOLS * FP_FACTOR, T_OBSTRUCTS_VISION, 0, false);
1118 grid = allocGrid();
1119 fillGrid(grid, 0);
1120 calculateDistances(grid, monst->xLoc, monst->yLoc, forbiddenFlagsForMonster(&(monst->info)) & T_DIVIDES_LEVEL, NULL, true, false);
1121 findReplaceGrid(grid, -30000, DCOLS/2, 0);
1122 findReplaceGrid(grid, 2, 30000, 1);
1123 if (validLocationCount(grid, 1) < 1) {
1124 fillGrid(grid, 1);
1125 }
1126 if (respectTerrainAvoidancePreferences) {
1127 if (monst->info.flags & MONST_RESTRICTED_TO_LIQUID) {
1128 fillGrid(grid, 0);
1129 getTMGrid(grid, 1, TM_ALLOWS_SUBMERGING);
1130 }
1131 getTerrainGrid(grid, 0, avoidedFlagsForMonster(&(monst->info)), (IS_IN_MACHINE | HAS_PLAYER | HAS_MONSTER | HAS_STAIRS));
1132 } else {
1133 getTerrainGrid(grid, 0, forbiddenFlagsForMonster(&(monst->info)), (IS_IN_MACHINE | HAS_PLAYER | HAS_MONSTER | HAS_STAIRS));
1134 }
1135 for (i=0; i<DCOLS; i++) {
1136 for (j=0; j<DROWS; j++) {
1137 if (monstFOV[i][j]) {
1138 grid[i][j] = 0;
1139 }
1140 }
1141 }
1142 randomLocationInGrid(grid, &x, &y, 1);
1143 // DEBUG {
1144 // dumpLevelToScreen();
1145 // hiliteGrid(grid, &orange, 50);
1146 // plotCharWithColor('X', mapToWindowX(x), mapToWindowY(y), &white, &red);
1147 // temporaryMessage("Teleport candidate locations:", REQUIRE_ACKNOWLEDGMENT);
1148 // }
1149 freeGrid(grid);
1150 if (x < 0 || y < 0) {
1151 return; // Failure!
1152 }
1153 }
1154 // Always break free on teleport
1155 disentangle(monst);
1156 setMonsterLocation(monst, x, y);
1157 if (monst != &player) {
1158 chooseNewWanderDestination(monst);
1159 }
1160 }
1161
isValidWanderDestination(creature * monst,short wpIndex)1162 boolean isValidWanderDestination(creature *monst, short wpIndex) {
1163 return (wpIndex >= 0
1164 && wpIndex < rogue.wpCount
1165 && !monst->waypointAlreadyVisited[wpIndex]
1166 && rogue.wpDistance[wpIndex][monst->xLoc][monst->yLoc] >= 0
1167 && nextStep(rogue.wpDistance[wpIndex], monst->xLoc, monst->yLoc, monst, false) != NO_DIRECTION);
1168 }
1169
closestWaypointIndex(creature * monst)1170 short closestWaypointIndex(creature *monst) {
1171 short i, closestDistance, closestIndex;
1172
1173 closestDistance = DCOLS/2;
1174 closestIndex = -1;
1175 for (i=0; i < rogue.wpCount; i++) {
1176 if (isValidWanderDestination(monst, i)
1177 && rogue.wpDistance[i][monst->xLoc][monst->yLoc] < closestDistance) {
1178
1179 closestDistance = rogue.wpDistance[i][monst->xLoc][monst->yLoc];
1180 closestIndex = i;
1181 }
1182 }
1183 return closestIndex;
1184 }
1185
chooseNewWanderDestination(creature * monst)1186 void chooseNewWanderDestination(creature *monst) {
1187 short i;
1188
1189 brogueAssert(monst->targetWaypointIndex < MAX_WAYPOINT_COUNT);
1190 brogueAssert(rogue.wpCount > 0 && rogue.wpCount <= MAX_WAYPOINT_COUNT);
1191
1192 // Set two checkpoints at random to false (which equilibrates to 50% of checkpoints being active).
1193 monst->waypointAlreadyVisited[rand_range(0, rogue.wpCount - 1)] = false;
1194 monst->waypointAlreadyVisited[rand_range(0, rogue.wpCount - 1)] = false;
1195 // Set the targeted checkpoint to true.
1196 if (monst->targetWaypointIndex >= 0) {
1197 monst->waypointAlreadyVisited[monst->targetWaypointIndex] = true;
1198 }
1199
1200 monst->targetWaypointIndex = closestWaypointIndex(monst); // Will be -1 if no waypoints were available.
1201 if (monst->targetWaypointIndex == -1) {
1202 for (i=0; i < rogue.wpCount; i++) {
1203 monst->waypointAlreadyVisited[i] = 0;
1204 }
1205 monst->targetWaypointIndex = closestWaypointIndex(monst);
1206 }
1207 }
1208
1209 enum subseqDFTypes {
1210 SUBSEQ_PROMOTE = 0,
1211 SUBSEQ_BURN,
1212 SUBSEQ_DISCOVER,
1213 };
1214
1215 // Returns the terrain flags of this tile after it's promoted according to the event corresponding to subseqDFTypes.
successorTerrainFlags(enum tileType tile,enum subseqDFTypes promotionType)1216 unsigned long successorTerrainFlags(enum tileType tile, enum subseqDFTypes promotionType) {
1217 enum dungeonFeatureTypes DF = 0;
1218
1219 switch (promotionType) {
1220 case SUBSEQ_PROMOTE:
1221 DF = tileCatalog[tile].promoteType;
1222 break;
1223 case SUBSEQ_BURN:
1224 DF = tileCatalog[tile].fireType;
1225 break;
1226 case SUBSEQ_DISCOVER:
1227 DF = tileCatalog[tile].discoverType;
1228 break;
1229 default:
1230 break;
1231 }
1232
1233 if (DF) {
1234 return tileCatalog[dungeonFeatureCatalog[DF].tile].flags;
1235 } else {
1236 return 0;
1237 }
1238 }
1239
burnedTerrainFlagsAtLoc(short x,short y)1240 unsigned long burnedTerrainFlagsAtLoc(short x, short y) {
1241 short layer;
1242 unsigned long flags = 0;
1243
1244 for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
1245 if (tileCatalog[pmap[x][y].layers[layer]].flags & T_IS_FLAMMABLE) {
1246 flags |= successorTerrainFlags(pmap[x][y].layers[layer], SUBSEQ_BURN);
1247 if (tileCatalog[pmap[x][y].layers[layer]].mechFlags & TM_EXPLOSIVE_PROMOTE) {
1248 flags |= successorTerrainFlags(pmap[x][y].layers[layer], SUBSEQ_PROMOTE);
1249 }
1250 }
1251 }
1252
1253 return flags;
1254 }
1255
discoveredTerrainFlagsAtLoc(short x,short y)1256 unsigned long discoveredTerrainFlagsAtLoc(short x, short y) {
1257 short layer;
1258 unsigned long flags = 0;
1259
1260 for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
1261 if (tileCatalog[pmap[x][y].layers[layer]].mechFlags & TM_IS_SECRET) {
1262 flags |= successorTerrainFlags(pmap[x][y].layers[layer], SUBSEQ_DISCOVER);
1263 }
1264 }
1265
1266 return flags;
1267 }
1268
monsterAvoids(creature * monst,short x,short y)1269 boolean monsterAvoids(creature *monst, short x, short y) {
1270 unsigned long terrainImmunities;
1271 creature *defender;
1272 unsigned long tFlags, cFlags;
1273
1274 getLocationFlags(x, y, &tFlags, NULL, &cFlags, monst == &player);
1275
1276 // everyone but the player avoids the stairs
1277 if ((x == rogue.downLoc[0] && y == rogue.downLoc[1])
1278 || (x == rogue.upLoc[0] && y == rogue.upLoc[1])) {
1279
1280 return monst != &player;
1281 }
1282
1283 // dry land
1284 if (monst->info.flags & MONST_RESTRICTED_TO_LIQUID
1285 && !cellHasTMFlag(x, y, TM_ALLOWS_SUBMERGING)) {
1286 return true;
1287 }
1288
1289 // non-allied monsters can always attack the player
1290 if (player.xLoc == x && player.yLoc == y && monst != &player && monst->creatureState != MONSTER_ALLY) {
1291 return false;
1292 }
1293
1294 // walls
1295 if (tFlags & T_OBSTRUCTS_PASSABILITY) {
1296 if (monst != &player
1297 && cellHasTMFlag(x, y, TM_IS_SECRET)
1298 && !(discoveredTerrainFlagsAtLoc(x, y) & avoidedFlagsForMonster(&(monst->info)))) {
1299 // This is so monsters can use secret doors but won't embed themselves in secret levers.
1300 return false;
1301 }
1302 if (distanceBetween(monst->xLoc, monst->yLoc, x, y) <= 1) {
1303 defender = monsterAtLoc(x, y);
1304 if (defender
1305 && (defender->info.flags & MONST_ATTACKABLE_THRU_WALLS)) {
1306 return false;
1307 }
1308 }
1309 return true;
1310 }
1311
1312 // Monsters can always attack unfriendly neighboring monsters,
1313 // unless it is immune to us for whatever reason.
1314 if (distanceBetween(monst->xLoc, monst->yLoc, x, y) <= 1) {
1315 defender = monsterAtLoc(x, y);
1316 if (defender
1317 && !(defender->bookkeepingFlags & MB_IS_DYING)
1318 && monsterWillAttackTarget(monst, defender)) {
1319
1320 if (attackWouldBeFutile(monst, defender)) {
1321 return true;
1322 } else {
1323 return false;
1324 }
1325 }
1326 }
1327
1328 // Monsters always avoid enemy monsters that we can't damage.
1329 defender = monsterAtLoc(x, y);
1330 if (defender
1331 && !(defender->bookkeepingFlags & MB_IS_DYING)
1332 && monstersAreEnemies(monst, defender)
1333 && attackWouldBeFutile(monst, defender)) {
1334
1335 return true;
1336 }
1337
1338 // hidden terrain
1339 if (cellHasTMFlag(x, y, TM_IS_SECRET) && monst == &player) {
1340 return false; // player won't avoid what he doesn't know about
1341 }
1342
1343 // Determine invulnerabilities based only on monster characteristics.
1344 terrainImmunities = 0;
1345 if (monst->status[STATUS_IMMUNE_TO_FIRE]) {
1346 terrainImmunities |= (T_IS_FIRE | T_SPONTANEOUSLY_IGNITES | T_LAVA_INSTA_DEATH);
1347 }
1348 if (monst->info.flags & MONST_INVULNERABLE) {
1349 terrainImmunities |= T_HARMFUL_TERRAIN | T_ENTANGLES | T_SPONTANEOUSLY_IGNITES | T_LAVA_INSTA_DEATH;
1350 }
1351 if (monst->info.flags & MONST_INANIMATE) {
1352 terrainImmunities |= (T_CAUSES_DAMAGE | T_CAUSES_PARALYSIS | T_CAUSES_CONFUSION | T_CAUSES_NAUSEA | T_CAUSES_POISON);
1353 }
1354 if (monst->status[STATUS_LEVITATING]) {
1355 terrainImmunities |= (T_AUTO_DESCENT | T_CAUSES_POISON | T_IS_DEEP_WATER | T_IS_DF_TRAP | T_LAVA_INSTA_DEATH);
1356 }
1357 if (monst->info.flags & MONST_IMMUNE_TO_WEBS) {
1358 terrainImmunities |= T_ENTANGLES;
1359 }
1360 if (monst->info.flags & MONST_IMMUNE_TO_WATER) {
1361 terrainImmunities |= T_IS_DEEP_WATER;
1362 }
1363 if (monst == &player) {
1364 terrainImmunities |= T_SACRED;
1365 }
1366 if (monst == &player
1367 && rogue.armor
1368 && (rogue.armor->flags & ITEM_RUNIC)
1369 && rogue.armor->enchant2 == A_RESPIRATION) {
1370
1371 terrainImmunities |= T_RESPIRATION_IMMUNITIES;
1372 }
1373
1374 // sacred ground
1375 if ((tFlags & T_SACRED & ~terrainImmunities)) {
1376 return true;
1377 }
1378
1379 // brimstone
1380 if (!(monst->status[STATUS_IMMUNE_TO_FIRE])
1381 && !(monst->info.flags & MONST_INVULNERABLE)
1382 && (tFlags & T_SPONTANEOUSLY_IGNITES)
1383 && !(cFlags & (HAS_MONSTER | HAS_PLAYER))
1384 && !cellHasTerrainFlag(monst->xLoc, monst->yLoc, T_IS_FIRE | T_SPONTANEOUSLY_IGNITES)
1385 && (monst == &player || (monst->creatureState != MONSTER_TRACKING_SCENT && monst->creatureState != MONSTER_FLEEING))) {
1386 return true;
1387 }
1388
1389 // burning wandering monsters avoid flammable terrain out of common courtesy
1390 if (monst != &player
1391 && monst->creatureState == MONSTER_WANDERING
1392 && (monst->info.flags & MONST_FIERY)
1393 && (tFlags & T_IS_FLAMMABLE)) {
1394
1395 return true;
1396 }
1397
1398 // burning monsters avoid explosive terrain and steam-emitting terrain
1399 if (monst != &player
1400 && monst->status[STATUS_BURNING]
1401 && (burnedTerrainFlagsAtLoc(x, y) & (T_CAUSES_EXPLOSIVE_DAMAGE | T_CAUSES_DAMAGE | T_AUTO_DESCENT) & ~terrainImmunities)) {
1402
1403 return true;
1404 }
1405
1406 // fire
1407 if ((tFlags & T_IS_FIRE & ~terrainImmunities)
1408 && !cellHasTerrainFlag(monst->xLoc, monst->yLoc, T_IS_FIRE)
1409 && !(cFlags & (HAS_MONSTER | HAS_PLAYER))
1410 && (monst != &player || rogue.mapToShore[x][y] >= player.status[STATUS_IMMUNE_TO_FIRE])) {
1411 return true;
1412 }
1413
1414 // non-fire harmful terrain
1415 if ((tFlags & T_HARMFUL_TERRAIN & ~T_IS_FIRE & ~terrainImmunities)
1416 && !cellHasTerrainFlag(monst->xLoc, monst->yLoc, (T_HARMFUL_TERRAIN & ~T_IS_FIRE))) {
1417 return true;
1418 }
1419
1420 // chasms or trap doors
1421 if ((tFlags & T_AUTO_DESCENT & ~terrainImmunities)
1422 && (!(tFlags & T_ENTANGLES) || !(monst->info.flags & MONST_IMMUNE_TO_WEBS))) {
1423 return true;
1424 }
1425
1426 // gas or other environmental traps
1427 if ((tFlags & T_IS_DF_TRAP & ~terrainImmunities)
1428 && !(cFlags & PRESSURE_PLATE_DEPRESSED)
1429 && (monst == &player || monst->creatureState == MONSTER_WANDERING
1430 || (monst->creatureState == MONSTER_ALLY && !(cellHasTMFlag(x, y, TM_IS_SECRET))))
1431 && !(monst->status[STATUS_ENTRANCED])
1432 && (!(tFlags & T_ENTANGLES) || !(monst->info.flags & MONST_IMMUNE_TO_WEBS))) {
1433 return true;
1434 }
1435
1436 // lava
1437 if ((tFlags & T_LAVA_INSTA_DEATH & ~terrainImmunities)
1438 && (!(tFlags & T_ENTANGLES) || !(monst->info.flags & MONST_IMMUNE_TO_WEBS))
1439 && (monst != &player || rogue.mapToShore[x][y] >= max(player.status[STATUS_IMMUNE_TO_FIRE], player.status[STATUS_LEVITATING]))) {
1440 return true;
1441 }
1442
1443 // deep water
1444 if ((tFlags & T_IS_DEEP_WATER & ~terrainImmunities)
1445 && (!(tFlags & T_ENTANGLES) || !(monst->info.flags & MONST_IMMUNE_TO_WEBS))
1446 && !cellHasTerrainFlag(monst->xLoc, monst->yLoc, T_IS_DEEP_WATER)) {
1447 return true; // avoid only if not already in it
1448 }
1449
1450 // poisonous lichen
1451 if ((tFlags & T_CAUSES_POISON & ~terrainImmunities)
1452 && !cellHasTerrainFlag(monst->xLoc, monst->yLoc, T_CAUSES_POISON)
1453 && (monst == &player || monst->creatureState != MONSTER_TRACKING_SCENT || monst->currentHP < 10)) {
1454 return true;
1455 }
1456
1457 // Smart monsters don't attack in corridors if they belong to a group and they can help it.
1458 if ((monst->info.abilityFlags & MA_AVOID_CORRIDORS)
1459 && !(monst->status[STATUS_ENRAGED] && monst->currentHP <= monst->info.maxHP / 2)
1460 && monst->creatureState == MONSTER_TRACKING_SCENT
1461 && (monst->bookkeepingFlags & (MB_FOLLOWER | MB_LEADER))
1462 && passableArcCount(x, y) >= 2
1463 && passableArcCount(monst->xLoc, monst->yLoc) < 2
1464 && !cellHasTerrainFlag(monst->xLoc, monst->yLoc, (T_HARMFUL_TERRAIN & ~terrainImmunities))) {
1465 return true;
1466 }
1467
1468 return false;
1469 }
1470
moveMonsterPassivelyTowards(creature * monst,short targetLoc[2],boolean willingToAttackPlayer)1471 boolean moveMonsterPassivelyTowards(creature *monst, short targetLoc[2], boolean willingToAttackPlayer) {
1472 short x, y, dx, dy, newX, newY;
1473
1474 x = monst->xLoc;
1475 y = monst->yLoc;
1476
1477 if (targetLoc[0] == x) {
1478 dx = 0;
1479 } else {
1480 dx = (targetLoc[0] < x ? -1 : 1);
1481 }
1482 if (targetLoc[1] == y) {
1483 dy = 0;
1484 } else {
1485 dy = (targetLoc[1] < y ? -1 : 1);
1486 }
1487
1488 if (dx == 0 && dy == 0) { // already at the destination
1489 return false;
1490 }
1491
1492 newX = x + dx;
1493 newY = y + dy;
1494
1495 if (!coordinatesAreInMap(newX, newY)) {
1496 return false;
1497 }
1498
1499 if (monst->creatureState != MONSTER_TRACKING_SCENT && dx && dy) {
1500 if (abs(targetLoc[0] - x) > abs(targetLoc[1] - y) && rand_range(0, abs(targetLoc[0] - x)) > abs(targetLoc[1] - y)) {
1501 if (!(monsterAvoids(monst, newX, y) || (!willingToAttackPlayer && (pmap[newX][y].flags & HAS_PLAYER)) || !moveMonster(monst, dx, 0))) {
1502 return true;
1503 }
1504 } else if (abs(targetLoc[0] - x) < abs(targetLoc[1] - y) && rand_range(0, abs(targetLoc[1] - y)) > abs(targetLoc[0] - x)) {
1505 if (!(monsterAvoids(monst, x, newY) || (!willingToAttackPlayer && (pmap[x][newY].flags & HAS_PLAYER)) || !moveMonster(monst, 0, dy))) {
1506 return true;
1507 }
1508 }
1509 }
1510
1511 // Try to move toward the goal diagonally if possible or else straight.
1512 // If that fails, try both directions for the shorter coordinate.
1513 // If they all fail, return false.
1514 if (monsterAvoids(monst, newX, newY) || (!willingToAttackPlayer && (pmap[newX][newY].flags & HAS_PLAYER)) || !moveMonster(monst, dx, dy)) {
1515 if (distanceBetween(x, y, targetLoc[0], targetLoc[1]) <= 1 && (dx == 0 || dy == 0)) { // cardinally adjacent
1516 return false; // destination is blocked
1517 }
1518 //abs(targetLoc[0] - x) < abs(targetLoc[1] - y)
1519 if ((max(targetLoc[0], x) - min(targetLoc[0], x)) < (max(targetLoc[1], y) - min(targetLoc[1], y))) {
1520 if (monsterAvoids(monst, x, newY) || (!willingToAttackPlayer && pmap[x][newY].flags & HAS_PLAYER) || !moveMonster(monst, 0, dy)) {
1521 if (monsterAvoids(monst, newX, y) || (!willingToAttackPlayer && pmap[newX][y].flags & HAS_PLAYER) || !moveMonster(monst, dx, 0)) {
1522 if (monsterAvoids(monst, x-1, newY) || (!willingToAttackPlayer && pmap[x-1][newY].flags & HAS_PLAYER) || !moveMonster(monst, -1, dy)) {
1523 if (monsterAvoids(monst, x+1, newY) || (!willingToAttackPlayer && pmap[x+1][newY].flags & HAS_PLAYER) || !moveMonster(monst, 1, dy)) {
1524 return false;
1525 }
1526 }
1527 }
1528 }
1529 } else {
1530 if (monsterAvoids(monst, newX, y) || (!willingToAttackPlayer && pmap[newX][y].flags & HAS_PLAYER) || !moveMonster(monst, dx, 0)) {
1531 if (monsterAvoids(monst, x, newY) || (!willingToAttackPlayer && pmap[x][newY].flags & HAS_PLAYER) || !moveMonster(monst, 0, dy)) {
1532 if (monsterAvoids(monst, newX, y-1) || (!willingToAttackPlayer && pmap[newX][y-1].flags & HAS_PLAYER) || !moveMonster(monst, dx, -1)) {
1533 if (monsterAvoids(monst, newX, y+1) || (!willingToAttackPlayer && pmap[newX][y+1].flags & HAS_PLAYER) || !moveMonster(monst, dx, 1)) {
1534 return false;
1535 }
1536 }
1537 }
1538 }
1539 }
1540 }
1541 return true;
1542 }
1543
distanceBetween(short x1,short y1,short x2,short y2)1544 short distanceBetween(short x1, short y1, short x2, short y2) {
1545 return max(abs(x1 - x2), abs(y1 - y2));
1546 }
1547
alertMonster(creature * monst)1548 void alertMonster(creature *monst) {
1549 monst->creatureState = (monst->creatureMode == MODE_PERM_FLEEING ? MONSTER_FLEEING : MONSTER_TRACKING_SCENT);
1550 monst->lastSeenPlayerAt[0] = player.xLoc;
1551 monst->lastSeenPlayerAt[1] = player.yLoc;
1552 }
1553
wakeUp(creature * monst)1554 void wakeUp(creature *monst) {
1555 if (monst->creatureState != MONSTER_ALLY) {
1556 alertMonster(monst);
1557 }
1558 monst->ticksUntilTurn = 100;
1559 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
1560 creature *teammate = nextCreature(&it);
1561 if (monst != teammate && monstersAreTeammates(monst, teammate) && teammate->creatureMode == MODE_NORMAL) {
1562 if (teammate->creatureState == MONSTER_SLEEPING
1563 || teammate->creatureState == MONSTER_WANDERING) {
1564 teammate->ticksUntilTurn = max(100, teammate->ticksUntilTurn);
1565 }
1566 if (monst->creatureState != MONSTER_ALLY) {
1567 teammate->creatureState =
1568 (teammate->creatureMode == MODE_PERM_FLEEING ? MONSTER_FLEEING : MONSTER_TRACKING_SCENT);
1569 updateMonsterState(teammate);
1570 }
1571 }
1572 }
1573 }
1574
monsterCanShootWebs(creature * monst)1575 boolean monsterCanShootWebs(creature *monst) {
1576 short i;
1577 for (i=0; monst->info.bolts[i] != 0; i++) {
1578 const bolt *theBolt = &boltCatalog[monst->info.bolts[i]];
1579 if (theBolt->pathDF && (tileCatalog[dungeonFeatureCatalog[theBolt->pathDF].tile].flags & T_ENTANGLES)) {
1580 return true;
1581 }
1582 }
1583 return false;
1584 }
1585
1586 // Assumes that observer is not the player.
1587 // Returns approximately double the actual (quasi-euclidian) distance.
awarenessDistance(creature * observer,creature * target)1588 short awarenessDistance(creature *observer, creature *target) {
1589 long perceivedDistance;
1590
1591 // When determining distance from the player for purposes of monster state changes
1592 // (i.e. whether they start or stop hunting), take the scent value of the monster's tile
1593 // OR, if the monster is in the player's FOV (including across chasms, through green crystal, etc.),
1594 // the direct distance -- whichever is less.
1595 // This means that monsters can aggro within stealth range if they're on the other side
1596 // of a transparent obstruction, and may just stand motionless but hunting if there's no scent map
1597 // to guide them, but only as long as the player is within FOV. After that, we switch to wandering
1598 // and wander toward the last location that we saw the player.
1599 perceivedDistance = (rogue.scentTurnNumber - scentMap[observer->xLoc][observer->yLoc]); // this value is double the apparent distance
1600 if ((target == &player && (pmap[observer->xLoc][observer->yLoc].flags & IN_FIELD_OF_VIEW))
1601 || (target != &player && openPathBetween(observer->xLoc, observer->yLoc, target->xLoc, target->yLoc))) {
1602
1603 perceivedDistance = min(perceivedDistance, scentDistance(observer->xLoc, observer->yLoc, target->xLoc, target->yLoc));
1604 }
1605
1606 perceivedDistance = min(perceivedDistance, 1000);
1607
1608 if (perceivedDistance < 0) {
1609 perceivedDistance = 1000;
1610 }
1611 return ((short) perceivedDistance);
1612 }
1613
1614 // yes or no -- observer is aware of the target as of this new turn.
1615 // takes into account whether it is ALREADY aware of the target.
awareOfTarget(creature * observer,creature * target)1616 boolean awareOfTarget(creature *observer, creature *target) {
1617 short perceivedDistance = awarenessDistance(observer, target);
1618 short awareness = rogue.aggroRange * 2;
1619 boolean retval;
1620
1621 brogueAssert(perceivedDistance >= 0 && awareness >= 0);
1622
1623 if (observer->info.flags & MONST_ALWAYS_HUNTING) {
1624 retval = true;
1625 } else if (observer->info.flags & MONST_IMMOBILE) {
1626 // Turrets and totems are aware of you iff they are within stealth range.
1627 // The only exception is mirror totems; they're always ready to shoot because they have "always hunting" set.
1628 retval = perceivedDistance <= awareness;
1629 } else if (perceivedDistance > awareness * 3) {
1630 // out of awareness range, even if hunting
1631 retval = false;
1632 } else if (observer->creatureState == MONSTER_TRACKING_SCENT) {
1633 // already aware of the target, lose track 3% of the time if outside of stealth range.
1634 if (perceivedDistance > awareness) {
1635 retval = rand_percent(97);
1636 } else {
1637 retval = true;
1638 }
1639 } else if (target == &player
1640 && !(pmap[observer->xLoc][observer->yLoc].flags & IN_FIELD_OF_VIEW)) {
1641 // observer not hunting and player-target not in field of view
1642 retval = false;
1643 } else if (perceivedDistance <= awareness) {
1644 // within range but currently unaware
1645 retval = rand_percent(25);
1646 } else {
1647 retval = false;
1648 }
1649 return retval;
1650 }
1651
closestWaypointIndexTo(const short x,const short y)1652 short closestWaypointIndexTo(const short x, const short y) {
1653 short i, closestDistance, closestIndex;
1654
1655 closestDistance = 1000;
1656 closestIndex = -1;
1657 for (i=0; i < rogue.wpCount; i++) {
1658 if (rogue.wpDistance[i][x][y] < closestDistance) {
1659 closestDistance = rogue.wpDistance[i][x][y];
1660 closestIndex = i;
1661 }
1662 }
1663 return closestIndex;
1664 }
1665
wanderToward(creature * monst,const short x,const short y)1666 void wanderToward(creature *monst, const short x, const short y) {
1667 if (coordinatesAreInMap(x, y)) {
1668 const short theWaypointIndex = closestWaypointIndexTo(x, y);
1669 if (theWaypointIndex != -1) {
1670 monst->waypointAlreadyVisited[theWaypointIndex] = false;
1671 monst->targetWaypointIndex = theWaypointIndex;
1672 }
1673 }
1674 }
1675
updateMonsterState(creature * monst)1676 void updateMonsterState(creature *monst) {
1677 short x, y, closestFearedEnemy;
1678 boolean awareOfPlayer;
1679
1680 x = monst->xLoc;
1681 y = monst->yLoc;
1682
1683 if ((monst->info.flags & MONST_ALWAYS_HUNTING)
1684 && monst->creatureState != MONSTER_ALLY) {
1685
1686 monst->creatureState = MONSTER_TRACKING_SCENT;
1687 return;
1688 }
1689
1690 awareOfPlayer = awareOfTarget(monst, &player);
1691
1692 if ((monst->info.flags & MONST_IMMOBILE)
1693 && monst->creatureState != MONSTER_ALLY) {
1694
1695 if (awareOfPlayer) {
1696 monst->creatureState = MONSTER_TRACKING_SCENT;
1697 } else {
1698 monst->creatureState = MONSTER_SLEEPING;
1699 }
1700 return;
1701 }
1702
1703 if (monst->creatureMode == MODE_PERM_FLEEING
1704 && (monst->creatureState == MONSTER_WANDERING || monst->creatureState == MONSTER_TRACKING_SCENT)) {
1705
1706 monst->creatureState = MONSTER_FLEEING;
1707 }
1708
1709 closestFearedEnemy = DCOLS+DROWS;
1710
1711 boolean handledPlayer = false;
1712 for (creatureIterator it = iterateCreatures(monsters); !handledPlayer || hasNextCreature(it);) {
1713 creature *monst2 = !handledPlayer ? &player : nextCreature(&it);
1714 handledPlayer = true;
1715 if (monsterFleesFrom(monst, monst2)
1716 && distanceBetween(x, y, monst2->xLoc, monst2->yLoc) < closestFearedEnemy
1717 && traversiblePathBetween(monst2, x, y)
1718 && openPathBetween(x, y, monst2->xLoc, monst2->yLoc)) {
1719
1720 closestFearedEnemy = distanceBetween(x, y, monst2->xLoc, monst2->yLoc);
1721 }
1722 }
1723
1724 if ((monst->creatureState == MONSTER_WANDERING)
1725 && awareOfPlayer
1726 && (pmap[player.xLoc][player.yLoc].flags & IN_FIELD_OF_VIEW)) {
1727 // If wandering and you notice the player, start tracking the scent.
1728 alertMonster(monst);
1729 } else if (monst->creatureState == MONSTER_SLEEPING) {
1730 // if sleeping, the monster has a chance to awaken
1731 if (awareOfPlayer) {
1732 wakeUp(monst); // wakes up the whole horde if necessary
1733 }
1734 } else if (monst->creatureState == MONSTER_TRACKING_SCENT && !awareOfPlayer) {
1735 // if tracking scent, but the scent is weaker than the scent detection threshold, begin wandering.
1736 monst->creatureState = MONSTER_WANDERING;
1737 wanderToward(monst, monst->lastSeenPlayerAt[0], monst->lastSeenPlayerAt[1]);
1738 } else if (monst->creatureState == MONSTER_TRACKING_SCENT
1739 && closestFearedEnemy < 3) {
1740 monst->creatureState = MONSTER_FLEEING;
1741 } else if (monst->creatureState != MONSTER_ALLY
1742 && (monst->info.flags & MONST_FLEES_NEAR_DEATH)
1743 && monst->currentHP <= 3 * monst->info.maxHP / 4) {
1744
1745 if (monst->creatureState == MONSTER_FLEEING
1746 || monst->currentHP <= monst->info.maxHP / 4) {
1747
1748 monst->creatureState = MONSTER_FLEEING;
1749 }
1750 } else if (monst->creatureMode == MODE_NORMAL
1751 && monst->creatureState == MONSTER_FLEEING
1752 && !(monst->status[STATUS_MAGICAL_FEAR])
1753 && closestFearedEnemy >= 3) {
1754
1755 monst->creatureState = MONSTER_TRACKING_SCENT;
1756 } else if (monst->creatureMode == MODE_PERM_FLEEING
1757 && monst->creatureState == MONSTER_FLEEING
1758 && (monst->info.abilityFlags & MA_HIT_STEAL_FLEE)
1759 && !(monst->status[STATUS_MAGICAL_FEAR])
1760 && !(monst->carriedItem)) {
1761
1762 monst->creatureMode = MODE_NORMAL;
1763 alertMonster(monst);
1764 } else if (monst->creatureMode == MODE_NORMAL
1765 && monst->creatureState == MONSTER_FLEEING
1766 && (monst->info.flags & MONST_FLEES_NEAR_DEATH)
1767 && !(monst->status[STATUS_MAGICAL_FEAR])
1768 && monst->currentHP >= monst->info.maxHP * 3 / 4) {
1769
1770 if ((monst->bookkeepingFlags & MB_FOLLOWER) && monst->leader == &player) {
1771 monst->creatureState = MONSTER_ALLY;
1772 } else {
1773 alertMonster(monst);
1774 }
1775 }
1776
1777 if (awareOfPlayer) {
1778 if (monst->creatureState == MONSTER_FLEEING
1779 || monst->creatureState == MONSTER_TRACKING_SCENT) {
1780
1781 monst->lastSeenPlayerAt[0] = player.xLoc;
1782 monst->lastSeenPlayerAt[1] = player.yLoc;
1783 }
1784 }
1785 }
1786
decrementMonsterStatus(creature * monst)1787 void decrementMonsterStatus(creature *monst) {
1788 short i, damage;
1789 char buf[COLS], buf2[COLS];
1790
1791 monst->bookkeepingFlags &= ~MB_JUST_SUMMONED;
1792
1793 if (monst->currentHP < monst->info.maxHP
1794 && monst->info.turnsBetweenRegen > 0
1795 && !monst->status[STATUS_POISONED]) {
1796
1797 if ((monst->turnsUntilRegen -= 1000) <= 0) {
1798 monst->currentHP++;
1799 monst->previousHealthPoints++;
1800 monst->turnsUntilRegen += monst->info.turnsBetweenRegen;
1801 }
1802 }
1803
1804 for (i=0; i<NUMBER_OF_STATUS_EFFECTS; i++) {
1805 switch (i) {
1806 case STATUS_LEVITATING:
1807 if (monst->status[i] && !(monst->info.flags & MONST_FLIES)) {
1808 monst->status[i]--;
1809 }
1810 break;
1811 case STATUS_SLOWED:
1812 if (monst->status[i] && !--monst->status[i]) {
1813 monst->movementSpeed = monst->info.movementSpeed;
1814 monst->attackSpeed = monst->info.attackSpeed;
1815 }
1816 break;
1817 case STATUS_WEAKENED:
1818 if (monst->status[i] && !--monst->status[i]) {
1819 monst->weaknessAmount = 0;
1820 }
1821 break;
1822 case STATUS_HASTED:
1823 if (monst->status[i]) {
1824 if (!--monst->status[i]) {
1825 monst->movementSpeed = monst->info.movementSpeed;
1826 monst->attackSpeed = monst->info.attackSpeed;
1827 }
1828 }
1829 break;
1830 case STATUS_BURNING:
1831 if (monst->status[i]) {
1832 if (!(monst->info.flags & MONST_FIERY)) {
1833 monst->status[i]--;
1834 }
1835 damage = rand_range(1, 3);
1836 if (!(monst->status[STATUS_IMMUNE_TO_FIRE])
1837 && !(monst->info.flags & MONST_INVULNERABLE)
1838 && inflictDamage(NULL, monst, damage, &orange, true)) {
1839
1840 if (canSeeMonster(monst)) {
1841 monsterName(buf, monst, true);
1842 sprintf(buf2, "%s burns %s.",
1843 buf,
1844 (monst->info.flags & MONST_INANIMATE) ? "up" : "to death");
1845 messageWithColor(buf2, messageColorFromVictim(monst), 0);
1846 }
1847 return;
1848 }
1849 if (monst->status[i] <= 0) {
1850 extinguishFireOnCreature(monst);
1851 }
1852 }
1853 break;
1854 case STATUS_LIFESPAN_REMAINING:
1855 if (monst->status[i]) {
1856 monst->status[i]--;
1857 if (monst->status[i] <= 0) {
1858 killCreature(monst, false);
1859 if (canSeeMonster(monst)) {
1860 monsterName(buf, monst, true);
1861 sprintf(buf2, "%s dissipates into thin air.", buf);
1862 messageWithColor(buf2, &white, 0);
1863 }
1864 return;
1865 }
1866 }
1867 break;
1868 case STATUS_POISONED:
1869 if (monst->status[i]) {
1870 monst->status[i]--;
1871 if (inflictDamage(NULL, monst, monst->poisonAmount, &green, true)) {
1872 if (canSeeMonster(monst)) {
1873 monsterName(buf, monst, true);
1874 sprintf(buf2, "%s dies of poison.", buf);
1875 messageWithColor(buf2, messageColorFromVictim(monst), 0);
1876 }
1877 return;
1878 }
1879 if (!monst->status[i]) {
1880 monst->poisonAmount = 0;
1881 }
1882 }
1883 break;
1884 case STATUS_STUCK:
1885 if (monst->status[i] && !cellHasTerrainFlag(monst->xLoc, monst->yLoc, T_ENTANGLES)) {
1886 monst->status[i] = 0;
1887 }
1888 break;
1889 case STATUS_DISCORDANT:
1890 if (monst->status[i] && !--monst->status[i]) {
1891 if (monst->creatureState == MONSTER_FLEEING
1892 && !monst->status[STATUS_MAGICAL_FEAR]
1893 && monst->leader == &player) {
1894
1895 monst->creatureState = MONSTER_ALLY;
1896 if (monst->carriedItem) {
1897 makeMonsterDropItem(monst);
1898 }
1899 }
1900 }
1901 break;
1902 case STATUS_MAGICAL_FEAR:
1903 if (monst->status[i]) {
1904 if (!--monst->status[i]) {
1905 monst->creatureState = (monst->leader == &player ? MONSTER_ALLY : MONSTER_TRACKING_SCENT);
1906 }
1907 }
1908 break;
1909 case STATUS_SHIELDED:
1910 monst->status[i] -= monst->maxStatus[i] / 20;
1911 if (monst->status[i] <= 0) {
1912 monst->status[i] = monst->maxStatus[i] = 0;
1913 }
1914 break;
1915 case STATUS_IMMUNE_TO_FIRE:
1916 if (monst->status[i] && !(monst->info.flags & MONST_IMMUNE_TO_FIRE)) {
1917 monst->status[i]--;
1918 }
1919 break;
1920 case STATUS_INVISIBLE:
1921 if (monst->status[i]
1922 && !(monst->info.flags & MONST_INVISIBLE)
1923 && !--monst->status[i]
1924 && playerCanSee(monst->xLoc, monst->yLoc)) {
1925
1926 refreshDungeonCell(monst->xLoc, monst->yLoc);
1927 }
1928 break;
1929 default:
1930 if (monst->status[i]) {
1931 monst->status[i]--;
1932 }
1933 break;
1934 }
1935 }
1936
1937 if (monsterCanSubmergeNow(monst) && !(monst->bookkeepingFlags & MB_SUBMERGED)) {
1938 if (rand_percent(20)) {
1939 monst->bookkeepingFlags |= MB_SUBMERGED;
1940 if (!monst->status[STATUS_MAGICAL_FEAR]
1941 && monst->creatureState == MONSTER_FLEEING
1942 && (!(monst->info.flags & MONST_FLEES_NEAR_DEATH) || monst->currentHP >= monst->info.maxHP * 3 / 4)) {
1943
1944 monst->creatureState = MONSTER_TRACKING_SCENT;
1945 }
1946 refreshDungeonCell(monst->xLoc, monst->yLoc);
1947 } else if (monst->info.flags & (MONST_RESTRICTED_TO_LIQUID)
1948 && monst->creatureState != MONSTER_ALLY) {
1949 monst->creatureState = MONSTER_FLEEING;
1950 }
1951 }
1952 }
1953
traversiblePathBetween(creature * monst,short x2,short y2)1954 boolean traversiblePathBetween(creature *monst, short x2, short y2) {
1955 short coords[DCOLS][2], i, x, y, n;
1956 short originLoc[2] = {monst->xLoc, monst->yLoc};
1957 short targetLoc[2] = {x2, y2};
1958
1959 // Using BOLT_NONE here to favor a path that avoids obstacles to one that hits them
1960 n = getLineCoordinates(coords, originLoc, targetLoc, &boltCatalog[BOLT_NONE]);
1961
1962 for (i=0; i<n; i++) {
1963 x = coords[i][0];
1964 y = coords[i][1];
1965 if (x == x2 && y == y2) {
1966 return true;
1967 }
1968 if (monsterAvoids(monst, x, y)) {
1969 return false;
1970 }
1971 }
1972 brogueAssert(false);
1973 return true; // should never get here
1974 }
1975
specifiedPathBetween(short x1,short y1,short x2,short y2,unsigned long blockingTerrain,unsigned long blockingFlags)1976 boolean specifiedPathBetween(short x1, short y1, short x2, short y2,
1977 unsigned long blockingTerrain, unsigned long blockingFlags) {
1978 short coords[DCOLS][2], i, x, y, n;
1979 short originLoc[2] = {x1, y1};
1980 short targetLoc[2] = {x2, y2};
1981 n = getLineCoordinates(coords, originLoc, targetLoc, &boltCatalog[BOLT_NONE]);
1982
1983 for (i=0; i<n; i++) {
1984 x = coords[i][0];
1985 y = coords[i][1];
1986 if (cellHasTerrainFlag(x, y, blockingTerrain) || (pmap[x][y].flags & blockingFlags)) {
1987 return false;
1988 }
1989 if (x == x2 && y == y2) {
1990 return true;
1991 }
1992 }
1993 brogueAssert(false);
1994 return true; // should never get here
1995 }
1996
openPathBetween(short x1,short y1,short x2,short y2)1997 boolean openPathBetween(short x1, short y1, short x2, short y2) {
1998 short returnLoc[2], startLoc[2] = {x1, y1}, targetLoc[2] = {x2, y2};
1999
2000 getImpactLoc(returnLoc, startLoc, targetLoc, DCOLS, false, &boltCatalog[BOLT_NONE]);
2001 if (returnLoc[0] == targetLoc[0] && returnLoc[1] == targetLoc[1]) {
2002 return true;
2003 }
2004 return false;
2005 }
2006
2007 // will return the player if the player is at (x, y).
monsterAtLoc(short x,short y)2008 creature *monsterAtLoc(short x, short y) {
2009 if (!(pmap[x][y].flags & (HAS_MONSTER | HAS_PLAYER))) {
2010 return NULL;
2011 }
2012 if (player.xLoc == x && player.yLoc == y) {
2013 return &player;
2014 }
2015 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
2016 creature *monst = nextCreature(&it);
2017 if (monst->xLoc == x && monst->yLoc == y) {
2018 return monst;
2019 }
2020 }
2021 // This should be unreachable, since the HAS_MONSTER
2022 // flag was true at (x, y).
2023 brogueAssert(0);
2024 return NULL;
2025 }
2026
dormantMonsterAtLoc(short x,short y)2027 creature *dormantMonsterAtLoc(short x, short y) {
2028 if (!(pmap[x][y].flags & HAS_DORMANT_MONSTER)) {
2029 return NULL;
2030 }
2031
2032 for (creatureIterator it = iterateCreatures(dormantMonsters); hasNextCreature(it);) {
2033 creature *monst = nextCreature(&it);
2034 if (monst->xLoc == x && monst->yLoc == y) {
2035 return monst;
2036 }
2037 }
2038 // This should be unreachable, since the HAS_DORMANT_MONSTER
2039 // flag was true at (x, y).
2040 brogueAssert(0);
2041 return NULL;
2042 }
2043
monsterHasBoltEffect(creature * monst,enum boltEffects boltEffectIndex)2044 enum boltType monsterHasBoltEffect(creature *monst, enum boltEffects boltEffectIndex) {
2045 short i;
2046 for (i=0; monst->info.bolts[i] != 0; i++) {
2047 if (boltCatalog[monst->info.bolts[i]].boltEffect == boltEffectIndex) {
2048 return monst->info.bolts[i];
2049 }
2050 }
2051 return BOLT_NONE;
2052 }
2053
pathTowardCreature(creature * monst,creature * target)2054 void pathTowardCreature(creature *monst, creature *target) {
2055 short targetLoc[2], dir;
2056
2057 if (traversiblePathBetween(monst, target->xLoc, target->yLoc)) {
2058 if (distanceBetween(monst->xLoc, monst->yLoc, target->xLoc, target->yLoc) <= 2) {
2059 monst->bookkeepingFlags &= ~MB_GIVEN_UP_ON_SCENT;
2060 }
2061 targetLoc[0] = target->xLoc;
2062 targetLoc[1] = target->yLoc;
2063 moveMonsterPassivelyTowards(monst, targetLoc, (monst->creatureState != MONSTER_ALLY));
2064 return;
2065 }
2066
2067 // is the target missing his map altogether?
2068 if (!target->mapToMe) {
2069 target->mapToMe = allocGrid();
2070 fillGrid(target->mapToMe, 0);
2071 calculateDistances(target->mapToMe, target->xLoc, target->yLoc, 0, monst, true, false);
2072 }
2073
2074 // is the target map out of date?
2075 if (target->mapToMe[target->xLoc][target->yLoc] > 3) {
2076 // it is. recalculate the map.
2077 calculateDistances(target->mapToMe, target->xLoc, target->yLoc, 0, monst, true, false);
2078 }
2079
2080 // blink to the target?
2081 if (distanceBetween(monst->xLoc, monst->yLoc, target->xLoc, target->yLoc) > 10
2082 || monstersAreEnemies(monst, target)) {
2083
2084 if (monsterBlinkToPreferenceMap(monst, target->mapToMe, false)) { // if it blinked
2085 monst->ticksUntilTurn = monst->attackSpeed * (monst->info.flags & MONST_CAST_SPELLS_SLOWLY ? 2 : 1);
2086 return;
2087 }
2088 }
2089
2090 // follow the map.
2091 dir = nextStep(target->mapToMe, monst->xLoc, monst->yLoc, monst, true);
2092 if (dir == NO_DIRECTION) {
2093 dir = randValidDirectionFrom(monst, monst->xLoc, monst->yLoc, true);
2094 }
2095 if (dir == NO_DIRECTION) {
2096 return; // monster is blocked
2097 }
2098 targetLoc[0] = monst->xLoc + nbDirs[dir][0];
2099 targetLoc[1] = monst->yLoc + nbDirs[dir][1];
2100
2101 moveMonsterPassivelyTowards(monst, targetLoc, (monst->creatureState != MONSTER_ALLY));
2102 }
2103
creatureEligibleForSwarming(creature * monst)2104 boolean creatureEligibleForSwarming(creature *monst) {
2105 if ((monst->info.flags & (MONST_IMMOBILE | MONST_GETS_TURN_ON_ACTIVATION | MONST_MAINTAINS_DISTANCE))
2106 || monst->status[STATUS_ENTRANCED]
2107 || monst->status[STATUS_CONFUSED]
2108 || monst->status[STATUS_STUCK]
2109 || monst->status[STATUS_PARALYZED]
2110 || monst->status[STATUS_MAGICAL_FEAR]
2111 || monst->status[STATUS_LIFESPAN_REMAINING] == 1
2112 || (monst->bookkeepingFlags & (MB_SEIZED | MB_SEIZING))) {
2113
2114 return false;
2115 }
2116 if (monst != &player
2117 && monst->creatureState != MONSTER_ALLY
2118 && monst->creatureState != MONSTER_TRACKING_SCENT) {
2119
2120 return false;
2121 }
2122 return true;
2123 }
2124
2125 // Swarming behavior.
2126 // If you’re adjacent to an enemy and about to strike it, and you’re adjacent to a hunting-mode tribemate
2127 // who is not adjacent to another enemy, and there is no empty space adjacent to the tribemate AND the enemy,
2128 // and there is an empty space adjacent to you AND the enemy, then move into that last space.
2129 // (In each case, "adjacent" excludes diagonal tiles obstructed by corner walls.)
monsterSwarmDirection(creature * monst,creature * enemy)2130 enum directions monsterSwarmDirection(creature *monst, creature *enemy) {
2131 short newX, newY, i;
2132 enum directions dir, targetDir;
2133 short dirList[8] = {0, 1, 2, 3, 4, 5, 6, 7};
2134 boolean alternateDirectionExists;
2135
2136 if (monst == &player || !creatureEligibleForSwarming(monst)) {
2137 return NO_DIRECTION;
2138 }
2139
2140 if (distanceBetween(monst->xLoc, monst->yLoc, enemy->xLoc, enemy->yLoc) != 1
2141 || (diagonalBlocked(monst->xLoc, monst->yLoc, enemy->xLoc, enemy->yLoc, false) || (enemy->info.flags & MONST_ATTACKABLE_THRU_WALLS))
2142 || !monstersAreEnemies(monst, enemy)) {
2143
2144 return NO_DIRECTION; // Too far from the enemy, diagonally blocked, or not enemies with it.
2145 }
2146
2147 // Find a location that is adjacent to you and to the enemy.
2148 targetDir = NO_DIRECTION;
2149 shuffleList(dirList, 4);
2150 shuffleList(&(dirList[4]), 4);
2151 for (i=0; i<8 && targetDir == NO_DIRECTION; i++) {
2152 dir = dirList[i];
2153 newX = monst->xLoc + nbDirs[dir][0];
2154 newY = monst->yLoc + nbDirs[dir][1];
2155 if (coordinatesAreInMap(newX, newY)
2156 && distanceBetween(enemy->xLoc, enemy->yLoc, newX, newY) == 1
2157 && !(pmap[newX][newY].flags & (HAS_PLAYER | HAS_MONSTER))
2158 && !diagonalBlocked(monst->xLoc, monst->yLoc, newX, newY, false)
2159 && (!diagonalBlocked(enemy->xLoc, enemy->yLoc, newX, newY, false) || (enemy->info.flags & MONST_ATTACKABLE_THRU_WALLS))
2160 && !monsterAvoids(monst, newX, newY)) {
2161
2162 targetDir = dir;
2163 }
2164 }
2165 if (targetDir == NO_DIRECTION) {
2166 return NO_DIRECTION; // No open location next to both you and the enemy.
2167 }
2168
2169 // OK, now we have a place to move toward. Let's analyze the teammates around us to make sure that
2170 // one of them could take advantage of the space we open.
2171 boolean handledPlayer = false;
2172 for (creatureIterator it = iterateCreatures(monsters); !handledPlayer || hasNextCreature(it);) {
2173 creature *ally = !handledPlayer ? &player : nextCreature(&it);
2174 handledPlayer = true;
2175 if (ally != monst
2176 && ally != enemy
2177 && monstersAreTeammates(monst, ally)
2178 && monstersAreEnemies(ally, enemy)
2179 && creatureEligibleForSwarming(ally)
2180 && distanceBetween(monst->xLoc, monst->yLoc, ally->xLoc, ally->yLoc) == 1
2181 && !diagonalBlocked(monst->xLoc, monst->yLoc, ally->xLoc, ally->yLoc, false)
2182 && !monsterAvoids(ally, monst->xLoc, monst->yLoc)
2183 && (distanceBetween(enemy->xLoc, enemy->yLoc, ally->xLoc, ally->yLoc) > 1 || diagonalBlocked(enemy->xLoc, enemy->yLoc, ally->xLoc, ally->yLoc, false))) {
2184
2185 // Found a prospective ally.
2186 // Check that there isn't already an open space from which to attack the enemy that is accessible to the ally.
2187 alternateDirectionExists = false;
2188 for (dir=0; dir< DIRECTION_COUNT && !alternateDirectionExists; dir++) {
2189 newX = ally->xLoc + nbDirs[dir][0];
2190 newY = ally->yLoc + nbDirs[dir][1];
2191 if (coordinatesAreInMap(newX, newY)
2192 && !(pmap[newX][newY].flags & (HAS_PLAYER | HAS_MONSTER))
2193 && distanceBetween(enemy->xLoc, enemy->yLoc, newX, newY) == 1
2194 && !diagonalBlocked(enemy->xLoc, enemy->yLoc, newX, newY, false)
2195 && !diagonalBlocked(ally->xLoc, ally->yLoc, newX, newY, false)
2196 && !monsterAvoids(ally, newX, newY)) {
2197
2198 alternateDirectionExists = true;
2199 }
2200 }
2201 if (!alternateDirectionExists) {
2202 // OK, no alternative open spaces exist.
2203 // Check that the ally isn't already occupied with an enemy of its own.
2204 boolean foundConflict = false;
2205 boolean handledPlayer = false;
2206 for (creatureIterator it2 = iterateCreatures(monsters); !handledPlayer || hasNextCreature(it2);) {
2207 creature *otherEnemy = !handledPlayer ? &player : nextCreature(&it2);
2208 handledPlayer = true;
2209 if (ally != otherEnemy
2210 && monst != otherEnemy
2211 && enemy != otherEnemy
2212 && monstersAreEnemies(ally, otherEnemy)
2213 && distanceBetween(ally->xLoc, ally->yLoc, otherEnemy->xLoc, otherEnemy->yLoc) == 1
2214 && (!diagonalBlocked(ally->xLoc, ally->yLoc, otherEnemy->xLoc, otherEnemy->yLoc, false) || (otherEnemy->info.flags & MONST_ATTACKABLE_THRU_WALLS))) {
2215
2216 foundConflict = true;
2217 break; // Ally is already occupied.
2218 }
2219 }
2220 if (!foundConflict) {
2221 // Success!
2222 return targetDir;
2223 }
2224 }
2225 }
2226 }
2227 return NO_DIRECTION; // Failure!
2228 }
2229
2230 // Isomorphs a number in [0, 39] to coordinates along the square of radius 5 surrounding (0,0).
2231 // This is used as the sample space for bolt target coordinates, e.g. when reflecting or when
2232 // monsters are deciding where to blink.
perimeterCoords(short returnCoords[2],short n)2233 void perimeterCoords(short returnCoords[2], short n) {
2234 if (n <= 10) { // top edge, left to right
2235 returnCoords[0] = n - 5;
2236 returnCoords[1] = -5;
2237 } else if (n <= 21) { // bottom edge, left to right
2238 returnCoords[0] = (n - 11) - 5;
2239 returnCoords[1] = 5;
2240 } else if (n <= 30) { // left edge, top to bottom
2241 returnCoords[0] = -5;
2242 returnCoords[1] = (n - 22) - 4;
2243 } else if (n <= 39) { // right edge, top to bottom
2244 returnCoords[0] = 5;
2245 returnCoords[1] = (n - 31) - 4;
2246 } else {
2247 message("ERROR! Bad perimeter coordinate request!", REQUIRE_ACKNOWLEDGMENT);
2248 returnCoords[0] = returnCoords[1] = 0; // garbage in, garbage out
2249 }
2250 }
2251
2252 // Tries to make the monster blink to the most desirable square it can aim at, according to the
2253 // preferenceMap argument. "blinkUphill" determines whether it's aiming for higher or lower numbers on
2254 // the preference map -- true means higher. Returns true if the monster blinked; false if it didn't.
monsterBlinkToPreferenceMap(creature * monst,short ** preferenceMap,boolean blinkUphill)2255 boolean monsterBlinkToPreferenceMap(creature *monst, short **preferenceMap, boolean blinkUphill) {
2256 short i, bestTarget[2], bestPreference, nowPreference, maxDistance, target[2], impact[2], origin[2];
2257 boolean gotOne;
2258 char monstName[DCOLS];
2259 char buf[DCOLS];
2260 enum boltType theBoltType;
2261 bolt theBolt;
2262
2263 theBoltType = monsterHasBoltEffect(monst, BE_BLINKING);
2264 if (!theBoltType) {
2265 return false;
2266 }
2267
2268 maxDistance = staffBlinkDistance(5 * FP_FACTOR);
2269 gotOne = false;
2270
2271 origin[0] = monst->xLoc;
2272 origin[1] = monst->yLoc;
2273
2274 bestTarget[0] = 0;
2275 bestTarget[1] = 0;
2276 bestPreference = preferenceMap[monst->xLoc][monst->yLoc];
2277
2278 // make sure that we beat the four cardinal neighbors
2279 for (i = 0; i < 4; i++) {
2280 nowPreference = preferenceMap[monst->xLoc + nbDirs[i][0]][monst->yLoc + nbDirs[i][1]];
2281
2282 if (((blinkUphill && nowPreference > bestPreference) || (!blinkUphill && nowPreference < bestPreference))
2283 && !monsterAvoids(monst, monst->xLoc + nbDirs[i][0], monst->yLoc + nbDirs[i][1])) {
2284
2285 bestPreference = nowPreference;
2286 }
2287 }
2288
2289 for (i=0; i<40; i++) {
2290 perimeterCoords(target, i);
2291 target[0] += monst->xLoc;
2292 target[1] += monst->yLoc;
2293
2294 getImpactLoc(impact, origin, target, maxDistance, true, &boltCatalog[BOLT_BLINKING]);
2295 nowPreference = preferenceMap[impact[0]][impact[1]];
2296
2297 if (((blinkUphill && (nowPreference > bestPreference))
2298 || (!blinkUphill && (nowPreference < bestPreference)))
2299 && !monsterAvoids(monst, impact[0], impact[1])) {
2300
2301 bestTarget[0] = target[0];
2302 bestTarget[1] = target[1];
2303 bestPreference = nowPreference;
2304
2305 if ((abs(impact[0] - origin[0]) > 1 || abs(impact[1] - origin[1]) > 1)
2306 || (cellHasTerrainFlag(impact[0], origin[1], T_OBSTRUCTS_PASSABILITY))
2307 || (cellHasTerrainFlag(origin[0], impact[1], T_OBSTRUCTS_PASSABILITY))) {
2308 gotOne = true;
2309 } else {
2310 gotOne = false;
2311 }
2312 }
2313 }
2314
2315 if (gotOne) {
2316 if (canDirectlySeeMonster(monst)) {
2317 monsterName(monstName, monst, true);
2318 sprintf(buf, "%s blinks", monstName);
2319 combatMessage(buf, 0);
2320 }
2321 monst->ticksUntilTurn = monst->attackSpeed * (monst->info.flags & MONST_CAST_SPELLS_SLOWLY ? 2 : 1);
2322 theBolt = boltCatalog[theBoltType];
2323 zap(origin, bestTarget, &theBolt, false);
2324 return true;
2325 }
2326 return false;
2327 }
2328
fleeingMonsterAwareOfPlayer(creature * monst)2329 boolean fleeingMonsterAwareOfPlayer(creature *monst) {
2330 if (player.status[STATUS_INVISIBLE]) {
2331 return (distanceBetween(monst->xLoc, monst->yLoc, player.xLoc, player.yLoc) <= 1);
2332 } else {
2333 return (pmap[monst->xLoc][monst->yLoc].flags & IN_FIELD_OF_VIEW) ? true : false;
2334 }
2335 }
2336
getSafetyMap(creature * monst)2337 static short **getSafetyMap(creature *monst) {
2338 if (fleeingMonsterAwareOfPlayer(monst)) {
2339 if (monst->safetyMap) {
2340 freeGrid(monst->safetyMap);
2341 monst->safetyMap = NULL;
2342 }
2343 if (!rogue.updatedSafetyMapThisTurn) {
2344 updateSafetyMap();
2345 }
2346 return safetyMap;
2347 } else {
2348 if (!monst->safetyMap) {
2349 if (!rogue.updatedSafetyMapThisTurn) {
2350 updateSafetyMap();
2351 }
2352 monst->safetyMap = allocGrid();
2353 copyGrid(monst->safetyMap, safetyMap);
2354 }
2355 return monst->safetyMap;
2356 }
2357 }
2358
2359 // returns whether the monster did something (and therefore ended its turn)
monsterBlinkToSafety(creature * monst)2360 boolean monsterBlinkToSafety(creature *monst) {
2361 short **blinkSafetyMap;
2362
2363 if (monst->creatureState == MONSTER_ALLY) {
2364 if (!rogue.updatedAllySafetyMapThisTurn) {
2365 updateAllySafetyMap();
2366 }
2367 blinkSafetyMap = allySafetyMap;
2368 } else {
2369 blinkSafetyMap = getSafetyMap(monst);
2370 }
2371
2372 return monsterBlinkToPreferenceMap(monst, blinkSafetyMap, false);
2373 }
2374
monsterSummons(creature * monst,boolean alwaysUse)2375 boolean monsterSummons(creature *monst, boolean alwaysUse) {
2376 short minionCount = 0;
2377
2378 if (monst->info.abilityFlags & (MA_CAST_SUMMON)) {
2379 // Count existing minions.
2380 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
2381 creature *target = nextCreature(&it);
2382 if (monst->creatureState == MONSTER_ALLY) {
2383 if (target->creatureState == MONSTER_ALLY) {
2384 minionCount++; // Allied summoners count all allies.
2385 }
2386 } else if ((target->bookkeepingFlags & MB_FOLLOWER) && target->leader == monst) {
2387 minionCount++; // Enemy summoners count only direct followers, not teammates.
2388 }
2389 }
2390 if (monst->creatureState == MONSTER_ALLY) { // Allied summoners also count monsters on the previous and next depths.
2391 if (rogue.depthLevel > 1) {
2392 for (creatureIterator it = iterateCreatures(&levels[rogue.depthLevel - 2].monsters); hasNextCreature(it);) {
2393 creature *target = nextCreature(&it);
2394 if (target->creatureState == MONSTER_ALLY && !(target->info.flags & MONST_WILL_NOT_USE_STAIRS)) {
2395 minionCount++;
2396 }
2397 }
2398 }
2399 if (rogue.depthLevel < DEEPEST_LEVEL) {
2400 for (creatureIterator it = iterateCreatures(&levels[rogue.depthLevel].monsters); hasNextCreature(it);) {
2401 creature *target = nextCreature(&it);
2402 if (target->creatureState == MONSTER_ALLY && !(target->info.flags & MONST_WILL_NOT_USE_STAIRS)) {
2403 minionCount++;
2404 }
2405 }
2406 }
2407 }
2408 if (alwaysUse && minionCount < 50) {
2409 summonMinions(monst);
2410 return true;
2411 } else if (monst->info.abilityFlags & MA_ENTER_SUMMONS) {
2412 if (!rand_range(0, 7)) {
2413 summonMinions(monst);
2414 return true;
2415 }
2416 } else if ((monst->creatureState != MONSTER_ALLY || minionCount < 5)
2417 && !rand_range(0, minionCount * minionCount * 3 + 1)) {
2418
2419 summonMinions(monst);
2420 return true;
2421 }
2422 }
2423 return false;
2424 }
2425
2426 // Some monsters never make good targets irrespective of what bolt we're contemplating.
2427 // Return false for those. Otherwise, return true.
generallyValidBoltTarget(creature * caster,creature * target)2428 boolean generallyValidBoltTarget(creature *caster, creature *target) {
2429 if (caster == target) {
2430 // Can't target yourself; that's the fundamental theorem of Brogue bolts.
2431 return false;
2432 }
2433 if (caster->status[STATUS_DISCORDANT]
2434 && caster->creatureState == MONSTER_WANDERING
2435 && target == &player) {
2436 // Discordant monsters always try to cast spells regardless of whether
2437 // they're hunting the player, so that they cast at other monsters. This
2438 // by bypasses the usual awareness checks, so the player and any allies
2439 // can be hit when far away. Hence, we don't target the player with
2440 // bolts if we're discordant and wandering.
2441 return false;
2442 }
2443 if (caster->creatureState == MONSTER_ALLY && !caster->status[STATUS_DISCORDANT]
2444 && (target->bookkeepingFlags & MB_MARKED_FOR_SACRIFICE)) {
2445 // Don't let (sane) allies cast at sacrifice targets.
2446 return false;
2447 }
2448
2449 if (monsterIsHidden(target, caster)
2450 || (target->bookkeepingFlags & MB_SUBMERGED)) {
2451 // No bolt will affect a submerged creature. Can't shoot at invisible creatures unless it's in gas.
2452 return false;
2453 }
2454 return openPathBetween(caster->xLoc, caster->yLoc, target->xLoc, target->yLoc);
2455 }
2456
targetEligibleForCombatBuff(creature * caster,creature * target)2457 boolean targetEligibleForCombatBuff(creature *caster, creature *target) {
2458 if (caster->creatureState == MONSTER_ALLY) {
2459 if (canDirectlySeeMonster(caster)) {
2460 boolean handledPlayer = false;
2461 for (creatureIterator it = iterateCreatures(monsters); !handledPlayer || hasNextCreature(it);) {
2462 creature *enemy = !handledPlayer ? &player : nextCreature(&it);
2463 handledPlayer = true;
2464 if (monstersAreEnemies(&player, enemy)
2465 && canSeeMonster(enemy)
2466 && (pmap[enemy->xLoc][enemy->yLoc].flags & IN_FIELD_OF_VIEW)) {
2467
2468 return true;
2469 }
2470 }
2471 }
2472 return false;
2473 } else {
2474 return (target->creatureState == MONSTER_TRACKING_SCENT);
2475 }
2476 }
2477
2478 // Make a decision as to whether the given caster should fire the given bolt at the given target.
2479 // Assumes that the conditions in generallyValidBoltTarget have already been satisfied.
specificallyValidBoltTarget(creature * caster,creature * target,enum boltType theBoltType)2480 boolean specificallyValidBoltTarget(creature *caster, creature *target, enum boltType theBoltType) {
2481
2482 if ((boltCatalog[theBoltType].flags & BF_TARGET_ALLIES)
2483 && (!monstersAreTeammates(caster, target) || monstersAreEnemies(caster, target))) {
2484
2485 return false;
2486 }
2487 if ((boltCatalog[theBoltType].flags & BF_TARGET_ENEMIES)
2488 && (!monstersAreEnemies(caster, target))) {
2489
2490 return false;
2491 }
2492 if ((boltCatalog[theBoltType].flags & BF_TARGET_ENEMIES)
2493 && (target->info.flags & MONST_INVULNERABLE)) {
2494
2495 return false;
2496 }
2497 if ((target->info.flags & MONST_REFLECT_4)
2498 && target->creatureState != MONSTER_ALLY
2499 && !(boltCatalog[theBoltType].flags & (BF_NEVER_REFLECTS | BF_HALTS_BEFORE_OBSTRUCTION))) {
2500 // Don't fire a reflectable bolt at a reflective target unless it's your ally.
2501 return false;
2502 }
2503 if (boltCatalog[theBoltType].forbiddenMonsterFlags & target->info.flags) {
2504 // Don't fire a bolt at a creature type that it won't affect.
2505 return false;
2506 }
2507 if ((boltCatalog[theBoltType].flags & BF_FIERY)
2508 && target->status[STATUS_IMMUNE_TO_FIRE]) {
2509 // Don't shoot fireballs at fire-immune creatures.
2510 return false;
2511 }
2512 if ((boltCatalog[theBoltType].flags & BF_FIERY)
2513 && burnedTerrainFlagsAtLoc(caster->xLoc, caster->yLoc) & avoidedFlagsForMonster(&(caster->info))) {
2514 // Don't shoot fireballs if you're standing on a tile that could combust into something that harms you.
2515 return false;
2516 }
2517
2518 // Rules specific to bolt effects:
2519 switch (boltCatalog[theBoltType].boltEffect) {
2520 case BE_BECKONING:
2521 if (distanceBetween(caster->xLoc, caster->yLoc, target->xLoc, target->yLoc) <= 1) {
2522 return false;
2523 }
2524 break;
2525 case BE_ATTACK:
2526 if (cellHasTerrainFlag(target->xLoc, target->yLoc, T_OBSTRUCTS_PASSABILITY)
2527 && !(target->info.flags & MONST_ATTACKABLE_THRU_WALLS)) {
2528 // Don't shoot an arrow at an embedded creature.
2529 return false;
2530 }
2531 // continue to BE_DAMAGE below
2532 case BE_DAMAGE:
2533 if (target->status[STATUS_ENTRANCED]
2534 && monstersAreEnemies(caster, target)) {
2535 // Don't break your enemies' entrancement.
2536 return false;
2537 }
2538 break;
2539 case BE_NONE:
2540 // BE_NONE bolts are always going to be all about the terrain effects,
2541 // so our logic has to follow from the terrain parameters of the bolt's target DF.
2542 if (boltCatalog[theBoltType].targetDF) {
2543 const unsigned long terrainFlags = tileCatalog[dungeonFeatureCatalog[boltCatalog[theBoltType].targetDF].tile].flags;
2544 if ((terrainFlags & T_ENTANGLES)
2545 && target->status[STATUS_STUCK]) {
2546 // Don't try to entangle a creature that is already entangled.
2547 return false;
2548 }
2549 if ((boltCatalog[theBoltType].flags & BF_TARGET_ENEMIES)
2550 && !(terrainFlags & avoidedFlagsForMonster(&(target->info)))
2551 && (!(terrainFlags & T_ENTANGLES) || (target->info.flags & MONST_IMMUNE_TO_WEBS))) {
2552
2553 return false;
2554 }
2555 }
2556 break;
2557 case BE_DISCORD:
2558 if (target->status[STATUS_DISCORDANT]
2559 || target == &player) {
2560 // Don't cast discord if the target is already discordant, or if it is the player.
2561 // (Players should never be intentionally targeted by discord. It's just a fact of monster psychology.)
2562 return false;
2563 }
2564 break;
2565 case BE_NEGATION:
2566 if (monstersAreEnemies(caster, target)) {
2567 if (target->status[STATUS_HASTED] || target->status[STATUS_TELEPATHIC] || target->status[STATUS_SHIELDED]) {
2568 // Dispel haste, telepathy, protection.
2569 return true;
2570 }
2571 if (target->info.flags & (MONST_DIES_IF_NEGATED | MONST_IMMUNE_TO_WEAPONS)) {
2572 // Dispel magic creatures; strip weapon invulnerability from revenants.
2573 return true;
2574 }
2575 if ((target->status[STATUS_IMMUNE_TO_FIRE] || target->status[STATUS_LEVITATING])
2576 && cellHasTerrainFlag(target->xLoc, target->yLoc, (T_LAVA_INSTA_DEATH | T_IS_DEEP_WATER | T_AUTO_DESCENT))) {
2577 // Drop the target into lava or a chasm if opportunity knocks.
2578 return true;
2579 }
2580 if (monstersAreTeammates(caster, target)
2581 && target->status[STATUS_DISCORDANT]
2582 && !(target->info.flags & MONST_DIES_IF_NEGATED)) {
2583 // Dispel discord from allies unless it would destroy them.
2584 return true;
2585 }
2586 } else if (monstersAreTeammates(caster, target)) {
2587 if (target == &player && rogue.armor && (rogue.armor->flags & ITEM_RUNIC) && (rogue.armor->flags & ITEM_RUNIC_IDENTIFIED)
2588 && rogue.armor->enchant2 == A_REFLECTION && netEnchant(rogue.armor) > 0) {
2589 // Allies shouldn't cast negation on the player if she's knowingly wearing armor of reflection.
2590 // Too much risk of negating themselves in the process.
2591 return false;
2592 }
2593 if (target->info.flags & MONST_DIES_IF_NEGATED) {
2594 // Never cast negation if it would destroy an allied creature.
2595 return false;
2596 }
2597 if (target->status[STATUS_ENTRANCED]
2598 && caster->creatureState != MONSTER_ALLY) {
2599 // Non-allied monsters will dispel entrancement on their own kind.
2600 return true;
2601 }
2602 if (target->status[STATUS_MAGICAL_FEAR]) {
2603 // Dispel magical fear.
2604 return true;
2605 }
2606 }
2607 return false; // Don't cast negation unless there's a good reason.
2608 break;
2609 case BE_SLOW:
2610 if (target->status[STATUS_SLOWED]) {
2611 return false;
2612 }
2613 break;
2614 case BE_HASTE:
2615 if (target->status[STATUS_HASTED]) {
2616 return false;
2617 }
2618 if (!targetEligibleForCombatBuff(caster, target)) {
2619 return false;
2620 }
2621 break;
2622 case BE_SHIELDING:
2623 if (target->status[STATUS_SHIELDED]) {
2624 return false;
2625 }
2626 if (!targetEligibleForCombatBuff(caster, target)) {
2627 return false;
2628 }
2629 break;
2630 case BE_HEALING:
2631 if (target->currentHP >= target->info.maxHP) {
2632 // Don't heal a creature already at full health.
2633 return false;
2634 }
2635 break;
2636 case BE_TUNNELING:
2637 case BE_OBSTRUCTION:
2638 // Monsters will never cast these.
2639 return false;
2640 break;
2641 default:
2642 break;
2643 }
2644 return true;
2645 }
2646
monsterCastSpell(creature * caster,creature * target,enum boltType boltIndex)2647 void monsterCastSpell(creature *caster, creature *target, enum boltType boltIndex) {
2648 bolt theBolt;
2649 short originLoc[2], targetLoc[2];
2650 char buf[200], monstName[100];
2651
2652 if (canDirectlySeeMonster(caster)) {
2653 monsterName(monstName, caster, true);
2654 sprintf(buf, "%s %s", monstName, boltCatalog[boltIndex].description);
2655 resolvePronounEscapes(buf, caster);
2656 combatMessage(buf, 0);
2657 }
2658
2659 theBolt = boltCatalog[boltIndex];
2660 originLoc[0] = caster->xLoc;
2661 originLoc[1] = caster->yLoc;
2662 targetLoc[0] = target->xLoc;
2663 targetLoc[1] = target->yLoc;
2664 zap(originLoc, targetLoc, &theBolt, false);
2665
2666 if (player.currentHP <= 0) {
2667 gameOver(monsterCatalog[caster->info.monsterID].monsterName, false);
2668 }
2669 }
2670
2671 // returns whether the monster cast a bolt.
monstUseBolt(creature * monst)2672 boolean monstUseBolt(creature *monst) {
2673 short i;
2674
2675 if (!monst->info.bolts[0]) {
2676 return false; // Don't waste time with monsters that can't cast anything.
2677 }
2678
2679 boolean handledPlayer = false;
2680 for (creatureIterator it = iterateCreatures(monsters); !handledPlayer || hasNextCreature(it);) {
2681 creature *target = !handledPlayer ? &player : nextCreature(&it);
2682 handledPlayer = true;
2683 if (generallyValidBoltTarget(monst, target)) {
2684 for (i = 0; monst->info.bolts[i]; i++) {
2685 if (boltCatalog[monst->info.bolts[i]].boltEffect == BE_BLINKING) {
2686 continue; // Blinking is handled elsewhere.
2687 }
2688 if (specificallyValidBoltTarget(monst, target, monst->info.bolts[i])) {
2689 if ((monst->info.flags & MONST_ALWAYS_USE_ABILITY)
2690 || rand_percent(30)) {
2691
2692 monsterCastSpell(monst, target, monst->info.bolts[i]);
2693 return true;
2694 }
2695 }
2696 }
2697 }
2698 }
2699 return false;
2700 }
2701
2702 // returns whether the monster did something (and therefore ended its turn)
monstUseMagic(creature * monst)2703 boolean monstUseMagic(creature *monst) {
2704 if (monsterSummons(monst, (monst->info.flags & MONST_ALWAYS_USE_ABILITY))) {
2705 return true;
2706 } else if (monstUseBolt(monst)) {
2707 return true;
2708 }
2709 return false;
2710 }
2711
isLocalScentMaximum(short x,short y)2712 boolean isLocalScentMaximum(short x, short y) {
2713 enum directions dir;
2714 short newX, newY;
2715
2716 const short baselineScent = scentMap[x][y];
2717
2718 for (dir=0; dir< DIRECTION_COUNT; dir++) {
2719 newX = x + nbDirs[dir][0];
2720 newY = y + nbDirs[dir][1];
2721 if (coordinatesAreInMap(newX, newY)
2722 && (scentMap[newX][newY] > baselineScent)
2723 && !cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_PASSABILITY)
2724 && !diagonalBlocked(x, y, newX, newY, false)) {
2725
2726 return false;
2727 }
2728 }
2729 return true;
2730 }
2731
2732 // Returns the direction the player's scent points to from a given cell. Returns -1 if the nose comes up blank.
scentDirection(creature * monst)2733 enum directions scentDirection(creature *monst) {
2734 short newX, newY, x, y, newestX, newestY;
2735 enum directions bestDirection = NO_DIRECTION, dir, dir2;
2736 unsigned short bestNearbyScent = 0;
2737 boolean canTryAgain = true;
2738 creature *otherMonst;
2739
2740 x = monst->xLoc;
2741 y = monst->yLoc;
2742
2743 for (;;) {
2744
2745 for (dir=0; dir< DIRECTION_COUNT; dir++) {
2746 newX = x + nbDirs[dir][0];
2747 newY = y + nbDirs[dir][1];
2748 otherMonst = monsterAtLoc(newX, newY);
2749 if (coordinatesAreInMap(newX, newY)
2750 && (scentMap[newX][newY] > bestNearbyScent)
2751 && (!(pmap[newX][newY].flags & HAS_MONSTER) || (otherMonst && canPass(monst, otherMonst)))
2752 && !cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_PASSABILITY)
2753 && !diagonalBlocked(x, y, newX, newY, false)
2754 && !monsterAvoids(monst, newX, newY)) {
2755
2756 bestNearbyScent = scentMap[newX][newY];
2757 bestDirection = dir;
2758 }
2759 }
2760
2761 if (bestDirection >= 0 && bestNearbyScent > scentMap[x][y]) {
2762 return bestDirection;
2763 }
2764
2765 if (canTryAgain) {
2766 // Okay, the monster may be stuck in some irritating diagonal.
2767 // If so, we can diffuse the scent into the offending kink and solve the problem.
2768 // There's a possibility he's stuck for some other reason, though, so we'll only
2769 // try once per his move -- hence the failsafe.
2770 canTryAgain = false;
2771 for (dir=0; dir<4; dir++) {
2772 newX = x + nbDirs[dir][0];
2773 newY = y + nbDirs[dir][1];
2774 for (dir2=0; dir2<4; dir2++) {
2775 newestX = newX + nbDirs[dir2][0];
2776 newestY = newY + nbDirs[dir2][1];
2777 if (coordinatesAreInMap(newX, newY) && coordinatesAreInMap(newestX, newestY)) {
2778 scentMap[newX][newY] = max(scentMap[newX][newY], scentMap[newestX][newestY] - 1);
2779 }
2780 }
2781 }
2782 } else {
2783 return NO_DIRECTION; // failure!
2784 }
2785 }
2786 }
2787
2788 // returns true if the resurrection was successful.
resurrectAlly(const short x,const short y)2789 boolean resurrectAlly(const short x, const short y) {
2790 boolean success;
2791 creature *monst = firstCreature(&purgatory);
2792 if (monst) {
2793 // Remove from purgatory and insert into the mortal plane.
2794 removeCreature(&purgatory, monst);
2795 prependCreature(monsters, monst);
2796
2797 getQualifyingPathLocNear(&monst->xLoc, &monst->yLoc, x, y, true,
2798 (T_PATHING_BLOCKER | T_HARMFUL_TERRAIN), 0,
2799 0, (HAS_PLAYER | HAS_MONSTER), false);
2800 pmap[monst->xLoc][monst->yLoc].flags |= HAS_MONSTER;
2801
2802 // Restore health etc.
2803 monst->bookkeepingFlags &= ~(MB_IS_DYING | MB_IS_FALLING);
2804 if (!(monst->info.flags & MONST_FIERY)
2805 && monst->status[STATUS_BURNING]) {
2806
2807 monst->status[STATUS_BURNING] = 0;
2808 }
2809 monst->status[STATUS_DISCORDANT] = 0;
2810 heal(monst, 100, true);
2811
2812 success = true;
2813 } else {
2814 success = false;
2815 }
2816 return success;
2817 }
2818
unAlly(creature * monst)2819 void unAlly(creature *monst) {
2820 if (monst->creatureState == MONSTER_ALLY) {
2821 monst->creatureState = MONSTER_TRACKING_SCENT;
2822 monst->bookkeepingFlags &= ~(MB_FOLLOWER | MB_TELEPATHICALLY_REVEALED);
2823 monst->leader = NULL;
2824 }
2825 }
2826
monsterFleesFrom(creature * monst,creature * defender)2827 boolean monsterFleesFrom(creature *monst, creature *defender) {
2828 const short x = monst->xLoc;
2829 const short y = monst->yLoc;
2830
2831 if (!monsterWillAttackTarget(defender, monst)) {
2832 return false;
2833 }
2834
2835 if (distanceBetween(x, y, defender->xLoc, defender->yLoc) >= 4) {
2836 return false;
2837 }
2838
2839 if ((defender->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE))
2840 && !(defender->info.flags & MONST_IMMOBILE)) {
2841 // Don't charge if the monster is damage-immune and is NOT immobile;
2842 // i.e., keep distance from revenants and stone guardians but not mirror totems.
2843 return true;
2844 }
2845
2846 if (monst->creatureState == MONSTER_ALLY && !monst->status[STATUS_DISCORDANT]
2847 && (defender->bookkeepingFlags & MB_MARKED_FOR_SACRIFICE)) {
2848 // Willing allies shouldn't charge sacrifice targets.
2849 return true;
2850 }
2851
2852 if ((monst->info.flags & MONST_MAINTAINS_DISTANCE)
2853 || (defender->info.abilityFlags & MA_KAMIKAZE)) {
2854
2855 // Don't charge if you maintain distance or if it's a kamikaze monster.
2856 return true;
2857 }
2858
2859 if (monst->info.abilityFlags & MA_POISONS
2860 && defender->status[STATUS_POISONED] * defender->poisonAmount > defender->currentHP) {
2861
2862 return true;
2863 }
2864
2865 return false;
2866 }
2867
allyFlees(creature * ally,creature * closestEnemy)2868 boolean allyFlees(creature *ally, creature *closestEnemy) {
2869 const short x = ally->xLoc;
2870 const short y = ally->yLoc;
2871
2872 if (!closestEnemy) {
2873 return false; // No one to flee from.
2874 }
2875
2876 if (ally->info.maxHP <= 1 || (ally->status[STATUS_LIFESPAN_REMAINING]) > 0) { // Spectral blades and timed allies should never flee.
2877 return false;
2878 }
2879
2880 if (distanceBetween(x, y, closestEnemy->xLoc, closestEnemy->yLoc) < 10
2881 && (100 * ally->currentHP / ally->info.maxHP <= 33)
2882 && ally->info.turnsBetweenRegen > 0
2883 && !ally->carriedMonster
2884 && ((ally->info.flags & MONST_FLEES_NEAR_DEATH) || (100 * ally->currentHP / ally->info.maxHP * 2 < 100 * player.currentHP / player.info.maxHP))) {
2885 // Flee if you're within 10 spaces, your HP is under 1/3, you're not a phoenix or lich or vampire in bat form,
2886 // and you either flee near death or your health fraction is less than half of the player's.
2887 return true;
2888 }
2889
2890 // so do allies that keep their distance or while in the presence of damage-immune or kamikaze enemies
2891 if (monsterFleesFrom(ally, closestEnemy)) {
2892 // Flee if you're within 3 spaces and you either flee near death or the closest enemy is a bloat, revenant or guardian.
2893 return true;
2894 }
2895
2896 return false;
2897 }
2898
monsterMillAbout(creature * monst,short movementChance)2899 void monsterMillAbout(creature *monst, short movementChance) {
2900 enum directions dir;
2901 short targetLoc[2];
2902
2903 const short x = monst->xLoc;
2904 const short y = monst->yLoc;
2905
2906 if (rand_percent(movementChance)) {
2907 dir = randValidDirectionFrom(monst, x, y, true);
2908 if (dir != -1) {
2909 targetLoc[0] = x + nbDirs[dir][0];
2910 targetLoc[1] = y + nbDirs[dir][1];
2911 moveMonsterPassivelyTowards(monst, targetLoc, false);
2912 }
2913 }
2914 }
2915
moveAlly(creature * monst)2916 void moveAlly(creature *monst) {
2917 creature *closestMonster = NULL;
2918 short i, j, x, y, dir, shortestDistance, targetLoc[2], leashLength;
2919 short **enemyMap, **costMap;
2920 char buf[DCOLS], monstName[DCOLS];
2921
2922 x = monst->xLoc;
2923 y = monst->yLoc;
2924
2925 targetLoc[0] = targetLoc[1] = 0;
2926
2927 if (!(monst->leader)) {
2928 monst->leader = &player;
2929 monst->bookkeepingFlags |= MB_FOLLOWER;
2930 }
2931
2932 // If we're standing in harmful terrain and there is a way to escape it, spend this turn escaping it.
2933 if (cellHasTerrainFlag(x, y, (T_HARMFUL_TERRAIN & ~(T_IS_FIRE | T_CAUSES_DAMAGE | T_CAUSES_PARALYSIS | T_CAUSES_CONFUSION)))
2934 || (cellHasTerrainFlag(x, y, T_IS_FIRE) && !monst->status[STATUS_IMMUNE_TO_FIRE])
2935 || (cellHasTerrainFlag(x, y, T_CAUSES_DAMAGE | T_CAUSES_PARALYSIS | T_CAUSES_CONFUSION) && !(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE)))) {
2936
2937 if (!rogue.updatedMapToSafeTerrainThisTurn) {
2938 updateSafeTerrainMap();
2939 }
2940
2941 if (monsterBlinkToPreferenceMap(monst, rogue.mapToSafeTerrain, false)) {
2942 monst->ticksUntilTurn = monst->attackSpeed * (monst->info.flags & MONST_CAST_SPELLS_SLOWLY ? 2 : 1);
2943 return;
2944 }
2945
2946 dir = nextStep(rogue.mapToSafeTerrain, x, y, monst, true);
2947 if (dir != -1) {
2948 targetLoc[0] = x + nbDirs[dir][0];
2949 targetLoc[1] = y + nbDirs[dir][1];
2950 if (moveMonsterPassivelyTowards(monst, targetLoc, false)) {
2951 return;
2952 }
2953 }
2954 }
2955
2956 // Look around for enemies; shortestDistance will be the distance to the nearest.
2957 shortestDistance = max(DROWS, DCOLS);
2958 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
2959 creature *target = nextCreature(&it);
2960 if (target != monst
2961 && (!(target->bookkeepingFlags & MB_SUBMERGED) || (monst->bookkeepingFlags & MB_SUBMERGED))
2962 && monsterWillAttackTarget(monst, target)
2963 && distanceBetween(x, y, target->xLoc, target->yLoc) < shortestDistance
2964 && traversiblePathBetween(monst, target->xLoc, target->yLoc)
2965 && (!cellHasTerrainFlag(target->xLoc, target->yLoc, T_OBSTRUCTS_PASSABILITY) || (target->info.flags & MONST_ATTACKABLE_THRU_WALLS))
2966 && (!target->status[STATUS_INVISIBLE] || rand_percent(33))) {
2967
2968 shortestDistance = distanceBetween(x, y, target->xLoc, target->yLoc);
2969 closestMonster = target;
2970 }
2971 }
2972
2973 // Weak allies in the presence of enemies seek safety;
2974 if (allyFlees(monst, closestMonster)) {
2975 if (monsterHasBoltEffect(monst, BE_BLINKING)
2976 && ((monst->info.flags & MONST_ALWAYS_USE_ABILITY) || rand_percent(30))
2977 && monsterBlinkToSafety(monst)) {
2978
2979 return;
2980 }
2981 if (monsterSummons(monst, (monst->info.flags & MONST_ALWAYS_USE_ABILITY))) {
2982 return;
2983 }
2984 if (!rogue.updatedAllySafetyMapThisTurn) {
2985 updateAllySafetyMap();
2986 }
2987 dir = nextStep(allySafetyMap, monst->xLoc, monst->yLoc, monst, true);
2988 if (dir != -1) {
2989 targetLoc[0] = x + nbDirs[dir][0];
2990 targetLoc[1] = y + nbDirs[dir][1];
2991 }
2992 if (dir == -1
2993 || (allySafetyMap[targetLoc[0]][targetLoc[1]] >= allySafetyMap[x][y])
2994 || (!moveMonster(monst, nbDirs[dir][0], nbDirs[dir][1]) && !moveMonsterPassivelyTowards(monst, targetLoc, true))) {
2995 // ally can't flee; continue below
2996 } else {
2997 return;
2998 }
2999 }
3000
3001 // Magic users sometimes cast spells.
3002 if (monstUseMagic(monst)) { // if he actually cast a spell
3003 monst->ticksUntilTurn = monst->attackSpeed * (monst->info.flags & MONST_CAST_SPELLS_SLOWLY ? 2 : 1);
3004 return;
3005 }
3006
3007 if (monst->bookkeepingFlags & MB_SEIZED) {
3008 leashLength = max(DCOLS, DROWS); // Ally will never be prevented from attacking while seized.
3009 } else if (rogue.justRested || rogue.justSearched) {
3010 leashLength = 10;
3011 } else {
3012 leashLength = 4;
3013 }
3014 if (shortestDistance == 1) {
3015 if (closestMonster->movementSpeed < monst->movementSpeed
3016 && !(closestMonster->info.flags & (MONST_FLITS | MONST_IMMOBILE))
3017 && closestMonster->creatureState == MONSTER_TRACKING_SCENT) {
3018 // Never try to flee from combat with a faster enemy.
3019 leashLength = max(DCOLS, DROWS);
3020 } else {
3021 leashLength++; // If the ally is adjacent to a monster at the end of its leash, it shouldn't be prevented from attacking.
3022 }
3023 }
3024
3025 if (closestMonster
3026 && (distanceBetween(x, y, player.xLoc, player.yLoc) < leashLength || (monst->bookkeepingFlags & MB_DOES_NOT_TRACK_LEADER))
3027 && !(monst->info.flags & MONST_MAINTAINS_DISTANCE)
3028 && !attackWouldBeFutile(monst, closestMonster)) {
3029
3030 // Blink toward an enemy?
3031 if (monsterHasBoltEffect(monst, BE_BLINKING)
3032 && ((monst->info.flags & MONST_ALWAYS_USE_ABILITY) || rand_percent(30))) {
3033
3034 enemyMap = allocGrid();
3035 costMap = allocGrid();
3036
3037 for (i=0; i<DCOLS; i++) {
3038 for (j=0; j<DROWS; j++) {
3039 if (cellHasTerrainFlag(i, j, T_OBSTRUCTS_PASSABILITY)) {
3040 costMap[i][j] = cellHasTerrainFlag(i, j, T_OBSTRUCTS_DIAGONAL_MOVEMENT) ? PDS_OBSTRUCTION : PDS_FORBIDDEN;
3041 enemyMap[i][j] = 0; // safeguard against OOS
3042 } else if (monsterAvoids(monst, i, j)) {
3043 costMap[i][j] = PDS_FORBIDDEN;
3044 enemyMap[i][j] = 0; // safeguard against OOS
3045 } else {
3046 costMap[i][j] = 1;
3047 enemyMap[i][j] = 10000;
3048 }
3049 }
3050 }
3051
3052 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
3053 creature *target = nextCreature(&it);
3054 if (target != monst
3055 && (!(target->bookkeepingFlags & MB_SUBMERGED) || (monst->bookkeepingFlags & MB_SUBMERGED))
3056 && monsterWillAttackTarget(monst, target)
3057 && distanceBetween(x, y, target->xLoc, target->yLoc) < shortestDistance
3058 && traversiblePathBetween(monst, target->xLoc, target->yLoc)
3059 && (!monsterAvoids(monst, target->xLoc, target->yLoc) || (target->info.flags & MONST_ATTACKABLE_THRU_WALLS))
3060 && (!target->status[STATUS_INVISIBLE] || ((monst->info.flags & MONST_ALWAYS_USE_ABILITY) || rand_percent(33)))) {
3061
3062 enemyMap[target->xLoc][target->yLoc] = 0;
3063 costMap[target->xLoc][target->yLoc] = 1;
3064 }
3065 }
3066
3067 dijkstraScan(enemyMap, costMap, true);
3068 freeGrid(costMap);
3069
3070 if (monsterBlinkToPreferenceMap(monst, enemyMap, false)) {
3071 monst->ticksUntilTurn = monst->attackSpeed * (monst->info.flags & MONST_CAST_SPELLS_SLOWLY ? 2 : 1);
3072 freeGrid(enemyMap);
3073 return;
3074 }
3075 freeGrid(enemyMap);
3076 }
3077
3078 targetLoc[0] = closestMonster->xLoc;
3079 targetLoc[1] = closestMonster->yLoc;
3080 moveMonsterPassivelyTowards(monst, targetLoc, false);
3081 } else if (monst->targetCorpseLoc[0]
3082 && !monst->status[STATUS_POISONED]
3083 && (!monst->status[STATUS_BURNING] || monst->status[STATUS_IMMUNE_TO_FIRE])) { // Going to start eating a corpse.
3084 moveMonsterPassivelyTowards(monst, monst->targetCorpseLoc, false);
3085 if (monst->xLoc == monst->targetCorpseLoc[0]
3086 && monst->yLoc == monst->targetCorpseLoc[1]
3087 && !(monst->bookkeepingFlags & MB_ABSORBING)) {
3088 if (canSeeMonster(monst)) {
3089 monsterName(monstName, monst, true);
3090 sprintf(buf, "%s begins %s the fallen %s.", monstName, monsterText[monst->info.monsterID].absorbing, monst->targetCorpseName);
3091 messageWithColor(buf, &goodMessageColor, 0);
3092 }
3093 monst->corpseAbsorptionCounter = 20;
3094 monst->bookkeepingFlags |= MB_ABSORBING;
3095 }
3096 } else if ((monst->bookkeepingFlags & MB_DOES_NOT_TRACK_LEADER)
3097 || (distanceBetween(x, y, player.xLoc, player.yLoc) < 3 && (pmap[x][y].flags & IN_FIELD_OF_VIEW))) {
3098
3099 monst->bookkeepingFlags &= ~MB_GIVEN_UP_ON_SCENT;
3100 monsterMillAbout(monst, 30);
3101 } else {
3102 if (!(monst->bookkeepingFlags & MB_GIVEN_UP_ON_SCENT)
3103 && distanceBetween(x, y, player.xLoc, player.yLoc) > 10
3104 && monsterBlinkToPreferenceMap(monst, scentMap, true)) {
3105
3106 monst->ticksUntilTurn = monst->attackSpeed * (monst->info.flags & MONST_CAST_SPELLS_SLOWLY ? 2 : 1);
3107 return;
3108 }
3109 dir = scentDirection(monst);
3110 if (dir == -1 || (monst->bookkeepingFlags & MB_GIVEN_UP_ON_SCENT)) {
3111 monst->bookkeepingFlags |= MB_GIVEN_UP_ON_SCENT;
3112 pathTowardCreature(monst, monst->leader);
3113 } else {
3114 targetLoc[0] = x + nbDirs[dir][0];
3115 targetLoc[1] = y + nbDirs[dir][1];
3116 moveMonsterPassivelyTowards(monst, targetLoc, false);
3117 }
3118 }
3119 }
3120
3121 // Returns whether to abort the turn.
updateMonsterCorpseAbsorption(creature * monst)3122 boolean updateMonsterCorpseAbsorption(creature *monst) {
3123 short i;
3124 char buf[COLS], buf2[COLS];
3125
3126 if (monst->xLoc == monst->targetCorpseLoc[0]
3127 && monst->yLoc == monst->targetCorpseLoc[1]
3128 && (monst->bookkeepingFlags & MB_ABSORBING)) {
3129
3130 if (--monst->corpseAbsorptionCounter <= 0) {
3131 monst->targetCorpseLoc[0] = monst->targetCorpseLoc[1] = 0;
3132 if (monst->absorptionBolt != BOLT_NONE) {
3133 for (i=0; monst->info.bolts[i] != BOLT_NONE; i++);
3134 monst->info.bolts[i] = monst->absorptionBolt;
3135 } else if (monst->absorbBehavior) {
3136 monst->info.flags |= monst->absorptionFlags;
3137 } else {
3138 monst->info.abilityFlags |= monst->absorptionFlags;
3139 }
3140 monst->newPowerCount--;
3141 monst->bookkeepingFlags &= ~MB_ABSORBING;
3142
3143 if (monst->info.flags & MONST_FIERY) {
3144 monst->status[STATUS_BURNING] = monst->maxStatus[STATUS_BURNING] = 1000; // won't decrease
3145 }
3146 if (monst->info.flags & MONST_FLIES) {
3147 monst->status[STATUS_LEVITATING] = monst->maxStatus[STATUS_LEVITATING] = 1000; // won't decrease
3148 monst->info.flags &= ~(MONST_RESTRICTED_TO_LIQUID | MONST_SUBMERGES);
3149 monst->bookkeepingFlags &= ~(MB_SUBMERGED);
3150 }
3151 if (monst->info.flags & MONST_IMMUNE_TO_FIRE) {
3152 monst->status[STATUS_IMMUNE_TO_FIRE] = monst->maxStatus[STATUS_IMMUNE_TO_FIRE] = 1000; // won't decrease
3153 }
3154 if (monst->info.flags & MONST_INVISIBLE) {
3155 monst->status[STATUS_INVISIBLE] = monst->maxStatus[STATUS_INVISIBLE] = 1000; // won't decrease
3156 }
3157 if (canSeeMonster(monst)) {
3158 monsterName(buf2, monst, true);
3159 sprintf(buf, "%s finished %s the %s.", buf2, monsterText[monst->info.monsterID].absorbing, monst->targetCorpseName);
3160 messageWithColor(buf, &goodMessageColor, 0);
3161 if (monst->absorptionBolt != BOLT_NONE) {
3162 sprintf(buf, "%s %s!", buf2, boltCatalog[monst->absorptionBolt].abilityDescription);
3163 } else if (monst->absorbBehavior) {
3164 sprintf(buf, "%s now %s!", buf2, monsterBehaviorFlagDescriptions[unflag(monst->absorptionFlags)]);
3165 } else {
3166 sprintf(buf, "%s now %s!", buf2, monsterAbilityFlagDescriptions[unflag(monst->absorptionFlags)]);
3167 }
3168 resolvePronounEscapes(buf, monst);
3169 messageWithColor(buf, &advancementMessageColor, 0);
3170 }
3171 monst->absorptionFlags = 0;
3172 monst->absorptionBolt = BOLT_NONE;
3173 }
3174 monst->ticksUntilTurn = 100;
3175 return true;
3176 } else if (--monst->corpseAbsorptionCounter <= 0) {
3177 monst->targetCorpseLoc[0] = monst->targetCorpseLoc[1] = 0; // lost its chance
3178 monst->bookkeepingFlags &= ~MB_ABSORBING;
3179 monst->absorptionFlags = 0;
3180 monst->absorptionBolt = BOLT_NONE;
3181 } else if (monst->bookkeepingFlags & MB_ABSORBING) {
3182 monst->bookkeepingFlags &= ~MB_ABSORBING; // absorbing but not on the corpse
3183 if (monst->corpseAbsorptionCounter <= 15) {
3184 monst->targetCorpseLoc[0] = monst->targetCorpseLoc[1] = 0; // lost its chance
3185 monst->absorptionFlags = 0;
3186 monst->absorptionBolt = BOLT_NONE;
3187 }
3188 }
3189 return false;
3190 }
3191
monstersTurn(creature * monst)3192 void monstersTurn(creature *monst) {
3193 short x, y, playerLoc[2], targetLoc[2], dir, shortestDistance;
3194 boolean alreadyAtBestScent;
3195 creature *closestMonster;
3196
3197 monst->turnsSpentStationary++;
3198
3199 if (monst->corpseAbsorptionCounter >= 0 && updateMonsterCorpseAbsorption(monst)) {
3200 return;
3201 }
3202
3203 if (monst->info.DFChance
3204 && (monst->info.flags & MONST_GETS_TURN_ON_ACTIVATION)
3205 && rand_percent(monst->info.DFChance)) {
3206
3207 spawnDungeonFeature(monst->xLoc, monst->yLoc, &dungeonFeatureCatalog[monst->info.DFType], true, false);
3208 }
3209
3210 applyInstantTileEffectsToCreature(monst); // Paralysis, confusion etc. take effect before the monster can move.
3211
3212 // if the monster is paralyzed, entranced or chained, this is where its turn ends.
3213 if (monst->status[STATUS_PARALYZED] || monst->status[STATUS_ENTRANCED] || (monst->bookkeepingFlags & MB_CAPTIVE)) {
3214 monst->ticksUntilTurn = monst->movementSpeed;
3215 if ((monst->bookkeepingFlags & MB_CAPTIVE) && monst->carriedItem) {
3216 makeMonsterDropItem(monst);
3217 }
3218 return;
3219 }
3220
3221 if (monst->bookkeepingFlags & MB_IS_DYING) {
3222 return;
3223 }
3224
3225 monst->ticksUntilTurn = monst->movementSpeed / 3; // will be later overwritten by movement or attack
3226
3227 x = monst->xLoc;
3228 y = monst->yLoc;
3229
3230 // Sleepers can awaken, but it takes a whole turn.
3231 if (monst->creatureState == MONSTER_SLEEPING) {
3232 monst->ticksUntilTurn = monst->movementSpeed;
3233 updateMonsterState(monst);
3234 return;
3235 }
3236
3237 // Update creature state if appropriate.
3238 updateMonsterState(monst);
3239
3240 if (monst->creatureState == MONSTER_SLEEPING) {
3241 monst->ticksUntilTurn = monst->movementSpeed;
3242 return;
3243 }
3244
3245 // and move the monster.
3246
3247 // immobile monsters can only use special abilities:
3248 if (monst->info.flags & MONST_IMMOBILE) {
3249 if (monstUseMagic(monst)) { // if he actually cast a spell
3250 monst->ticksUntilTurn = monst->attackSpeed * (monst->info.flags & MONST_CAST_SPELLS_SLOWLY ? 2 : 1);
3251 return;
3252 }
3253 monst->ticksUntilTurn = monst->attackSpeed;
3254 return;
3255 }
3256
3257 // discordant monsters
3258 if (monst->status[STATUS_DISCORDANT] && monst->creatureState != MONSTER_FLEEING) {
3259 shortestDistance = max(DROWS, DCOLS);
3260 closestMonster = NULL;
3261 boolean handledPlayer = false;
3262 for (creatureIterator it = iterateCreatures(monsters); !handledPlayer || hasNextCreature(it);) {
3263 creature *target = !handledPlayer ? &player : nextCreature(&it);
3264 handledPlayer = true;
3265 if (target != monst
3266 && (!(target->bookkeepingFlags & MB_SUBMERGED) || (monst->bookkeepingFlags & MB_SUBMERGED))
3267 && monsterWillAttackTarget(monst, target)
3268 && distanceBetween(x, y, target->xLoc, target->yLoc) < shortestDistance
3269 && traversiblePathBetween(monst, target->xLoc, target->yLoc)
3270 && (!monsterAvoids(monst, target->xLoc, target->yLoc) || (target->info.flags & MONST_ATTACKABLE_THRU_WALLS))
3271 && (!target->status[STATUS_INVISIBLE] || rand_percent(33))) {
3272
3273 shortestDistance = distanceBetween(x, y, target->xLoc, target->yLoc);
3274 closestMonster = target;
3275 }
3276 }
3277 if (closestMonster && monstUseMagic(monst)) {
3278 monst->ticksUntilTurn = monst->attackSpeed * (monst->info.flags & MONST_CAST_SPELLS_SLOWLY ? 2 : 1);
3279 return;
3280 }
3281 if (closestMonster && !(monst->info.flags & MONST_MAINTAINS_DISTANCE)) {
3282 targetLoc[0] = closestMonster->xLoc;
3283 targetLoc[1] = closestMonster->yLoc;
3284 if (moveMonsterPassivelyTowards(monst, targetLoc, monst->creatureState == MONSTER_ALLY)) {
3285 return;
3286 }
3287 }
3288 }
3289
3290 // hunting
3291 if ((monst->creatureState == MONSTER_TRACKING_SCENT
3292 || (monst->creatureState == MONSTER_ALLY && monst->status[STATUS_DISCORDANT]))
3293 // eels don't charge if you're not in the water
3294 && (!(monst->info.flags & MONST_RESTRICTED_TO_LIQUID) || cellHasTMFlag(player.xLoc, player.yLoc, TM_ALLOWS_SUBMERGING))) {
3295
3296 // magic users sometimes cast spells
3297 if (monstUseMagic(monst)
3298 || (monsterHasBoltEffect(monst, BE_BLINKING)
3299 && ((monst->info.flags & MONST_ALWAYS_USE_ABILITY) || rand_percent(30))
3300 && monsterBlinkToPreferenceMap(monst, scentMap, true))) { // if he actually cast a spell
3301
3302 monst->ticksUntilTurn = monst->attackSpeed * (monst->info.flags & MONST_CAST_SPELLS_SLOWLY ? 2 : 1);
3303 return;
3304 }
3305
3306 // if the monster is adjacent to an ally and not adjacent to the player, attack the ally
3307 if (distanceBetween(x, y, player.xLoc, player.yLoc) > 1
3308 || diagonalBlocked(x, y, player.xLoc, player.yLoc, false)) {
3309 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
3310 creature *ally = nextCreature(&it);
3311 if (monsterWillAttackTarget(monst, ally)
3312 && distanceBetween(x, y, ally->xLoc, ally->yLoc) == 1
3313 && (!ally->status[STATUS_INVISIBLE] || rand_percent(33))) {
3314
3315 targetLoc[0] = ally->xLoc;
3316 targetLoc[1] = ally->yLoc;
3317 if (moveMonsterPassivelyTowards(monst, targetLoc, true)) { // attack
3318 return;
3319 }
3320 }
3321 }
3322 }
3323
3324 if ((monst->status[STATUS_LEVITATING] || (monst->info.flags & MONST_RESTRICTED_TO_LIQUID) || (monst->bookkeepingFlags & MB_SUBMERGED)
3325 || ((monst->info.flags & (MONST_IMMUNE_TO_WEBS | MONST_INVULNERABLE) && monsterCanShootWebs(monst))))
3326 && pmap[x][y].flags & IN_FIELD_OF_VIEW) {
3327
3328 playerLoc[0] = player.xLoc;
3329 playerLoc[1] = player.yLoc;
3330 moveMonsterPassivelyTowards(monst, playerLoc, true); // attack
3331 return;
3332 }
3333 if ((monst->info.flags & MONST_ALWAYS_HUNTING)
3334 && (monst->bookkeepingFlags & MB_GIVEN_UP_ON_SCENT)) {
3335
3336 pathTowardCreature(monst, &player);
3337 return;
3338 }
3339
3340 dir = scentDirection(monst);
3341 if (dir == NO_DIRECTION) {
3342 alreadyAtBestScent = isLocalScentMaximum(x, y);
3343 if (alreadyAtBestScent && monst->creatureState != MONSTER_ALLY && !(pmap[x][y].flags & IN_FIELD_OF_VIEW)) {
3344 if (monst->info.flags & MONST_ALWAYS_HUNTING) {
3345 pathTowardCreature(monst, &player);
3346 monst->bookkeepingFlags |= MB_GIVEN_UP_ON_SCENT;
3347 return;
3348 }
3349 monst->creatureState = MONSTER_WANDERING;
3350 // If we're out of the player's FOV and the scent map is a dead end,
3351 // wander over to near where we last saw the player.
3352 wanderToward(monst, monst->lastSeenPlayerAt[0], monst->lastSeenPlayerAt[1]);
3353 }
3354 } else {
3355 moveMonster(monst, nbDirs[dir][0], nbDirs[dir][1]);
3356 }
3357 } else if (monst->creatureState == MONSTER_FLEEING) {
3358 // fleeing
3359 if (monsterHasBoltEffect(monst, BE_BLINKING)
3360 && ((monst->info.flags & MONST_ALWAYS_USE_ABILITY) || rand_percent(30))
3361 && monsterBlinkToSafety(monst)) {
3362
3363 return;
3364 }
3365
3366 if (monsterSummons(monst, (monst->info.flags & MONST_ALWAYS_USE_ABILITY))) {
3367 return;
3368 }
3369
3370 dir = nextStep(getSafetyMap(monst), monst->xLoc, monst->yLoc, NULL, true);
3371 if (dir != -1) {
3372 targetLoc[0] = x + nbDirs[dir][0];
3373 targetLoc[1] = y + nbDirs[dir][1];
3374 }
3375 if (dir == -1 || (!moveMonster(monst, nbDirs[dir][0], nbDirs[dir][1]) && !moveMonsterPassivelyTowards(monst, targetLoc, true))) {
3376 boolean handledPlayer = false;
3377 for (creatureIterator it = iterateCreatures(monsters); !handledPlayer || hasNextCreature(it);) {
3378 creature *ally = !handledPlayer ? &player : nextCreature(&it);
3379 handledPlayer = true;
3380 if (!monst->status[STATUS_MAGICAL_FEAR] // Fearful monsters will never attack.
3381 && monsterWillAttackTarget(monst, ally)
3382 && distanceBetween(x, y, ally->xLoc, ally->yLoc) <= 1) {
3383
3384 moveMonster(monst, ally->xLoc - x, ally->yLoc - y); // attack the player if cornered
3385 return;
3386 }
3387 }
3388 }
3389 return;
3390 } else if (monst->creatureState == MONSTER_WANDERING
3391 // eels wander if you're not in water
3392 || ((monst->info.flags & MONST_RESTRICTED_TO_LIQUID) && !cellHasTMFlag(player.xLoc, player.yLoc, TM_ALLOWS_SUBMERGING))) {
3393
3394 // if we're standing in harmful terrain and there is a way to escape it, spend this turn escaping it.
3395 if (cellHasTerrainFlag(x, y, (T_HARMFUL_TERRAIN & ~T_IS_FIRE))
3396 || (cellHasTerrainFlag(x, y, T_IS_FIRE) && !monst->status[STATUS_IMMUNE_TO_FIRE] && !(monst->info.flags & MONST_INVULNERABLE))) {
3397 if (!rogue.updatedMapToSafeTerrainThisTurn) {
3398 updateSafeTerrainMap();
3399 }
3400
3401 if (monsterBlinkToPreferenceMap(monst, rogue.mapToSafeTerrain, false)) {
3402 monst->ticksUntilTurn = monst->attackSpeed * (monst->info.flags & MONST_CAST_SPELLS_SLOWLY ? 2 : 1);
3403 return;
3404 }
3405
3406 dir = nextStep(rogue.mapToSafeTerrain, x, y, monst, true);
3407 if (dir != -1) {
3408 targetLoc[0] = x + nbDirs[dir][0];
3409 targetLoc[1] = y + nbDirs[dir][1];
3410 if (moveMonsterPassivelyTowards(monst, targetLoc, true)) {
3411 return;
3412 }
3413 }
3414 }
3415
3416 // if a captive leader is captive, regenerative and healthy enough to withstand an attack,
3417 // and we're not poisonous, then approach or attack him.
3418 if ((monst->bookkeepingFlags & MB_FOLLOWER)
3419 && (monst->leader->bookkeepingFlags & MB_CAPTIVE)
3420 && monst->leader->currentHP > (int) (monst->info.damage.upperBound * monsterDamageAdjustmentAmount(monst) / FP_FACTOR)
3421 && monst->leader->info.turnsBetweenRegen > 0
3422 && !(monst->info.abilityFlags & MA_POISONS)
3423 && !diagonalBlocked(monst->xLoc, monst->yLoc, monst->leader->xLoc, monst->leader->yLoc, false)) {
3424
3425 if (distanceBetween(monst->xLoc, monst->yLoc, monst->leader->xLoc, monst->leader->yLoc) == 1) {
3426 // Attack if adjacent.
3427 monst->ticksUntilTurn = monst->attackSpeed;
3428 attack(monst, monst->leader, false);
3429 return;
3430 } else {
3431 // Otherwise, approach.
3432 pathTowardCreature(monst, monst->leader);
3433 return;
3434 }
3435 }
3436
3437 // if the monster is adjacent to an ally and not fleeing, attack the ally
3438 if (monst->creatureState == MONSTER_WANDERING) {
3439 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
3440 creature *ally = nextCreature(&it);
3441 if (monsterWillAttackTarget(monst, ally)
3442 && distanceBetween(x, y, ally->xLoc, ally->yLoc) == 1
3443 && (!ally->status[STATUS_INVISIBLE] || rand_percent(33))) {
3444
3445 targetLoc[0] = ally->xLoc;
3446 targetLoc[1] = ally->yLoc;
3447 if (moveMonsterPassivelyTowards(monst, targetLoc, true)) {
3448 return;
3449 }
3450 }
3451 }
3452 }
3453
3454 // if you're a follower, don't get separated from the pack
3455 if (monst->bookkeepingFlags & MB_FOLLOWER) {
3456 if (distanceBetween(x, y, monst->leader->xLoc, monst->leader->yLoc) > 2) {
3457 pathTowardCreature(monst, monst->leader);
3458 } else if (monst->leader->info.flags & MONST_IMMOBILE) {
3459 monsterMillAbout(monst, 100); // Worshipers will pace frenetically.
3460 } else if (monst->leader->bookkeepingFlags & MB_CAPTIVE) {
3461 monsterMillAbout(monst, 10); // Captors are languid.
3462 } else {
3463 monsterMillAbout(monst, 30); // Other followers mill about like your allies do.
3464 }
3465 } else {
3466 // Step toward the chosen waypoint.
3467 dir = NO_DIRECTION;
3468 if (isValidWanderDestination(monst, monst->targetWaypointIndex)) {
3469 dir = nextStep(rogue.wpDistance[monst->targetWaypointIndex], monst->xLoc, monst->yLoc, monst, false);
3470 }
3471 // If there's no path forward, call that waypoint finished and pick a new one.
3472 if (!isValidWanderDestination(monst, monst->targetWaypointIndex)
3473 || dir == NO_DIRECTION) {
3474
3475 chooseNewWanderDestination(monst);
3476 if (isValidWanderDestination(monst, monst->targetWaypointIndex)) {
3477 dir = nextStep(rogue.wpDistance[monst->targetWaypointIndex], monst->xLoc, monst->yLoc, monst, false);
3478 }
3479 }
3480 // If there's still no path forward, step randomly as though flitting.
3481 // (This is how eels wander in deep water.)
3482 if (dir == NO_DIRECTION) {
3483 dir = randValidDirectionFrom(monst, x, y, true);
3484 }
3485 if (dir != NO_DIRECTION) {
3486 targetLoc[0] = x + nbDirs[dir][0];
3487 targetLoc[1] = y + nbDirs[dir][1];
3488 if (moveMonsterPassivelyTowards(monst, targetLoc, true)) {
3489 return;
3490 }
3491 }
3492 }
3493 } else if (monst->creatureState == MONSTER_ALLY) {
3494 moveAlly(monst);
3495 }
3496 }
3497
canPass(creature * mover,creature * blocker)3498 boolean canPass(creature *mover, creature *blocker) {
3499
3500 if (blocker == &player) {
3501 return false;
3502 }
3503
3504 if (blocker->status[STATUS_CONFUSED]
3505 || blocker->status[STATUS_STUCK]
3506 || blocker->status[STATUS_PARALYZED]
3507 || blocker->status[STATUS_ENTRANCED]
3508 || mover->status[STATUS_ENTRANCED]) {
3509
3510 return false;
3511 }
3512
3513 if ((blocker->bookkeepingFlags & (MB_CAPTIVE | MB_ABSORBING))
3514 || (blocker->info.flags & MONST_IMMOBILE)) {
3515 return false;
3516 }
3517
3518 if (monstersAreEnemies(mover, blocker)) {
3519 return false;
3520 }
3521
3522 if (blocker->leader == mover) {
3523 return true;
3524 }
3525
3526 if (mover->leader == blocker) {
3527 return false;
3528 }
3529
3530 return (monstersAreTeammates(mover, blocker)
3531 && blocker->currentHP < mover->currentHP);
3532 }
3533
isPassableOrSecretDoor(short x,short y)3534 boolean isPassableOrSecretDoor(short x, short y) {
3535 return (!cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY)
3536 || (cellHasTMFlag(x, y, TM_IS_SECRET) && !(discoveredTerrainFlagsAtLoc(x, y) & T_OBSTRUCTS_PASSABILITY)));
3537 }
3538
knownToPlayerAsPassableOrSecretDoor(short x,short y)3539 boolean knownToPlayerAsPassableOrSecretDoor(short x, short y) {
3540 unsigned long tFlags, TMFlags;
3541 getLocationFlags(x, y, &tFlags, &TMFlags, NULL, true);
3542 return (!(tFlags & T_OBSTRUCTS_PASSABILITY)
3543 || ((TMFlags & TM_IS_SECRET) && !(discoveredTerrainFlagsAtLoc(x, y) & T_OBSTRUCTS_PASSABILITY)));
3544 }
3545
setMonsterLocation(creature * monst,short newX,short newY)3546 void setMonsterLocation(creature *monst, short newX, short newY) {
3547 unsigned long creatureFlag = (monst == &player ? HAS_PLAYER : HAS_MONSTER);
3548 pmap[monst->xLoc][monst->yLoc].flags &= ~creatureFlag;
3549 refreshDungeonCell(monst->xLoc, monst->yLoc);
3550 monst->turnsSpentStationary = 0;
3551 monst->xLoc = newX;
3552 monst->yLoc = newY;
3553 pmap[newX][newY].flags |= creatureFlag;
3554 if ((monst->bookkeepingFlags & MB_SUBMERGED) && !cellHasTMFlag(newX, newY, TM_ALLOWS_SUBMERGING)) {
3555 monst->bookkeepingFlags &= ~MB_SUBMERGED;
3556 }
3557 if (playerCanSee(newX, newY)
3558 && cellHasTMFlag(newX, newY, TM_IS_SECRET)
3559 && cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_PASSABILITY)) {
3560
3561 discover(newX, newY); // if you see a monster use a secret door, you discover it
3562 }
3563 refreshDungeonCell(newX, newY);
3564 applyInstantTileEffectsToCreature(monst);
3565 if (monst == &player) {
3566 updateVision(true);
3567 // get any items at the destination location
3568 if (pmap[player.xLoc][player.yLoc].flags & HAS_ITEM) {
3569 pickUpItemAt(player.xLoc, player.yLoc);
3570 }
3571 }
3572 }
3573
3574 // Tries to move the given monster in the given vector; returns true if the move was legal
3575 // (including attacking player, vomiting or struggling in vain)
3576 // Be sure that dx, dy are both in the range [-1, 1] or the move will sometimes fail due to the diagonal check.
moveMonster(creature * monst,short dx,short dy)3577 boolean moveMonster(creature *monst, short dx, short dy) {
3578 short x = monst->xLoc, y = monst->yLoc;
3579 short newX, newY;
3580 short i;
3581 short confusedDirection, swarmDirection;
3582 creature *defender = NULL;
3583 creature *hitList[16] = {NULL};
3584 enum directions dir;
3585
3586 if (dx == 0 && dy == 0) {
3587 return false;
3588 }
3589
3590 newX = x + dx;
3591 newY = y + dy;
3592
3593 if (!coordinatesAreInMap(newX, newY)) {
3594 //DEBUG printf("\nProblem! Monster trying to move more than one space at a time.");
3595 return false;
3596 }
3597
3598 // vomiting
3599 if (monst->status[STATUS_NAUSEOUS] && rand_percent(25)) {
3600 vomit(monst);
3601 monst->ticksUntilTurn = monst->movementSpeed;
3602 return true;
3603 }
3604
3605 // move randomly?
3606 if (!monst->status[STATUS_ENTRANCED]) {
3607 if (monst->status[STATUS_CONFUSED]) {
3608 confusedDirection = randValidDirectionFrom(monst, x, y, false);
3609 if (confusedDirection != -1) {
3610 dx = nbDirs[confusedDirection][0];
3611 dy = nbDirs[confusedDirection][1];
3612 }
3613 } else if ((monst->info.flags & MONST_FLITS) && !(monst->bookkeepingFlags & MB_SEIZING) && rand_percent(33)) {
3614 confusedDirection = randValidDirectionFrom(monst, x, y, true);
3615 if (confusedDirection != -1) {
3616 dx = nbDirs[confusedDirection][0];
3617 dy = nbDirs[confusedDirection][1];
3618 }
3619 }
3620 }
3621
3622 newX = x + dx;
3623 newY = y + dy;
3624
3625 // Liquid-based monsters should never move or attack outside of liquid.
3626 if ((monst->info.flags & MONST_RESTRICTED_TO_LIQUID) && !cellHasTMFlag(newX, newY, TM_ALLOWS_SUBMERGING)) {
3627 return false;
3628 }
3629
3630 // Caught in spiderweb?
3631 if (monst->status[STATUS_STUCK] && !(pmap[newX][newY].flags & (HAS_PLAYER | HAS_MONSTER))
3632 && cellHasTerrainFlag(x, y, T_ENTANGLES) && !(monst->info.flags & MONST_IMMUNE_TO_WEBS)) {
3633 if (!(monst->info.flags & MONST_INVULNERABLE)
3634 && --monst->status[STATUS_STUCK]) {
3635
3636 monst->ticksUntilTurn = monst->movementSpeed;
3637 return true;
3638 } else if (tileCatalog[pmap[x][y].layers[SURFACE]].flags & T_ENTANGLES) {
3639 pmap[x][y].layers[SURFACE] = NOTHING;
3640 }
3641 }
3642
3643 if (pmap[newX][newY].flags & (HAS_MONSTER | HAS_PLAYER)) {
3644 defender = monsterAtLoc(newX, newY);
3645 } else {
3646 if (monst->bookkeepingFlags & MB_SEIZED) {
3647 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
3648 creature *defender = nextCreature(&it);
3649 if ((defender->bookkeepingFlags & MB_SEIZING)
3650 && monstersAreEnemies(monst, defender)
3651 && distanceBetween(monst->xLoc, monst->yLoc, defender->xLoc, defender->yLoc) == 1
3652 && !diagonalBlocked(monst->xLoc, monst->yLoc, defender->xLoc, defender->yLoc, false)) {
3653
3654 monst->ticksUntilTurn = monst->movementSpeed;
3655 return true;
3656 }
3657 }
3658 monst->bookkeepingFlags &= ~MB_SEIZED; // failsafe
3659 }
3660 if (monst->bookkeepingFlags & MB_SEIZING) {
3661 monst->bookkeepingFlags &= ~MB_SEIZING;
3662 }
3663 }
3664
3665
3666 for (dir = 0; dir < DIRECTION_COUNT; dir++) {
3667 if (dx == nbDirs[dir][0]
3668 && dy == nbDirs[dir][1]) {
3669
3670 break;
3671 }
3672 }
3673 brogueAssert(dir != NO_DIRECTION);
3674 if (handleWhipAttacks(monst, dir, NULL)
3675 || handleSpearAttacks(monst, dir, NULL)) {
3676
3677 monst->ticksUntilTurn = monst->attackSpeed;
3678 return true;
3679 }
3680
3681 if (((defender && (defender->info.flags & MONST_ATTACKABLE_THRU_WALLS))
3682 || (isPassableOrSecretDoor(newX, newY)
3683 && !diagonalBlocked(x, y, newX, newY, false)
3684 && isPassableOrSecretDoor(x, y)))
3685 && (!defender || canPass(monst, defender) || monsterWillAttackTarget(monst, defender))) {
3686 // if it's a legal move
3687
3688 if (defender) {
3689 if (canPass(monst, defender)) {
3690
3691 // swap places
3692 pmap[defender->xLoc][defender->yLoc].flags &= ~HAS_MONSTER;
3693 refreshDungeonCell(defender->xLoc, defender->yLoc);
3694
3695 pmap[monst->xLoc][monst->yLoc].flags &= ~HAS_MONSTER;
3696 refreshDungeonCell(monst->xLoc, monst->yLoc);
3697
3698 monst->xLoc = newX;
3699 monst->yLoc = newY;
3700 pmap[monst->xLoc][monst->yLoc].flags |= HAS_MONSTER;
3701
3702 if (monsterAvoids(defender, x, y)) { // don't want a flying monster to swap a non-flying monster into lava!
3703 getQualifyingPathLocNear(&(defender->xLoc), &(defender->yLoc), x, y, true,
3704 forbiddenFlagsForMonster(&(defender->info)), HAS_PLAYER,
3705 forbiddenFlagsForMonster(&(defender->info)), (HAS_PLAYER | HAS_MONSTER | HAS_STAIRS), false);
3706 } else {
3707 defender->xLoc = x;
3708 defender->yLoc = y;
3709 }
3710 pmap[defender->xLoc][defender->yLoc].flags |= HAS_MONSTER;
3711
3712 refreshDungeonCell(monst->xLoc, monst->yLoc);
3713 refreshDungeonCell(defender->xLoc, defender->yLoc);
3714
3715 monst->ticksUntilTurn = monst->movementSpeed;
3716 return true;
3717 }
3718
3719 // Sights are set on an enemy monster. Would we rather swarm than attack?
3720 swarmDirection = monsterSwarmDirection(monst, defender);
3721 if (swarmDirection != NO_DIRECTION) {
3722 newX = monst->xLoc + nbDirs[swarmDirection][0];
3723 newY = monst->yLoc + nbDirs[swarmDirection][1];
3724 setMonsterLocation(monst, newX, newY);
3725 monst->ticksUntilTurn = monst->movementSpeed;
3726 return true;
3727 } else {
3728 // attacking another monster!
3729 monst->ticksUntilTurn = monst->attackSpeed;
3730 if (!((monst->info.abilityFlags & MA_SEIZES) && !(monst->bookkeepingFlags & MB_SEIZING))) {
3731 // Bog monsters and krakens won't surface on the turn that they seize their target.
3732 monst->bookkeepingFlags &= ~MB_SUBMERGED;
3733 }
3734 refreshDungeonCell(x, y);
3735
3736 buildHitList(hitList, monst, defender,
3737 (monst->info.abilityFlags & MA_ATTACKS_ALL_ADJACENT) ? true : false);
3738 // Attack!
3739 for (i=0; i<16; i++) {
3740 if (hitList[i]
3741 && monsterWillAttackTarget(monst, hitList[i])
3742 && !(hitList[i]->bookkeepingFlags & MB_IS_DYING)
3743 && !rogue.gameHasEnded) {
3744
3745 attack(monst, hitList[i], false);
3746 }
3747 }
3748 }
3749 return true;
3750 } else {
3751 // okay we're moving!
3752 setMonsterLocation(monst, newX, newY);
3753 monst->ticksUntilTurn = monst->movementSpeed;
3754 return true;
3755 }
3756 }
3757 return false;
3758 }
3759
clearStatus(creature * monst)3760 void clearStatus(creature *monst) {
3761 short i;
3762
3763 for (i=0; i<NUMBER_OF_STATUS_EFFECTS; i++) {
3764 monst->status[i] = monst->maxStatus[i] = 0;
3765 }
3766 }
3767
3768 // Bumps a creature to a random nearby hospitable cell.
findAlternativeHomeFor(creature * monst,short * x,short * y,boolean chooseRandomly)3769 void findAlternativeHomeFor(creature *monst, short *x, short *y, boolean chooseRandomly) {
3770 short sCols[DCOLS], sRows[DROWS], i, j, maxPermissibleDifference, dist;
3771
3772 fillSequentialList(sCols, DCOLS);
3773 fillSequentialList(sRows, DROWS);
3774 if (chooseRandomly) {
3775 shuffleList(sCols, DCOLS);
3776 shuffleList(sRows, DROWS);
3777 }
3778
3779 for (maxPermissibleDifference = 1; maxPermissibleDifference < max(DCOLS, DROWS); maxPermissibleDifference++) {
3780 for (i=0; i < DCOLS; i++) {
3781 for (j=0; j<DROWS; j++) {
3782 dist = abs(sCols[i] - monst->xLoc) + abs(sRows[j] - monst->yLoc);
3783 if (dist <= maxPermissibleDifference
3784 && dist > 0
3785 && !(pmap[sCols[i]][sRows[j]].flags & (HAS_PLAYER | HAS_MONSTER))
3786 && !monsterAvoids(monst, sCols[i], sRows[j])
3787 && !(monst == &player && cellHasTerrainFlag(sCols[i], sRows[j], T_PATHING_BLOCKER))) {
3788
3789 // Success!
3790 *x = sCols[i];
3791 *y = sRows[j];
3792 return;
3793 }
3794 }
3795 }
3796 }
3797 // Failure!
3798 *x = *y = -1;
3799 }
3800
3801 // blockingMap is optional
getQualifyingLocNear(short loc[2],short x,short y,boolean hallwaysAllowed,char blockingMap[DCOLS][DROWS],unsigned long forbiddenTerrainFlags,unsigned long forbiddenMapFlags,boolean forbidLiquid,boolean deterministic)3802 boolean getQualifyingLocNear(short loc[2],
3803 short x, short y,
3804 boolean hallwaysAllowed,
3805 char blockingMap[DCOLS][DROWS],
3806 unsigned long forbiddenTerrainFlags,
3807 unsigned long forbiddenMapFlags,
3808 boolean forbidLiquid,
3809 boolean deterministic) {
3810 short i, j, k, candidateLocs, randIndex;
3811
3812 candidateLocs = 0;
3813
3814 // count up the number of candidate locations
3815 for (k=0; k<max(DROWS, DCOLS) && !candidateLocs; k++) {
3816 for (i = x-k; i <= x+k; i++) {
3817 for (j = y-k; j <= y+k; j++) {
3818 if (coordinatesAreInMap(i, j)
3819 && (i == x-k || i == x+k || j == y-k || j == y+k)
3820 && (!blockingMap || !blockingMap[i][j])
3821 && !cellHasTerrainFlag(i, j, forbiddenTerrainFlags)
3822 && !(pmap[i][j].flags & forbiddenMapFlags)
3823 && (!forbidLiquid || pmap[i][j].layers[LIQUID] == NOTHING)
3824 && (hallwaysAllowed || passableArcCount(i, j) < 2)) {
3825 candidateLocs++;
3826 }
3827 }
3828 }
3829 }
3830
3831 if (candidateLocs == 0) {
3832 return false;
3833 }
3834
3835 // and pick one
3836 if (deterministic) {
3837 randIndex = 1 + candidateLocs / 2;
3838 } else {
3839 randIndex = rand_range(1, candidateLocs);
3840 }
3841
3842 for (k=0; k<max(DROWS, DCOLS); k++) {
3843 for (i = x-k; i <= x+k; i++) {
3844 for (j = y-k; j <= y+k; j++) {
3845 if (coordinatesAreInMap(i, j)
3846 && (i == x-k || i == x+k || j == y-k || j == y+k)
3847 && (!blockingMap || !blockingMap[i][j])
3848 && !cellHasTerrainFlag(i, j, forbiddenTerrainFlags)
3849 && !(pmap[i][j].flags & forbiddenMapFlags)
3850 && (!forbidLiquid || pmap[i][j].layers[LIQUID] == NOTHING)
3851 && (hallwaysAllowed || passableArcCount(i, j) < 2)) {
3852 if (--randIndex == 0) {
3853 loc[0] = i;
3854 loc[1] = j;
3855 return true;
3856 }
3857 }
3858 }
3859 }
3860 }
3861
3862 brogueAssert(false);
3863 return false; // should never reach this point
3864 }
3865
getQualifyingGridLocNear(short loc[2],short x,short y,boolean grid[DCOLS][DROWS],boolean deterministic)3866 boolean getQualifyingGridLocNear(short loc[2],
3867 short x, short y,
3868 boolean grid[DCOLS][DROWS],
3869 boolean deterministic) {
3870 short i, j, k, candidateLocs, randIndex;
3871
3872 candidateLocs = 0;
3873
3874 // count up the number of candidate locations
3875 for (k=0; k<max(DROWS, DCOLS) && !candidateLocs; k++) {
3876 for (i = x-k; i <= x+k; i++) {
3877 for (j = y-k; j <= y+k; j++) {
3878 if (coordinatesAreInMap(i, j)
3879 && (i == x-k || i == x+k || j == y-k || j == y+k)
3880 && grid[i][j]) {
3881
3882 candidateLocs++;
3883 }
3884 }
3885 }
3886 }
3887
3888 if (candidateLocs == 0) {
3889 return false;
3890 }
3891
3892 // and pick one
3893 if (deterministic) {
3894 randIndex = 1 + candidateLocs / 2;
3895 } else {
3896 randIndex = rand_range(1, candidateLocs);
3897 }
3898
3899 for (k=0; k<max(DROWS, DCOLS); k++) {
3900 for (i = x-k; i <= x+k; i++) {
3901 for (j = y-k; j <= y+k; j++) {
3902 if (coordinatesAreInMap(i, j)
3903 && (i == x-k || i == x+k || j == y-k || j == y+k)
3904 && grid[i][j]) {
3905
3906 if (--randIndex == 0) {
3907 loc[0] = i;
3908 loc[1] = j;
3909 return true;
3910 }
3911 }
3912 }
3913 }
3914 }
3915
3916 brogueAssert(false);
3917 return false; // should never reach this point
3918 }
3919
makeMonsterDropItem(creature * monst)3920 void makeMonsterDropItem(creature *monst) {
3921 short x, y;
3922 getQualifyingPathLocNear(&x, &y, monst->xLoc, monst->yLoc, true,
3923 (T_DIVIDES_LEVEL), 0,
3924 T_OBSTRUCTS_ITEMS, (HAS_PLAYER | HAS_STAIRS | HAS_ITEM), false);
3925 placeItem(monst->carriedItem, x, y);
3926 monst->carriedItem = NULL;
3927 refreshDungeonCell(x, y);
3928 }
3929
checkForContinuedLeadership(creature * monst)3930 void checkForContinuedLeadership(creature *monst) {
3931 boolean maintainLeadership = false;
3932
3933 if (monst->bookkeepingFlags & MB_LEADER) {
3934 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
3935 creature *follower = nextCreature(&it);
3936 if (follower->leader == monst && monst != follower) {
3937 maintainLeadership = true;
3938 break;
3939 }
3940 }
3941 }
3942 if (!maintainLeadership) {
3943 monst->bookkeepingFlags &= ~MB_LEADER;
3944 }
3945 }
3946
demoteMonsterFromLeadership(creature * monst)3947 void demoteMonsterFromLeadership(creature *monst) {
3948 creature *newLeader = NULL;
3949 boolean atLeastOneNewFollower = false;
3950
3951 monst->bookkeepingFlags &= ~MB_LEADER;
3952 if (monst->mapToMe) {
3953 freeGrid(monst->mapToMe);
3954 monst->mapToMe = NULL;
3955 }
3956
3957 for (int level = 0; level <= DEEPEST_LEVEL; level++) {
3958 // we'll work on this level's monsters first, so that the new leader is preferably on the same level
3959 creatureList *nearbyList = (level == 0 ? monsters : &levels[level-1].monsters);
3960 for (creatureIterator it = iterateCreatures(nearbyList); hasNextCreature(it);) {
3961 creature *follower = nextCreature(&it);
3962 if (follower == monst || follower->leader != monst) continue;
3963 if (follower->bookkeepingFlags & MB_BOUND_TO_LEADER) {
3964 // gonna die in playerTurnEnded().
3965 follower->leader = NULL;
3966 follower->bookkeepingFlags &= ~MB_FOLLOWER;
3967 } else if (newLeader) {
3968 follower->leader = newLeader;
3969 atLeastOneNewFollower = true;
3970 follower->targetWaypointIndex = monst->targetWaypointIndex;
3971 if (follower->targetWaypointIndex >= 0) {
3972 follower->waypointAlreadyVisited[follower->targetWaypointIndex] = false;
3973 }
3974 } else {
3975 newLeader = follower;
3976 follower->bookkeepingFlags |= MB_LEADER;
3977 follower->bookkeepingFlags &= ~MB_FOLLOWER;
3978 follower->leader = NULL;
3979 }
3980 }
3981 }
3982
3983 if (newLeader
3984 && !atLeastOneNewFollower) {
3985 newLeader->bookkeepingFlags &= ~MB_LEADER;
3986 }
3987
3988 for (int level = 0; level <= DEEPEST_LEVEL; level++) {
3989 creatureList *candidateList = (level == 0 ? dormantMonsters : &levels[level-1].dormantMonsters);
3990 for (creatureIterator it = iterateCreatures(candidateList); hasNextCreature(it);) {
3991 creature *follower = nextCreature(&it);
3992 if (follower == monst || follower->leader != monst) continue;
3993 follower->leader = NULL;
3994 follower->bookkeepingFlags &= ~MB_FOLLOWER;
3995 }
3996 }
3997 }
3998
3999 // Makes a monster dormant, or awakens it from that state
toggleMonsterDormancy(creature * monst)4000 void toggleMonsterDormancy(creature *monst) {
4001 //short loc[2] = {0, 0};
4002
4003 if (removeCreature(dormantMonsters, monst)) {
4004 // Found it! It's dormant. Wake it up.
4005 // It's been removed from the dormant list.
4006
4007 // Add it to the normal list.
4008 prependCreature(monsters, monst);
4009
4010 pmap[monst->xLoc][monst->yLoc].flags &= ~HAS_DORMANT_MONSTER;
4011
4012 // Does it need a new location?
4013 if (pmap[monst->xLoc][monst->yLoc].flags & (HAS_MONSTER | HAS_PLAYER)) { // Occupied!
4014 getQualifyingPathLocNear(
4015 &(monst->xLoc),
4016 &(monst->yLoc),
4017 monst->xLoc,
4018 monst->yLoc,
4019 true,
4020 T_DIVIDES_LEVEL & avoidedFlagsForMonster(&(monst->info)),
4021 HAS_PLAYER,
4022 avoidedFlagsForMonster(&(monst->info)),
4023 (HAS_PLAYER | HAS_MONSTER | HAS_STAIRS),
4024 false
4025 );
4026 // getQualifyingLocNear(loc, monst->xLoc, monst->yLoc, true, 0, T_PATHING_BLOCKER, (HAS_PLAYER | HAS_MONSTER), false, false);
4027 // monst->xLoc = loc[0];
4028 // monst->yLoc = loc[1];
4029 }
4030
4031 if (monst->bookkeepingFlags & MB_MARKED_FOR_SACRIFICE) {
4032 monst->bookkeepingFlags |= MB_TELEPATHICALLY_REVEALED;
4033 if (monst->carriedItem) {
4034 makeMonsterDropItem(monst);
4035 }
4036 }
4037
4038 // Miscellaneous transitional tasks.
4039 // Don't want it to move before the player has a chance to react.
4040 monst->ticksUntilTurn = 200;
4041
4042 pmap[monst->xLoc][monst->yLoc].flags |= HAS_MONSTER;
4043 monst->bookkeepingFlags &= ~MB_IS_DORMANT;
4044 fadeInMonster(monst);
4045 return;
4046 }
4047
4048 if (removeCreature(monsters, monst)) {
4049 // Found it! It's alive. Put it into dormancy.
4050 // Add it to the dormant chain.
4051 prependCreature(dormantMonsters, monst);
4052 // Miscellaneous transitional tasks.
4053 pmap[monst->xLoc][monst->yLoc].flags &= ~HAS_MONSTER;
4054 pmap[monst->xLoc][monst->yLoc].flags |= HAS_DORMANT_MONSTER;
4055 monst->bookkeepingFlags |= MB_IS_DORMANT;
4056 return;
4057 }
4058 }
4059
staffOrWandEffectOnMonsterDescription(char * newText,item * theItem,creature * monst)4060 boolean staffOrWandEffectOnMonsterDescription(char *newText, item *theItem, creature *monst) {
4061 char theItemName[COLS], monstName[COLS];
4062 boolean successfulDescription = false;
4063 fixpt enchant = netEnchant(theItem);
4064
4065 if ((theItem->category & (STAFF | WAND))
4066 && tableForItemCategory(theItem->category, NULL)[theItem->kind].identified) {
4067
4068 monsterName(monstName, monst, true);
4069 itemName(theItem, theItemName, false, false, NULL);
4070
4071 switch (boltEffectForItem(theItem)) {
4072 case BE_DAMAGE:
4073 if ((boltCatalog[boltForItem(theItem)].flags & BF_FIERY) && (monst->status[STATUS_IMMUNE_TO_FIRE])
4074 || (monst->info.flags & MONST_INVULNERABLE)) {
4075
4076 sprintf(newText, "\n Your %s (%c) will not harm %s.",
4077 theItemName,
4078 theItem->inventoryLetter,
4079 monstName);
4080 successfulDescription = true;
4081 } else if (theItem->flags & (ITEM_MAX_CHARGES_KNOWN | ITEM_IDENTIFIED)) {
4082 if (staffDamageLow(enchant) >= monst->currentHP) {
4083 sprintf(newText, "\n Your %s (%c) will %s the %s in one hit.",
4084 theItemName,
4085 theItem->inventoryLetter,
4086 (monst->info.flags & MONST_INANIMATE) ? "destroy" : "kill",
4087 monstName);
4088 } else {
4089 sprintf(newText, "\n Your %s (%c) will hit %s for between %i%% and %i%% of $HISHER current health.",
4090 theItemName,
4091 theItem->inventoryLetter,
4092 monstName,
4093 100 * staffDamageLow(enchant) / monst->currentHP,
4094 100 * staffDamageHigh(enchant) / monst->currentHP);
4095 }
4096 successfulDescription = true;
4097 }
4098 break;
4099 case BE_POISON:
4100 if (monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE)) {
4101 sprintf(newText, "\n Your %s (%c) will not affect %s.",
4102 theItemName,
4103 theItem->inventoryLetter,
4104 monstName);
4105 } else {
4106 sprintf(newText, "\n Your %s (%c) will poison %s for %i%% of $HISHER current health.",
4107 theItemName,
4108 theItem->inventoryLetter,
4109 monstName,
4110 100 * staffPoison(enchant) / monst->currentHP);
4111 }
4112 successfulDescription = true;
4113 break;
4114 case BE_DOMINATION:
4115 if (monst->creatureState != MONSTER_ALLY) {
4116 if (monst->info.flags & MONST_INANIMATE) {
4117 sprintf(newText, "\n A wand of domination will have no effect on objects like %s.",
4118 monstName);
4119 } else if (monst->info.flags & MONST_INVULNERABLE) {
4120 sprintf(newText, "\n A wand of domination will not affect %s.",
4121 monstName);
4122 } else if (wandDominate(monst) <= 0) {
4123 sprintf(newText, "\n A wand of domination will fail at %s's current health level.",
4124 monstName);
4125 } else if (wandDominate(monst) >= 100) {
4126 sprintf(newText, "\n A wand of domination will always succeed at %s's current health level.",
4127 monstName);
4128 } else {
4129 sprintf(newText, "\n A wand of domination will have a %i%% chance of success at %s's current health level.",
4130 wandDominate(monst),
4131 monstName);
4132 }
4133 successfulDescription = true;
4134 }
4135 break;
4136 default:
4137 strcpy(newText, "");
4138 break;
4139 }
4140 }
4141 return successfulDescription;
4142 }
4143
monsterDetails(char buf[],creature * monst)4144 void monsterDetails(char buf[], creature *monst) {
4145 char monstName[COLS], capMonstName[COLS], theItemName[COLS * 3], newText[20*COLS];
4146 short i, j, combatMath, combatMath2, playerKnownAverageDamage, playerKnownMaxDamage, commaCount, realArmorValue;
4147 boolean anyFlags, alreadyDisplayedDominationText = false;
4148 item *theItem;
4149
4150 buf[0] = '\0';
4151 commaCount = 0;
4152
4153 monsterName(monstName, monst, true);
4154 strcpy(capMonstName, monstName);
4155 upperCase(capMonstName);
4156
4157 if (!(monst->info.flags & MONST_RESTRICTED_TO_LIQUID)
4158 || cellHasTMFlag(monst->xLoc, monst->yLoc, TM_ALLOWS_SUBMERGING)) {
4159 // If the monster is not a beached whale, print the ordinary flavor text.
4160 sprintf(newText, " %s\n ", monsterText[monst->info.monsterID].flavorText);
4161 strcat(buf, newText);
4162 }
4163
4164 if (monst->mutationIndex >= 0) {
4165 i = strlen(buf);
4166 i = encodeMessageColor(buf, i, mutationCatalog[monst->mutationIndex].textColor);
4167 strcpy(newText, mutationCatalog[monst->mutationIndex].description);
4168 resolvePronounEscapes(newText, monst);
4169 upperCase(newText);
4170 strcat(newText, "\n ");
4171 strcat(buf, newText);
4172 i = strlen(buf);
4173 i = encodeMessageColor(buf, i, &white);
4174 }
4175
4176 if (!(monst->info.flags & MONST_ATTACKABLE_THRU_WALLS)
4177 && cellHasTerrainFlag(monst->xLoc, monst->yLoc, T_OBSTRUCTS_PASSABILITY)) {
4178 // If the monster is trapped in impassible terrain, explain as much.
4179 sprintf(newText, "%s is trapped %s %s.\n ",
4180 capMonstName,
4181 (tileCatalog[pmap[monst->xLoc][monst->yLoc].layers[layerWithFlag(monst->xLoc, monst->yLoc, T_OBSTRUCTS_PASSABILITY)]].mechFlags & TM_STAND_IN_TILE) ? "in" : "on",
4182 tileCatalog[pmap[monst->xLoc][monst->yLoc].layers[layerWithFlag(monst->xLoc, monst->yLoc, T_OBSTRUCTS_PASSABILITY)]].description);
4183 strcat(buf, newText);
4184 }
4185
4186 // Allegiance and ability slots
4187 newText[0] = '\0';
4188 if (monst->creatureState == MONSTER_ALLY) {
4189 i = strlen(buf);
4190 i = encodeMessageColor(buf, i, &goodMessageColor);
4191
4192 sprintf(newText, "%s is your ally.\n ", capMonstName);
4193 strcat(buf, newText);
4194 if (monst->newPowerCount > 0) {
4195 i = strlen(buf);
4196 i = encodeMessageColor(buf, i, &advancementMessageColor);
4197
4198 if (monst->newPowerCount == 1) {
4199 sprintf(newText, "$HESHE seems ready to learn something new.\n ");
4200 } else {
4201 sprintf(newText, "$HESHE seems ready to learn %i new talents.\n ", monst->newPowerCount);
4202 }
4203 resolvePronounEscapes(newText, monst); // So that it gets capitalized appropriately.
4204 upperCase(newText);
4205 strcat(buf, newText);
4206 }
4207 }
4208
4209 if (!rogue.armor || (rogue.armor->flags & ITEM_IDENTIFIED)) {
4210 combatMath2 = hitProbability(monst, &player);
4211 } else {
4212 realArmorValue = player.info.defense;
4213 player.info.defense = (armorTable[rogue.armor->kind].range.upperBound + armorTable[rogue.armor->kind].range.lowerBound) / 2;
4214 player.info.defense += 10 * strengthModifier(rogue.armor) / FP_FACTOR;
4215 combatMath2 = hitProbability(monst, &player);
4216 player.info.defense = realArmorValue;
4217 }
4218
4219 // Combat info for the monster attacking the player
4220 if ((monst->info.flags & MONST_RESTRICTED_TO_LIQUID) && !cellHasTMFlag(monst->xLoc, monst->yLoc, TM_ALLOWS_SUBMERGING)) {
4221 sprintf(newText, " %s writhes helplessly on dry land.\n ", capMonstName);
4222 } else if (rogue.armor
4223 && (rogue.armor->flags & ITEM_RUNIC)
4224 && (rogue.armor->flags & ITEM_RUNIC_IDENTIFIED)
4225 && rogue.armor->enchant2 == A_IMMUNITY
4226 && monsterIsInClass(monst, rogue.armor->vorpalEnemy)) {
4227
4228 itemName(rogue.armor, theItemName, false, false, NULL);
4229 sprintf(newText, "Your %s renders you immune to %s.\n ", theItemName, monstName);
4230 } else if (monst->info.damage.upperBound * monsterDamageAdjustmentAmount(monst) / FP_FACTOR == 0) {
4231 sprintf(newText, "%s deals no direct damage.\n ", capMonstName);
4232 } else {
4233 i = strlen(buf);
4234 i = encodeMessageColor(buf, i, &badMessageColor);
4235 if (monst->info.abilityFlags & MA_POISONS) {
4236 combatMath = player.status[STATUS_POISONED]; // combatMath is poison duration
4237 for (i = 0; combatMath * (player.poisonAmount + i) < player.currentHP; i++) {
4238 combatMath += monst->info.damage.upperBound * monsterDamageAdjustmentAmount(monst) / FP_FACTOR;
4239 }
4240 if (i == 0) {
4241 // Already fatally poisoned.
4242 sprintf(newText, "%s has a %i%% chance to poison you and typically poisons for %i turns.\n ",
4243 capMonstName,
4244 combatMath2,
4245 (int) ((monst->info.damage.lowerBound + monst->info.damage.upperBound) * monsterDamageAdjustmentAmount(monst) / 2 / FP_FACTOR));
4246 } else {
4247 sprintf(newText, "%s has a %i%% chance to poison you, typically poisons for %i turns, and at worst, could fatally poison you in %i hit%s.\n ",
4248 capMonstName,
4249 combatMath2,
4250 (int) ((monst->info.damage.lowerBound + monst->info.damage.upperBound) * monsterDamageAdjustmentAmount(monst) / 2 / FP_FACTOR),
4251 i,
4252 (i > 1 ? "s" : ""));
4253 }
4254 } else {
4255 combatMath = ((player.currentHP + (monst->info.damage.upperBound * monsterDamageAdjustmentAmount(monst) / FP_FACTOR) - 1) * FP_FACTOR)
4256 / (monst->info.damage.upperBound * monsterDamageAdjustmentAmount(monst));
4257 if (combatMath < 1) {
4258 combatMath = 1;
4259 }
4260 sprintf(newText, "%s has a %i%% chance to hit you, typically hits for %i%% of your current health, and at worst, could defeat you in %i hit%s.\n ",
4261 capMonstName,
4262 combatMath2,
4263 (int) (100 * (monst->info.damage.lowerBound + monst->info.damage.upperBound) * monsterDamageAdjustmentAmount(monst) / 2 / player.currentHP / FP_FACTOR),
4264 combatMath,
4265 (combatMath > 1 ? "s" : ""));
4266 }
4267 }
4268 upperCase(newText);
4269 strcat(buf, newText);
4270
4271 if (!rogue.weapon || (rogue.weapon->flags & ITEM_IDENTIFIED)) {
4272 playerKnownAverageDamage = (player.info.damage.upperBound + player.info.damage.lowerBound) / 2;
4273 playerKnownMaxDamage = player.info.damage.upperBound;
4274 } else {
4275 fixpt strengthFactor = damageFraction(strengthModifier(rogue.weapon));
4276 short tempLow = rogue.weapon->damage.lowerBound * strengthFactor / FP_FACTOR;
4277 short tempHigh = rogue.weapon->damage.upperBound * strengthFactor / FP_FACTOR;
4278
4279 playerKnownAverageDamage = max(1, (tempLow + tempHigh) / 2);
4280 playerKnownMaxDamage = max(1, tempHigh);
4281 }
4282
4283 // Combat info for the player attacking the monster (or whether it's captive)
4284 if (playerKnownMaxDamage == 0) {
4285 i = strlen(buf);
4286 i = encodeMessageColor(buf, i, &white);
4287
4288 sprintf(newText, "You deal no direct damage.");
4289 } else if (rogue.weapon
4290 && (rogue.weapon->flags & ITEM_RUNIC)
4291 && (rogue.weapon->flags & ITEM_RUNIC_IDENTIFIED)
4292 && rogue.weapon->enchant2 == W_SLAYING
4293 && monsterIsInClass(monst, rogue.weapon->vorpalEnemy)) {
4294
4295 i = strlen(buf);
4296 i = encodeMessageColor(buf, i, &goodMessageColor);
4297 itemName(rogue.weapon, theItemName, false, false, NULL);
4298 sprintf(newText, "Your %s will slay %s in one stroke.", theItemName, monstName);
4299 } else if (monst->info.flags & (MONST_INVULNERABLE | MONST_IMMUNE_TO_WEAPONS)) {
4300 i = strlen(buf);
4301 i = encodeMessageColor(buf, i, &white);
4302 sprintf(newText, "%s is immune to your attacks.", monstName);
4303 } else if (monst->bookkeepingFlags & MB_CAPTIVE) {
4304 i = strlen(buf);
4305 i = encodeMessageColor(buf, i, &goodMessageColor);
4306
4307 sprintf(newText, "%s is being held captive.", capMonstName);
4308 } else {
4309 i = strlen(buf);
4310 i = encodeMessageColor(buf, i, &goodMessageColor);
4311
4312 combatMath = (monst->currentHP + playerKnownMaxDamage - 1) / playerKnownMaxDamage;
4313 if (combatMath < 1) {
4314 combatMath = 1;
4315 }
4316 if (rogue.weapon && !(rogue.weapon->flags & ITEM_IDENTIFIED)) {
4317 realArmorValue = rogue.weapon->enchant1;
4318 rogue.weapon->enchant1 = 0;
4319 combatMath2 = hitProbability(&player, monst);
4320 rogue.weapon->enchant1 = realArmorValue;
4321 } else {
4322 combatMath2 = hitProbability(&player, monst);
4323 }
4324 sprintf(newText, "You have a %i%% chance to hit %s, typically hit for %i%% of $HISHER current health, and at best, could defeat $HIMHER in %i hit%s.",
4325 combatMath2,
4326 monstName,
4327 100 * playerKnownAverageDamage / monst->currentHP,
4328 combatMath,
4329 (combatMath > 1 ? "s" : ""));
4330 }
4331 upperCase(newText);
4332 strcat(buf, newText);
4333
4334 for (theItem = packItems->nextItem; theItem != NULL; theItem = theItem->nextItem) {
4335 if (staffOrWandEffectOnMonsterDescription(newText, theItem, monst)) {
4336 if (boltEffectForItem(theItem) == BE_DOMINATION) {
4337 if (alreadyDisplayedDominationText) {
4338 continue;
4339 } else {
4340 alreadyDisplayedDominationText = true;
4341 }
4342 }
4343 i = strlen(buf);
4344 i = encodeMessageColor(buf, i, &itemMessageColor);
4345 strcat(buf, newText);
4346 }
4347 }
4348
4349 if (monst->carriedItem) {
4350 i = strlen(buf);
4351 i = encodeMessageColor(buf, i, &itemMessageColor);
4352 itemName(monst->carriedItem, theItemName, true, true, NULL);
4353 sprintf(newText, "%s has %s.", capMonstName, theItemName);
4354 upperCase(newText);
4355 strcat(buf, "\n ");
4356 strcat(buf, newText);
4357 }
4358
4359 if (monst->wasNegated && monst->newPowerCount == monst->totalPowerCount) {
4360 i = strlen(buf);
4361 i = encodeMessageColor(buf, i, &pink);
4362 sprintf(newText, "%s is stripped of $HISHER special traits.", capMonstName);
4363 resolvePronounEscapes(newText, monst);
4364 upperCase(newText);
4365 strcat(buf, "\n ");
4366 strcat(buf, newText);
4367 }
4368
4369 strcat(buf, "\n ");
4370
4371 i = strlen(buf);
4372 i = encodeMessageColor(buf, i, &white);
4373
4374 anyFlags = false;
4375 sprintf(newText, "%s ", capMonstName);
4376
4377 if (monst->attackSpeed < 100) {
4378 strcat(newText, "attacks quickly");
4379 anyFlags = true;
4380 } else if (monst->attackSpeed > 100) {
4381 strcat(newText, "attacks slowly");
4382 anyFlags = true;
4383 }
4384
4385 if (monst->movementSpeed < 100) {
4386 if (anyFlags) {
4387 strcat(newText, "& ");
4388 commaCount++;
4389 }
4390 strcat(newText, "moves quickly");
4391 anyFlags = true;
4392 } else if (monst->movementSpeed > 100) {
4393 if (anyFlags) {
4394 strcat(newText, "& ");
4395 commaCount++;
4396 }
4397 strcat(newText, "moves slowly");
4398 anyFlags = true;
4399 }
4400
4401 if (monst->info.turnsBetweenRegen == 0) {
4402 if (anyFlags) {
4403 strcat(newText, "& ");
4404 commaCount++;
4405 }
4406 strcat(newText, "does not regenerate");
4407 anyFlags = true;
4408 } else if (monst->info.turnsBetweenRegen < 5000) {
4409 if (anyFlags) {
4410 strcat(newText, "& ");
4411 commaCount++;
4412 }
4413 strcat(newText, "regenerates quickly");
4414 anyFlags = true;
4415 }
4416
4417 // bolt flags
4418 for (i = 0; monst->info.bolts[i] != BOLT_NONE; i++) {
4419 if (boltCatalog[monst->info.bolts[i]].abilityDescription[0]) {
4420 if (anyFlags) {
4421 strcat(newText, "& ");
4422 commaCount++;
4423 }
4424 strcat(newText, boltCatalog[monst->info.bolts[i]].abilityDescription);
4425 anyFlags = true;
4426 }
4427 }
4428
4429 // ability flags
4430 for (i=0; i<32; i++) {
4431 if ((monst->info.abilityFlags & (Fl(i)))
4432 && monsterAbilityFlagDescriptions[i][0]) {
4433 if (anyFlags) {
4434 strcat(newText, "& ");
4435 commaCount++;
4436 }
4437 strcat(newText, monsterAbilityFlagDescriptions[i]);
4438 anyFlags = true;
4439 }
4440 }
4441
4442 // behavior flags
4443 for (i=0; i<32; i++) {
4444 if ((monst->info.flags & (Fl(i)))
4445 && monsterBehaviorFlagDescriptions[i][0]) {
4446 if (anyFlags) {
4447 strcat(newText, "& ");
4448 commaCount++;
4449 }
4450 strcat(newText, monsterBehaviorFlagDescriptions[i]);
4451 anyFlags = true;
4452 }
4453 }
4454
4455 // bookkeeping flags
4456 for (i=0; i<32; i++) {
4457 if ((monst->bookkeepingFlags & (Fl(i)))
4458 && monsterBookkeepingFlagDescriptions[i][0]) {
4459 if (anyFlags) {
4460 strcat(newText, "& ");
4461 commaCount++;
4462 }
4463 strcat(newText, monsterBookkeepingFlagDescriptions[i]);
4464 anyFlags = true;
4465 }
4466 }
4467
4468 if (anyFlags) {
4469 strcat(newText, ". ");
4470 //strcat(buf, "\n\n");
4471 j = strlen(buf);
4472 for (i=0; newText[i] != '\0'; i++) {
4473 if (newText[i] == '&') {
4474 if (!--commaCount) {
4475 buf[j] = '\0';
4476 strcat(buf, " and");
4477 j += 4;
4478 } else {
4479 buf[j++] = ',';
4480 }
4481 } else {
4482 buf[j++] = newText[i];
4483 }
4484 }
4485 buf[j] = '\0';
4486 }
4487 resolvePronounEscapes(buf, monst);
4488 }
4489