Annotation of brogue-ce/src/brogue/Monsters.c, Revision 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