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