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