1 /*
2  *  Movement.c
3  *  Brogue
4  *
5  *  Created by Brian Walker on 1/10/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 
playerRuns(short direction)27 void playerRuns(short direction) {
28     short newX, newY, dir;
29     boolean cardinalPassability[4];
30 
31     rogue.disturbed = (player.status[STATUS_CONFUSED] ? true : false);
32 
33     for (dir = 0; dir < 4; dir++) {
34         newX = player.xLoc + nbDirs[dir][0];
35         newY = player.yLoc + nbDirs[dir][1];
36         cardinalPassability[dir] = monsterAvoids(&player, newX, newY);
37     }
38 
39     while (!rogue.disturbed) {
40         if (!playerMoves(direction)) {
41             rogue.disturbed = true;
42             break;
43         }
44 
45         newX = player.xLoc + nbDirs[direction][0];
46         newY = player.yLoc + nbDirs[direction][1];
47         if (!coordinatesAreInMap(newX, newY)
48             || monsterAvoids(&player, newX, newY)) {
49 
50             rogue.disturbed = true;
51         }
52         if (isDisturbed(player.xLoc, player.yLoc)) {
53             rogue.disturbed = true;
54         } else if (direction < 4) {
55             for (dir = 0; dir < 4; dir++) {
56                 newX = player.xLoc + nbDirs[dir][0];
57                 newY = player.yLoc + nbDirs[dir][1];
58                 if (cardinalPassability[dir] != monsterAvoids(&player, newX, newY)
59                     && !(nbDirs[dir][0] + nbDirs[direction][0] == 0 &&
60                          nbDirs[dir][1] + nbDirs[direction][1] == 0)) {
61                         // dir is not the x-opposite or y-opposite of direction
62                     rogue.disturbed = true;
63                 }
64             }
65         }
66     }
67     updateFlavorText();
68 }
69 
highestPriorityLayer(short x,short y,boolean skipGas)70 enum dungeonLayers highestPriorityLayer(short x, short y, boolean skipGas) {
71     short bestPriority = 10000;
72     enum dungeonLayers tt, best = 0;
73 
74     for (tt = 0; tt < NUMBER_TERRAIN_LAYERS; tt++) {
75         if (tt == GAS && skipGas) {
76             continue;
77         }
78         if (pmap[x][y].layers[tt] && tileCatalog[pmap[x][y].layers[tt]].drawPriority < bestPriority) {
79             bestPriority = tileCatalog[pmap[x][y].layers[tt]].drawPriority;
80             best = tt;
81         }
82     }
83     return best;
84 }
85 
layerWithTMFlag(short x,short y,unsigned long flag)86 enum dungeonLayers layerWithTMFlag(short x, short y, unsigned long flag) {
87     enum dungeonLayers layer;
88 
89     for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
90         if (tileCatalog[pmap[x][y].layers[layer]].mechFlags & flag) {
91             return layer;
92         }
93     }
94     return NO_LAYER;
95 }
96 
layerWithFlag(short x,short y,unsigned long flag)97 enum dungeonLayers layerWithFlag(short x, short y, unsigned long flag) {
98     enum dungeonLayers layer;
99 
100     for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
101         if (tileCatalog[pmap[x][y].layers[layer]].flags & flag) {
102             return layer;
103         }
104     }
105     return NO_LAYER;
106 }
107 
108 // Retrieves a pointer to the flavor text of the highest-priority terrain at the given location
tileFlavor(short x,short y)109 char *tileFlavor(short x, short y) {
110     return tileCatalog[pmap[x][y].layers[highestPriorityLayer(x, y, false)]].flavorText;
111 }
112 
113 // Retrieves a pointer to the description text of the highest-priority terrain at the given location
tileText(short x,short y)114 char *tileText(short x, short y) {
115     return tileCatalog[pmap[x][y].layers[highestPriorityLayer(x, y, false)]].description;
116 }
117 
describedItemBasedOnParameters(short theCategory,short theKind,short theQuantity,short theOriginDepth,char * buf)118 void describedItemBasedOnParameters(short theCategory, short theKind, short theQuantity, short theOriginDepth, char *buf) {
119     item *tempItem = initializeItem();
120     tempItem->category = theCategory;
121     tempItem->kind = theKind;
122     tempItem->quantity = theQuantity;
123     tempItem->originDepth = theOriginDepth;
124     itemName(tempItem, buf, false, true, NULL);
125     free(tempItem);
126     return;
127 }
128 
129 // Describes the item in question either by naming it if the player has already seen its name,
130 // or by tersely identifying its category otherwise.
describedItemName(item * theItem,char * buf)131 void describedItemName(item *theItem, char *buf) {
132     if (rogue.playbackOmniscience || (!player.status[STATUS_HALLUCINATING])) {
133         itemName(theItem, buf, (theItem->category & (WEAPON | ARMOR) ? false : true), true, NULL);
134     } else {
135         describeHallucinatedItem(buf);
136     }
137 }
138 
describeLocation(char * buf,short x,short y)139 void describeLocation(char *buf, short x, short y) {
140     creature *monst;
141     item *theItem, *magicItem;
142     boolean standsInTerrain;
143     boolean subjectMoving;
144     boolean prepositionLocked = false;
145     boolean monsterDormant;
146 
147     char subject[COLS * 3];
148     char verb[COLS * 3];
149     char preposition[COLS * 3];
150     char object[COLS * 3];
151     char adjective[COLS * 3];
152 
153     assureCosmeticRNG;
154 
155     if (x == player.xLoc && y == player.yLoc) {
156         if (player.status[STATUS_LEVITATING]) {
157             sprintf(buf, "you are hovering above %s.", tileText(x, y));
158         } else {
159             strcpy(buf, tileFlavor(x, y));
160         }
161         restoreRNG;
162         return;
163     }
164 
165     monst = NULL;
166     standsInTerrain = ((tileCatalog[pmap[x][y].layers[highestPriorityLayer(x, y, false)]].mechFlags & TM_STAND_IN_TILE) ? true : false);
167     theItem = itemAtLoc(x, y);
168     monsterDormant = false;
169     if (pmap[x][y].flags & HAS_MONSTER) {
170         monst = monsterAtLoc(x, y);
171     } else if (pmap[x][y].flags & HAS_DORMANT_MONSTER) {
172         monst = dormantMonsterAtLoc(x, y);
173         monsterDormant = true;
174     }
175 
176     // detecting magical items
177     magicItem = NULL;
178     if (theItem && !playerCanSeeOrSense(x, y)
179         && (theItem->flags & ITEM_MAGIC_DETECTED)
180         && itemMagicPolarity(theItem)) {
181         magicItem = theItem;
182     } else if (monst && !canSeeMonster(monst)
183                && monst->carriedItem
184                && (monst->carriedItem->flags & ITEM_MAGIC_DETECTED)
185                && itemMagicPolarity(monst->carriedItem)) {
186         magicItem = monst->carriedItem;
187     }
188     if (magicItem && !(pmap[x][y].flags & DISCOVERED)) {
189         switch (itemMagicPolarity(magicItem)) {
190             case 1:
191                 strcpy(object, magicItem->category == AMULET ? "the Amulet of Yendor" : "benevolent magic");
192                 break;
193             case -1:
194                 strcpy(object, "malevolent magic");
195                 break;
196             default:
197                 strcpy(object, "mysterious magic");
198                 break;
199         }
200         sprintf(buf, "you can detect the aura of %s here.", object);
201         restoreRNG;
202         return;
203     }
204 
205     // telepathy
206     if (monst
207         && !canSeeMonster(monst)
208         && monsterRevealed(monst)) {
209 
210         strcpy(adjective, (((!player.status[STATUS_HALLUCINATING] || rogue.playbackOmniscience) && !monst->info.isLarge)
211                            || (player.status[STATUS_HALLUCINATING] && !rogue.playbackOmniscience && rand_range(0, 1)) ? "small" : "large"));
212         if (pmap[x][y].flags & DISCOVERED) {
213             strcpy(object, tileText(x, y));
214             if (monst->bookkeepingFlags & MB_SUBMERGED) {
215                 strcpy(preposition, "under ");
216             } else if (monsterDormant) {
217                 strcpy(preposition, "coming from within ");
218             } else if (standsInTerrain) {
219                 strcpy(preposition, "in ");
220             } else {
221                 strcpy(preposition, "over ");
222             }
223         } else {
224             strcpy(object, "here");
225             strcpy(preposition, "");
226         }
227 
228         sprintf(buf, "you can sense a %s psychic emanation %s%s.", adjective, preposition, object);
229         restoreRNG;
230         return;
231     }
232 
233     if (monst && !canSeeMonster(monst) && !rogue.playbackOmniscience) {
234         // Monster is not visible.
235         monst = NULL;
236     }
237 
238     if (!playerCanSeeOrSense(x, y)) {
239         if (pmap[x][y].flags & DISCOVERED) { // memory
240             if (pmap[x][y].rememberedItemCategory) {
241                 if (player.status[STATUS_HALLUCINATING] && !rogue.playbackOmniscience) {
242                     describeHallucinatedItem(object);
243                 } else {
244                     describedItemBasedOnParameters(pmap[x][y].rememberedItemCategory, pmap[x][y].rememberedItemKind,
245                                                    pmap[x][y].rememberedItemQuantity, pmap[x][y].rememberedItemOriginDepth, object);
246                 }
247             } else {
248                 strcpy(object, tileCatalog[pmap[x][y].rememberedTerrain].description);
249             }
250             sprintf(buf, "you remember seeing %s here.", object);
251             restoreRNG;
252             return;
253         } else if (pmap[x][y].flags & MAGIC_MAPPED) { // magic mapped
254             sprintf(buf, "you expect %s to be here.", tileCatalog[pmap[x][y].rememberedTerrain].description);
255             restoreRNG;
256             return;
257         }
258         strcpy(buf, "");
259         restoreRNG;
260         return;
261     }
262 
263     if (monst) {
264 
265         monsterName(subject, monst, true);
266 
267         if (pmap[x][y].layers[GAS] && monst->status[STATUS_INVISIBLE]) { // phantoms in gas
268             sprintf(buf, "you can perceive the faint outline of %s in %s.", subject, tileCatalog[pmap[x][y].layers[GAS]].description);
269             restoreRNG;
270             return;
271         }
272 
273         subjectMoving = (monst->turnsSpentStationary == 0
274                          && !(monst->info.flags & (MONST_GETS_TURN_ON_ACTIVATION | MONST_IMMOBILE))
275                          && monst->creatureState != MONSTER_SLEEPING
276                          && !(monst->bookkeepingFlags & (MB_SEIZED | MB_CAPTIVE)));
277         if ((monst->info.flags & MONST_ATTACKABLE_THRU_WALLS)
278             && cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY)) {
279             strcpy(verb, "is embedded");
280         } else if (cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY)) {
281             strcpy(verb, "is trapped");
282             subjectMoving = false;
283         } else if (monst->bookkeepingFlags & MB_CAPTIVE) {
284             strcpy(verb, "is shackled in place");
285             subjectMoving = false;
286         } else if (monst->status[STATUS_PARALYZED]) {
287             strcpy(verb, "is frozen in place");
288             subjectMoving = false;
289         } else if (monst->status[STATUS_STUCK]) {
290             strcpy(verb, "is entangled");
291             subjectMoving = false;
292         } else if (monst->status[STATUS_LEVITATING]) {
293             strcpy(verb, (subjectMoving ? "is flying" : "is hovering"));
294             strcpy(preposition, "over");
295             prepositionLocked = true;
296         } else if (monsterCanSubmergeNow(monst)) {
297             strcpy(verb, (subjectMoving ? "is gliding" : "is drifting"));
298         } else if (cellHasTerrainFlag(x, y, T_MOVES_ITEMS) && !(monst->info.flags & MONST_SUBMERGES)) {
299             strcpy(verb, (subjectMoving ? "is swimming" : "is struggling"));
300         } else if (cellHasTerrainFlag(x, y, T_AUTO_DESCENT)) {
301             strcpy(verb, "is suspended in mid-air");
302             strcpy(preposition, "over");
303             prepositionLocked = true;
304             subjectMoving = false;
305         } else if (monst->status[STATUS_CONFUSED]) {
306             strcpy(verb, "is staggering");
307         } else if ((monst->info.flags & MONST_RESTRICTED_TO_LIQUID)
308                    && !cellHasTMFlag(monst->xLoc, monst->yLoc, TM_ALLOWS_SUBMERGING)) {
309             strcpy(verb, "is lying");
310             subjectMoving = false;
311         } else if (monst->info.flags & MONST_IMMOBILE) {
312             strcpy(verb, "is resting");
313         } else {
314             switch (monst->creatureState) {
315                 case MONSTER_SLEEPING:
316                     strcpy(verb, "is sleeping");
317                     subjectMoving = false;
318                     break;
319                 case MONSTER_WANDERING:
320                     strcpy(verb, subjectMoving ? "is wandering" : "is standing");
321                     break;
322                 case MONSTER_FLEEING:
323                     strcpy(verb, subjectMoving ? "is fleeing" : "is standing");
324                     break;
325                 case MONSTER_TRACKING_SCENT:
326                     strcpy(verb, subjectMoving ? "is charging" : "is standing");
327                     break;
328                 case MONSTER_ALLY:
329                     strcpy(verb, subjectMoving ? "is following you" : "is standing");
330                     break;
331                 default:
332                     strcpy(verb, "is standing");
333                     break;
334             }
335         }
336         if (monst->status[STATUS_BURNING] && !(monst->info.flags & MONST_FIERY)) {
337             strcat(verb, ", burning,");
338         }
339 
340         if (theItem) {
341             strcpy(preposition, "over");
342             describedItemName(theItem, object);
343         } else {
344             if (!prepositionLocked) {
345                 strcpy(preposition, subjectMoving ? (standsInTerrain ? "through" : "across")
346                        : (standsInTerrain ? "in" : "on"));
347             }
348 
349             strcpy(object, tileText(x, y));
350 
351         }
352     } else { // no monster
353         strcpy(object, tileText(x, y));
354         if (theItem) {
355             describedItemName(theItem, subject);
356             subjectMoving = cellHasTerrainFlag(x, y, T_MOVES_ITEMS);
357             if (player.status[STATUS_HALLUCINATING] && !rogue.playbackOmniscience) {
358                 strcpy(verb, "is");
359             } else {
360                 strcpy(verb, (theItem->quantity > 1 || (theItem->category & GOLD)) ? "are" : "is");
361             }
362             if (cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY)) {
363                 strcat(verb, " enclosed");
364             } else {
365                 strcat(verb, subjectMoving ? " drifting" : " lying");
366             }
367             strcpy(preposition, standsInTerrain ? (subjectMoving ? "through" : "in")
368                    : (subjectMoving ? "across" : "on"));
369 
370 
371         } else { // no item
372             sprintf(buf, "you %s %s.", (playerCanDirectlySee(x, y) ? "see" : "sense"), object);
373             restoreRNG;
374             return;
375         }
376     }
377 
378     sprintf(buf, "%s %s %s %s.", subject, verb, preposition, object);
379     restoreRNG;
380 }
381 
printLocationDescription(short x,short y)382 void printLocationDescription(short x, short y) {
383     char buf[DCOLS*3];
384     describeLocation(buf, x, y);
385     flavorMessage(buf);
386 }
387 
useKeyAt(item * theItem,short x,short y)388 void useKeyAt(item *theItem, short x, short y) {
389     short layer, i;
390     creature *monst;
391     char buf[COLS], buf2[COLS], terrainName[COLS], preposition[10];
392     boolean disposable;
393 
394     strcpy(terrainName, "unknown terrain"); // redundant failsafe
395     for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
396         if (tileCatalog[pmap[x][y].layers[layer]].mechFlags & TM_PROMOTES_WITH_KEY) {
397             if (tileCatalog[pmap[x][y].layers[layer]].description[0] == 'a'
398                 && tileCatalog[pmap[x][y].layers[layer]].description[1] == ' ') {
399                 sprintf(terrainName, "the %s", &(tileCatalog[pmap[x][y].layers[layer]].description[2]));
400             } else {
401                 strcpy(terrainName, tileCatalog[pmap[x][y].layers[layer]].description);
402             }
403             if (tileCatalog[pmap[x][y].layers[layer]].mechFlags & TM_STAND_IN_TILE) {
404                 strcpy(preposition, "in");
405             } else {
406                 strcpy(preposition, "on");
407             }
408             promoteTile(x, y, layer, false);
409         }
410     }
411 
412     disposable = false;
413     for (i=0; i < KEY_ID_MAXIMUM && (theItem->keyLoc[i].x || theItem->keyLoc[i].machine); i++) {
414         if (theItem->keyLoc[i].x == x && theItem->keyLoc[i].y == y && theItem->keyLoc[i].disposableHere) {
415             disposable = true;
416         } else if (theItem->keyLoc[i].machine == pmap[x][y].machineNumber && theItem->keyLoc[i].disposableHere) {
417             disposable = true;
418         }
419     }
420 
421     if (disposable) {
422         if (removeItemFromChain(theItem, packItems)) {
423             itemName(theItem, buf2, true, false, NULL);
424             sprintf(buf, "you use your %s %s %s.",
425                     buf2,
426                     preposition,
427                     terrainName);
428             messageWithColor(buf, &itemMessageColor, 0);
429             deleteItem(theItem);
430         } else if (removeItemFromChain(theItem, floorItems)) {
431             deleteItem(theItem);
432             pmap[x][y].flags &= ~HAS_ITEM;
433         } else if (pmap[x][y].flags & HAS_MONSTER) {
434             monst = monsterAtLoc(x, y);
435             if (monst->carriedItem && monst->carriedItem == theItem) {
436                 monst->carriedItem = NULL;
437                 deleteItem(theItem);
438             }
439         }
440     }
441 }
442 
randValidDirectionFrom(creature * monst,short x,short y,boolean respectAvoidancePreferences)443 short randValidDirectionFrom(creature *monst, short x, short y, boolean respectAvoidancePreferences) {
444     short i, newX, newY, validDirections[8], count = 0;
445 
446     brogueAssert(rogue.RNG == RNG_SUBSTANTIVE);
447     for (i=0; i<8; i++) {
448         newX = x + nbDirs[i][0];
449         newY = y + nbDirs[i][1];
450         if (coordinatesAreInMap(newX, newY)
451             && !cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_PASSABILITY)
452             && !diagonalBlocked(x, y, newX, newY, false)
453             && (!respectAvoidancePreferences
454                 || (!monsterAvoids(monst, newX, newY))
455                 || ((pmap[newX][newY].flags & HAS_PLAYER) && monst->creatureState != MONSTER_ALLY))) {
456             validDirections[count++] = i;
457         }
458     }
459     if (count == 0) {
460         // Rare, and important in this case that the function returns BEFORE a random roll is made to avoid OOS.
461         return NO_DIRECTION;
462     }
463     return validDirections[rand_range(0, count - 1)];
464 }
465 
vomit(creature * monst)466 void vomit(creature *monst) {
467     char buf[COLS], monstName[COLS];
468     spawnDungeonFeature(monst->xLoc, monst->yLoc, &dungeonFeatureCatalog[DF_VOMIT], true, false);
469 
470     if (canDirectlySeeMonster(monst)
471         && !rogue.automationActive) {
472 
473         monsterName(monstName, monst, true);
474         sprintf(buf, "%s vomit%s profusely", monstName, (monst == &player ? "" : "s"));
475         combatMessage(buf, NULL);
476     }
477 }
478 
moveEntrancedMonsters(enum directions dir)479 void moveEntrancedMonsters(enum directions dir) {
480     dir = oppositeDirection(dir);
481 
482     for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
483         creature *monst = nextCreature(&it);
484         monst->bookkeepingFlags &= ~MB_HAS_ENTRANCED_MOVED;
485     }
486 
487     for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
488         creature *monst = nextCreature(&it);
489         if (!(monst->bookkeepingFlags & MB_HAS_ENTRANCED_MOVED)
490             && monst->status[STATUS_ENTRANCED]
491             && !monst->status[STATUS_STUCK]
492             && !monst->status[STATUS_PARALYZED]
493             && !(monst->bookkeepingFlags & MB_CAPTIVE)) {
494 
495             moveMonster(monst, nbDirs[dir][0], nbDirs[dir][1]);
496             monst->bookkeepingFlags |= MB_HAS_ENTRANCED_MOVED;
497             restartIterator(&it); // loop through from the beginning to be safe
498         }
499     }
500 }
501 
becomeAllyWith(creature * monst)502 void becomeAllyWith(creature *monst) {
503     demoteMonsterFromLeadership(monst);
504     // Drop your item.
505     if (monst->carriedItem) {
506         makeMonsterDropItem(monst);
507     }
508     // If you're going to change into something, it should be friendly.
509     if (monst->carriedMonster) {
510         becomeAllyWith(monst->carriedMonster);
511     }
512     monst->creatureState = MONSTER_ALLY;
513     monst->bookkeepingFlags |= MB_FOLLOWER;
514     monst->leader = &player;
515     monst->bookkeepingFlags &= ~(MB_CAPTIVE | MB_SEIZED);
516     refreshDungeonCell(monst->xLoc, monst->yLoc);
517 }
518 
freeCaptive(creature * monst)519 void freeCaptive(creature *monst) {
520     char buf[COLS * 3], monstName[COLS];
521 
522     becomeAllyWith(monst);
523     monsterName(monstName, monst, false);
524     sprintf(buf, "you free the grateful %s and gain a faithful ally.", monstName);
525     message(buf, 0);
526 }
527 
freeCaptivesEmbeddedAt(short x,short y)528 boolean freeCaptivesEmbeddedAt(short x, short y) {
529     creature *monst;
530 
531     if (pmap[x][y].flags & HAS_MONSTER) {
532         // Free any captives trapped in the tunnelized terrain.
533         monst = monsterAtLoc(x, y);
534         if ((monst->bookkeepingFlags & MB_CAPTIVE)
535             && !(monst->info.flags & MONST_ATTACKABLE_THRU_WALLS)
536             && (cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY))) {
537             freeCaptive(monst);
538             return true;
539         }
540     }
541     return false;
542 }
543 
544 // Do we need confirmation so we don't accidently hit an acid mound?
abortAttackAgainstAcidicTarget(creature * hitList[8])545 boolean abortAttackAgainstAcidicTarget(creature *hitList[8]) {
546     short i;
547     char monstName[COLS], weaponName[COLS];
548     char buf[COLS*3];
549 
550     if (rogue.weapon
551         && !(rogue.weapon->flags & ITEM_PROTECTED)
552         && !player.status[STATUS_HALLUCINATING]
553         && !player.status[STATUS_CONFUSED]) {
554 
555         for (i=0; i<8; i++) {
556             if (hitList[i]
557                 && (hitList[i]->info.flags & MONST_DEFEND_DEGRADE_WEAPON)
558                 && canSeeMonster(hitList[i])
559                 && (!(rogue.weapon->flags & ITEM_RUNIC)
560                     || !(rogue.weapon->flags & ITEM_RUNIC_IDENTIFIED)
561                     || rogue.weapon->enchant2 != W_SLAYING
562                     || !monsterIsInClass(hitList[i], rogue.weapon->vorpalEnemy))) {
563 
564                 monsterName(monstName, hitList[i], true);
565                 itemName(rogue.weapon, weaponName, false, false, NULL);
566                 sprintf(buf, "Degrade your %s by attacking %s?", weaponName, monstName);
567                 if (confirm(buf, false)) {
568                     return false; // Fire when ready!
569                 } else {
570                     return true; // Abort!
571                 }
572             }
573         }
574     }
575     return false;
576 }
577 
578 // Returns true if a whip attack was launched.
579 // If "aborted" pointer is provided, sets it to true if it was aborted because
580 // the player opted not to attack an acid mound (in which case the whole turn
581 // should be aborted), as opposed to there being no valid whip attack available
582 // (in which case the player/monster should move instead).
handleWhipAttacks(creature * attacker,enum directions dir,boolean * aborted)583 boolean handleWhipAttacks(creature *attacker, enum directions dir, boolean *aborted) {
584     bolt theBolt;
585     creature *defender, *hitList[8] = {0};
586     short strikeLoc[2], originLoc[2], targetLoc[2];
587 
588     const char boltChar[DIRECTION_COUNT] = "||~~\\//\\";
589 
590     brogueAssert(dir > NO_DIRECTION && dir < DIRECTION_COUNT);
591 
592     if (attacker == &player) {
593         if (!rogue.weapon || !(rogue.weapon->flags & ITEM_ATTACKS_EXTEND)) {
594             return false;
595         }
596     } else if (!(attacker->info.abilityFlags & MA_ATTACKS_EXTEND)) {
597         return false;
598     }
599     originLoc[0] = attacker->xLoc;
600     originLoc[1] = attacker->yLoc;
601     targetLoc[0] = attacker->xLoc + nbDirs[dir][0];
602     targetLoc[1] = attacker->yLoc + nbDirs[dir][1];
603     getImpactLoc(strikeLoc, originLoc, targetLoc, 5, false, &boltCatalog[BOLT_WHIP]);
604 
605     defender = monsterAtLoc(strikeLoc[0], strikeLoc[1]);
606     if (defender
607         && (attacker != &player || canSeeMonster(defender))
608         && !monsterIsHidden(defender, attacker)
609         && monsterWillAttackTarget(attacker, defender)) {
610 
611         if (attacker == &player) {
612             hitList[0] = defender;
613             if (abortAttackAgainstAcidicTarget(hitList)) {
614                 if (aborted) {
615                     *aborted = true;
616                 }
617                 return false;
618             }
619         }
620         attacker->bookkeepingFlags &= ~MB_SUBMERGED;
621         theBolt = boltCatalog[BOLT_WHIP];
622         theBolt.theChar = boltChar[dir];
623         zap(originLoc, targetLoc, &theBolt, false);
624         return true;
625     }
626     return false;
627 }
628 
629 // Returns true if a spear attack was launched.
630 // If "aborted" pointer is provided, sets it to true if it was aborted because
631 // the player opted not to attack an acid mound (in which case the whole turn
632 // should be aborted), as opposed to there being no valid spear attack available
633 // (in which case the player/monster should move instead).
handleSpearAttacks(creature * attacker,enum directions dir,boolean * aborted)634 boolean handleSpearAttacks(creature *attacker, enum directions dir, boolean *aborted) {
635     creature *defender, *hitList[8] = {0};
636     short targetLoc[2], range = 2, i = 0, h = 0;
637     boolean proceed = false, visualEffect = false;
638 
639     const char boltChar[DIRECTION_COUNT] = "||--\\//\\";
640 
641     brogueAssert(dir > NO_DIRECTION && dir < DIRECTION_COUNT);
642 
643     if (attacker == &player) {
644         if (!rogue.weapon || !(rogue.weapon->flags & ITEM_ATTACKS_PENETRATE)) {
645             return false;
646         }
647     } else if (!(attacker->info.abilityFlags & MA_ATTACKS_PENETRATE)) {
648         return false;
649     }
650 
651     for (i = 0; i < range; i++) {
652         targetLoc[0] = attacker->xLoc + (1 + i) * nbDirs[dir][0];
653         targetLoc[1] = attacker->yLoc + (1 + i) * nbDirs[dir][1];
654         if (!coordinatesAreInMap(targetLoc[0], targetLoc[1])) {
655             break;
656         }
657 
658         /* Add creatures that we are willing to attack to the potential
659         hitlist. Any of those that are either right by us or visible will
660         trigger the attack. */
661         defender = monsterAtLoc(targetLoc[0], targetLoc[1]);
662         if (defender
663             && (!cellHasTerrainFlag(targetLoc[0], targetLoc[1], T_OBSTRUCTS_PASSABILITY)
664                 || (defender->info.flags & MONST_ATTACKABLE_THRU_WALLS))
665             && monsterWillAttackTarget(attacker, defender)) {
666 
667             hitList[h++] = defender;
668 
669             /* We check if i=0, i.e. the defender is right next to us, because
670             we have to do "normal" attacking here. We can't just return
671             false and leave to playerMoves/moveMonster due to the collateral hitlist. */
672             if (i == 0 || !monsterIsHidden(defender, attacker)
673                 && (attacker != &player || canSeeMonster(defender))) {
674                 // We'll attack.
675                 proceed = true;
676             }
677         }
678 
679         if (cellHasTerrainFlag(targetLoc[0], targetLoc[1], (T_OBSTRUCTS_PASSABILITY | T_OBSTRUCTS_VISION))) {
680             break;
681         }
682     }
683     range = i;
684     if (proceed) {
685         if (attacker == &player) {
686             if (abortAttackAgainstAcidicTarget(hitList)) {
687                 if (aborted) {
688                     *aborted = true;
689                 }
690                 return false;
691             }
692         }
693         if (!rogue.playbackFastForward) {
694             for (i = 0; i < range; i++) {
695                 targetLoc[0] = attacker->xLoc + (1 + i) * nbDirs[dir][0];
696                 targetLoc[1] = attacker->yLoc + (1 + i) * nbDirs[dir][1];
697                 if (coordinatesAreInMap(targetLoc[0], targetLoc[1])
698                     && playerCanSeeOrSense(targetLoc[0], targetLoc[1])) {
699 
700                     visualEffect = true;
701                     plotForegroundChar(boltChar[dir], targetLoc[0], targetLoc[1], &lightBlue, true);
702                 }
703             }
704         }
705         attacker->bookkeepingFlags &= ~MB_SUBMERGED;
706         // Artificially reverse the order of the attacks,
707         // so that spears of force can send both monsters flying.
708         for (i = h - 1; i >= 0; i--) {
709             attack(attacker, hitList[i], false);
710         }
711         if (visualEffect) {
712             pauseBrogue(16);
713             for (i = 0; i < range; i++) {
714                 targetLoc[0] = attacker->xLoc + (1 + i) * nbDirs[dir][0];
715                 targetLoc[1] = attacker->yLoc + (1 + i) * nbDirs[dir][1];
716                 if (coordinatesAreInMap(targetLoc[0], targetLoc[1])) {
717                     refreshDungeonCell(targetLoc[0], targetLoc[1]);
718                 }
719             }
720         }
721         return true;
722     }
723     return false;
724 }
725 
buildFlailHitList(const short x,const short y,const short newX,const short newY,creature * hitList[16])726 void buildFlailHitList(const short x, const short y, const short newX, const short newY, creature *hitList[16]) {
727     short mx, my;
728     short i = 0;
729 
730     for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
731         creature *monst = nextCreature(&it);
732         mx = monst->xLoc;
733         my = monst->yLoc;
734         if (distanceBetween(x, y, mx, my) == 1
735             && distanceBetween(newX, newY, mx, my) == 1
736             && canSeeMonster(monst)
737             && monstersAreEnemies(&player, monst)
738             && monst->creatureState != MONSTER_ALLY
739             && !(monst->bookkeepingFlags & MB_IS_DYING)
740             && (!cellHasTerrainFlag(monst->xLoc, monst->yLoc, T_OBSTRUCTS_PASSABILITY) || (monst->info.flags & MONST_ATTACKABLE_THRU_WALLS))) {
741 
742             while (hitList[i]) {
743                 i++;
744             }
745             hitList[i] = monst;
746         }
747     }
748 }
749 
diagonalBlocked(const short x1,const short y1,const short x2,const short y2,const boolean limitToPlayerKnowledge)750 boolean diagonalBlocked(const short x1, const short y1, const short x2, const short y2, const boolean limitToPlayerKnowledge) {
751     unsigned long tFlags;
752     if (x1 == x2 || y1 == y2) {
753         return false; // If it's not a diagonal, it's not diagonally blocked.
754     }
755     getLocationFlags(x1, y2, &tFlags, NULL, NULL, limitToPlayerKnowledge);
756     if (tFlags & T_OBSTRUCTS_DIAGONAL_MOVEMENT) {
757         return true;
758     }
759     getLocationFlags(x2, y1, &tFlags, NULL, NULL, limitToPlayerKnowledge);
760     if (tFlags & T_OBSTRUCTS_DIAGONAL_MOVEMENT) {
761         return true;
762     }
763     return false;
764 }
765 
766 // Called whenever the player voluntarily tries to move in a given direction.
767 // Can be called from movement keys, exploration, or auto-travel.
playerMoves(short direction)768 boolean playerMoves(short direction) {
769     short initialDirection = direction, i, layer;
770     short x = player.xLoc, y = player.yLoc;
771     short newX, newY, newestX, newestY;
772     boolean playerMoved = false, specialAttackAborted = false, anyAttackHit = false;
773     creature *defender = NULL, *tempMonst = NULL, *hitList[16] = {NULL};
774     char monstName[COLS];
775     char buf[COLS*3];
776     const int directionKeys[8] = {UP_KEY, DOWN_KEY, LEFT_KEY, RIGHT_KEY, UPLEFT_KEY, DOWNLEFT_KEY, UPRIGHT_KEY, DOWNRIGHT_KEY};
777 
778     brogueAssert(direction >= 0 && direction < DIRECTION_COUNT);
779 
780     newX = x + nbDirs[direction][0];
781     newY = y + nbDirs[direction][1];
782 
783     if (!coordinatesAreInMap(newX, newY)) {
784         return false;
785     }
786 
787     // Save thet keystroke up-front; we'll revert if the player cancels.
788     recordKeystroke(directionKeys[initialDirection], false, false);
789     boolean committed = false; // as long as this is false, the keystroke can be cancelled
790 
791     if (player.status[STATUS_CONFUSED]) {
792         // Confirmation dialog if you're moving while confused and you're next to lava and not levitating or immune to fire.
793         if (player.status[STATUS_LEVITATING] <= 1
794             && player.status[STATUS_IMMUNE_TO_FIRE] <= 1) {
795 
796             for (i=0; i<8; i++) {
797                 newestX = x + nbDirs[i][0];
798                 newestY = y + nbDirs[i][1];
799                 if (coordinatesAreInMap(newestX, newestY)
800                     && (pmap[newestX][newestY].flags & (DISCOVERED | MAGIC_MAPPED))
801                     && !diagonalBlocked(x, y, newestX, newestY, false)
802                     && cellHasTerrainFlag(newestX, newestY, T_LAVA_INSTA_DEATH)
803                     && !cellHasTerrainFlag(newestX, newestY, T_OBSTRUCTS_PASSABILITY | T_ENTANGLES)
804                     && !((pmap[newestX][newestY].flags & HAS_MONSTER)
805                          && canSeeMonster(monsterAtLoc(newestX, newestY))
806                          && monsterAtLoc(newestX, newestY)->creatureState != MONSTER_ALLY)) {
807 
808                     if (!confirm("Risk stumbling into lava?", false)) {
809                         cancelKeystroke();
810                         return false;
811                     } else {
812                         break;
813                     }
814                 }
815             }
816         }
817 
818         direction = randValidDirectionFrom(&player, x, y, false);
819         if (direction == -1) {
820             cancelKeystroke();
821             return false;
822         } else {
823             newX = x + nbDirs[direction][0];
824             newY = y + nbDirs[direction][1];
825             if (!coordinatesAreInMap(newX, newY)) {
826                 cancelKeystroke();
827                 return false;
828             }
829             committed = true;
830         }
831     }
832 
833     if (pmap[newX][newY].flags & HAS_MONSTER) {
834         defender = monsterAtLoc(newX, newY);
835     }
836 
837     // If there's no enemy at the movement location that the player is aware of, consider terrain promotions.
838     if (!defender
839         || (!canSeeMonster(defender) && !monsterRevealed(defender))
840         || !monstersAreEnemies(&player, defender)) {
841 
842         if (cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_PASSABILITY) && cellHasTMFlag(newX, newY, TM_PROMOTES_ON_PLAYER_ENTRY)) {
843             layer = layerWithTMFlag(newX, newY, TM_PROMOTES_ON_PLAYER_ENTRY);
844             if (tileCatalog[pmap[newX][newY].layers[layer]].flags & T_OBSTRUCTS_PASSABILITY) {
845                 committed = true;
846                 message(tileCatalog[pmap[newX][newY].layers[layer]].flavorText, 0);
847                 promoteTile(newX, newY, layer, false);
848                 playerTurnEnded();
849                 return true;
850             }
851         }
852 
853     }
854 
855     if (((!cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_PASSABILITY) || (cellHasTMFlag(newX, newY, TM_PROMOTES_WITH_KEY) && keyInPackFor(newX, newY)))
856          && !diagonalBlocked(x, y, newX, newY, false)
857          && (!cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY) || (cellHasTMFlag(x, y, TM_PROMOTES_WITH_KEY) && keyInPackFor(x, y))))
858         || (defender && defender->info.flags & MONST_ATTACKABLE_THRU_WALLS)) {
859         // if the move is not blocked
860 
861         if (handleWhipAttacks(&player, direction, &specialAttackAborted)
862             || handleSpearAttacks(&player, direction, &specialAttackAborted)) {
863 
864             committed = true;
865             playerRecoversFromAttacking(true);
866             moveEntrancedMonsters(direction);
867             playerTurnEnded();
868             return true;
869         } else if (specialAttackAborted) { // Canceled an attack against an acid mound.
870             brogueAssert(!committed);
871             cancelKeystroke();
872             rogue.disturbed = true;
873             return false;
874         }
875 
876         if (defender) {
877             // if there is a monster there
878 
879             if (defender->bookkeepingFlags & MB_CAPTIVE) {
880                 monsterName(monstName, defender, false);
881                 sprintf(buf, "Free the captive %s?", monstName);
882                 if (committed || confirm(buf, false)) {
883                     committed = true;
884                     if (cellHasTMFlag(newX, newY, TM_PROMOTES_WITH_KEY) && keyInPackFor(newX, newY)) {
885                         useKeyAt(keyInPackFor(newX, newY), newX, newY);
886                     }
887                     freeCaptive(defender);
888                     player.ticksUntilTurn += player.attackSpeed;
889                     playerTurnEnded();
890                     return true;
891                 } else {
892                     cancelKeystroke();
893                     return false;
894                 }
895             }
896 
897             if (defender->creatureState != MONSTER_ALLY) {
898                 // Make a hit list of monsters the player is attacking this turn.
899                 // We separate this tallying phase from the actual attacking phase because sometimes the attacks themselves
900                 // create more monsters, and those shouldn't be attacked in the same turn.
901 
902                 buildHitList(hitList, &player, defender,
903                              rogue.weapon && (rogue.weapon->flags & ITEM_ATTACKS_ALL_ADJACENT));
904 
905                 if (abortAttackAgainstAcidicTarget(hitList)) { // Acid mound attack confirmation.
906                     brogueAssert(!committed);
907                     cancelKeystroke();
908                     rogue.disturbed = true;
909                     return false;
910                 }
911 
912                 if (player.status[STATUS_NAUSEOUS]) {
913                     committed = true;
914                     if (rand_percent(25)) {
915                         vomit(&player);
916                         playerTurnEnded();
917                         return false;
918                     }
919                 }
920 
921                 // Proceeding with the attack.
922                 committed = true;
923 
924                 // Attack!
925                 for (i=0; i<16; i++) {
926                     if (hitList[i]
927                         && monsterWillAttackTarget(&player, hitList[i])
928                         && !(hitList[i]->bookkeepingFlags & MB_IS_DYING)
929                         && !rogue.gameHasEnded) {
930 
931                         if (attack(&player, hitList[i], false)) {
932                             anyAttackHit = true;
933                         }
934                     }
935                 }
936 
937                 playerRecoversFromAttacking(anyAttackHit);
938                 moveEntrancedMonsters(direction);
939                 playerTurnEnded();
940                 return true;
941             }
942         }
943 
944         if (player.bookkeepingFlags & MB_SEIZED) {
945             for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
946                 creature *tempMonst = nextCreature(&it);
947                 if ((tempMonst->bookkeepingFlags & MB_SEIZING)
948                     && monstersAreEnemies(&player, tempMonst)
949                     && distanceBetween(player.xLoc, player.yLoc, tempMonst->xLoc, tempMonst->yLoc) == 1
950                     && !diagonalBlocked(player.xLoc, player.yLoc, tempMonst->xLoc, tempMonst->yLoc, false)
951                     && !tempMonst->status[STATUS_ENTRANCED]) {
952 
953                     monsterName(monstName, tempMonst, true);
954                     if (committed || !canSeeMonster(tempMonst)) {
955                         committed = true;
956                         sprintf(buf, "you struggle but %s is holding your legs!", monstName);
957                         moveEntrancedMonsters(direction);
958                         message(buf, 0);
959                         playerTurnEnded();
960                         return true;
961                     } else {
962                         sprintf(buf, "you cannot move; %s is holding your legs!", monstName);
963                         message(buf, 0);
964                         cancelKeystroke();
965                         return false;
966                     }
967                 }
968             }
969             player.bookkeepingFlags &= ~MB_SEIZED; // failsafe
970         }
971 
972         if (pmap[newX][newY].flags & (DISCOVERED | MAGIC_MAPPED)
973             && player.status[STATUS_LEVITATING] <= 1
974             && !player.status[STATUS_CONFUSED]
975             && cellHasTerrainFlag(newX, newY, T_LAVA_INSTA_DEATH)
976             && player.status[STATUS_IMMUNE_TO_FIRE] <= 1
977             && !cellHasTerrainFlag(newX, newY, T_ENTANGLES)
978             && !cellHasTMFlag(newX, newY, TM_IS_SECRET)) {
979             message("that would be certain death!", 0);
980             brogueAssert(!committed);
981             cancelKeystroke();
982             return false; // player won't willingly step into lava
983 
984         } else if (pmap[newX][newY].flags & (DISCOVERED | MAGIC_MAPPED)
985                    && player.status[STATUS_LEVITATING] <= 1
986                    && !player.status[STATUS_CONFUSED]
987                    && cellHasTerrainFlag(newX, newY, T_AUTO_DESCENT)
988                    && !cellHasTerrainFlag(newX, newY, T_ENTANGLES)
989                    && !cellHasTMFlag(newX, newY, TM_IS_SECRET)
990                    && !confirm("Dive into the depths?", false)) {
991 
992             brogueAssert(!committed);
993             cancelKeystroke();
994             return false;
995 
996         } else if (playerCanSee(newX, newY)
997                    && !player.status[STATUS_CONFUSED]
998                    && !player.status[STATUS_BURNING]
999                    && player.status[STATUS_IMMUNE_TO_FIRE] <= 1
1000                    && cellHasTerrainFlag(newX, newY, T_IS_FIRE)
1001                    && !cellHasTMFlag(newX, newY, TM_EXTINGUISHES_FIRE)
1002                    && !confirm("Venture into flame?", false)) {
1003 
1004             brogueAssert(!committed);
1005             cancelKeystroke();
1006             return false;
1007 
1008         } else if (playerCanSee(newX, newY)
1009                    && !player.status[STATUS_CONFUSED]
1010                    && !player.status[STATUS_BURNING]
1011                    && cellHasTerrainFlag(newX, newY, T_CAUSES_CONFUSION | T_CAUSES_PARALYSIS)
1012                    && (!rogue.armor || !(rogue.armor->flags & ITEM_RUNIC) || !(rogue.armor->flags & ITEM_RUNIC_IDENTIFIED) || rogue.armor->enchant2 != A_RESPIRATION)
1013                    && !confirm("Venture into dangerous gas?", false)) {
1014 
1015             brogueAssert(!committed);
1016             cancelKeystroke();
1017             return false;
1018 
1019         } else if (pmap[newX][newY].flags & (ANY_KIND_OF_VISIBLE | MAGIC_MAPPED)
1020                    && player.status[STATUS_LEVITATING] <= 1
1021                    && !player.status[STATUS_CONFUSED]
1022                    && cellHasTerrainFlag(newX, newY, T_IS_DF_TRAP)
1023                    && !(pmap[newX][newY].flags & PRESSURE_PLATE_DEPRESSED)
1024                    && !cellHasTMFlag(newX, newY, TM_IS_SECRET)
1025                    && (!rogue.armor || !(rogue.armor->flags & ITEM_RUNIC) || !(rogue.armor->flags & ITEM_RUNIC_IDENTIFIED) || rogue.armor->enchant2 != A_RESPIRATION ||
1026                         (!cellHasTerrainType(newX, newY, GAS_TRAP_POISON)
1027                          && !cellHasTerrainType(newX, newY, GAS_TRAP_PARALYSIS)
1028                          && !cellHasTerrainType(newX, newY, GAS_TRAP_CONFUSION)))
1029                    && !confirm("Step onto the pressure plate?", false)) {
1030 
1031             brogueAssert(!committed);
1032             cancelKeystroke();
1033             return false;
1034         }
1035 
1036         if (rogue.weapon && (rogue.weapon->flags & ITEM_LUNGE_ATTACKS)) {
1037             newestX = player.xLoc + 2*nbDirs[direction][0];
1038             newestY = player.yLoc + 2*nbDirs[direction][1];
1039             if (coordinatesAreInMap(newestX, newestY) && (pmap[newestX][newestY].flags & HAS_MONSTER)) {
1040                 tempMonst = monsterAtLoc(newestX, newestY);
1041                 if (tempMonst
1042                     && canSeeMonster(tempMonst)
1043                     && monstersAreEnemies(&player, tempMonst)
1044                     && tempMonst->creatureState != MONSTER_ALLY
1045                     && !(tempMonst->bookkeepingFlags & MB_IS_DYING)
1046                     && (!cellHasTerrainFlag(tempMonst->xLoc, tempMonst->yLoc, T_OBSTRUCTS_PASSABILITY) || (tempMonst->info.flags & MONST_ATTACKABLE_THRU_WALLS))) {
1047 
1048                     hitList[0] = tempMonst;
1049                     if (abortAttackAgainstAcidicTarget(hitList)) { // Acid mound attack confirmation.
1050                         brogueAssert(!committed);
1051                         cancelKeystroke();
1052                         rogue.disturbed = true;
1053                         return false;
1054                     }
1055                 }
1056             }
1057         }
1058         if (rogue.weapon && (rogue.weapon->flags & ITEM_PASS_ATTACKS)) {
1059             buildFlailHitList(x, y, newX, newY, hitList);
1060             if (abortAttackAgainstAcidicTarget(hitList)) { // Acid mound attack confirmation.
1061                 brogueAssert(!committed);
1062                 cancelKeystroke();
1063                 rogue.disturbed = true;
1064                 return false;
1065             }
1066         }
1067 
1068         if (player.status[STATUS_STUCK] && cellHasTerrainFlag(x, y, T_ENTANGLES)) {
1069                 // Don't interrupt exploration with this message.
1070             if (--player.status[STATUS_STUCK]) {
1071                 if (!rogue.automationActive) {
1072                     message("you struggle but cannot free yourself.", 0);
1073                 }
1074                 moveEntrancedMonsters(direction);
1075                 committed = true;
1076                 playerTurnEnded();
1077                 return true;
1078             } else {
1079                 if (!rogue.automationActive) {
1080                     message("you break free!", 0);
1081                 }
1082                 if (tileCatalog[pmap[x][y].layers[SURFACE]].flags & T_ENTANGLES) {
1083                     pmap[x][y].layers[SURFACE] = NOTHING;
1084                 }
1085             }
1086         }
1087 
1088         if (player.status[STATUS_NAUSEOUS]) {
1089             committed = true;
1090             if (rand_percent(25)) {
1091                 vomit(&player);
1092                 playerTurnEnded();
1093                 return true;
1094             }
1095         }
1096 
1097         // Are we taking the stairs?
1098         if (rogue.downLoc[0] == newX && rogue.downLoc[1] == newY) {
1099             committed = true;
1100             useStairs(1);
1101         } else if (rogue.upLoc[0] == newX && rogue.upLoc[1] == newY) {
1102             committed = true;
1103             useStairs(-1);
1104         } else {
1105             // Okay, we're finally moving!
1106             committed = true;
1107 
1108             player.xLoc += nbDirs[direction][0];
1109             player.yLoc += nbDirs[direction][1];
1110             pmap[x][y].flags &= ~HAS_PLAYER;
1111             pmap[player.xLoc][player.yLoc].flags |= HAS_PLAYER;
1112             pmap[player.xLoc][player.yLoc].flags &= ~IS_IN_PATH;
1113             if (defender && defender->creatureState == MONSTER_ALLY) { // Swap places with ally.
1114                 pmap[defender->xLoc][defender->yLoc].flags &= ~HAS_MONSTER;
1115                 defender->xLoc = x;
1116                 defender->yLoc = y;
1117                 if (monsterAvoids(defender, x, y)) {
1118                     getQualifyingPathLocNear(&(defender->xLoc), &(defender->yLoc), player.xLoc, player.yLoc, true, forbiddenFlagsForMonster(&(defender->info)), 0, 0, (HAS_PLAYER | HAS_MONSTER | HAS_STAIRS), false);
1119                 }
1120                 //getQualifyingLocNear(loc, player.xLoc, player.yLoc, true, NULL, forbiddenFlagsForMonster(&(defender->info)) & ~(T_IS_DF_TRAP | T_IS_DEEP_WATER | T_SPONTANEOUSLY_IGNITES), HAS_MONSTER, false, false);
1121                 //defender->xLoc = loc[0];
1122                 //defender->yLoc = loc[1];
1123                 pmap[defender->xLoc][defender->yLoc].flags |= HAS_MONSTER;
1124             }
1125 
1126             if (pmap[player.xLoc][player.yLoc].flags & HAS_ITEM) {
1127                 pickUpItemAt(player.xLoc, player.yLoc);
1128                 rogue.disturbed = true;
1129             }
1130             refreshDungeonCell(x, y);
1131             refreshDungeonCell(player.xLoc, player.yLoc);
1132             playerMoved = true;
1133 
1134             checkForMissingKeys(x, y);
1135             if (monsterShouldFall(&player)) {
1136                 player.bookkeepingFlags |= MB_IS_FALLING;
1137             }
1138             moveEntrancedMonsters(direction);
1139 
1140             // Perform a lunge or flail attack if appropriate.
1141             for (i=0; i<16; i++) {
1142                 if (hitList[i]) {
1143                     if (attack(&player, hitList[i], (rogue.weapon && (rogue.weapon->flags & ITEM_LUNGE_ATTACKS)))) {
1144                         anyAttackHit = true;
1145                     }
1146                 }
1147             }
1148             if (hitList[0]) {
1149                 playerRecoversFromAttacking(anyAttackHit);
1150             }
1151 
1152             playerTurnEnded();
1153         }
1154     } else if (cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_PASSABILITY)) {
1155         i = pmap[newX][newY].layers[layerWithFlag(newX, newY, T_OBSTRUCTS_PASSABILITY)];
1156         if ((tileCatalog[i].flags & T_OBSTRUCTS_PASSABILITY)
1157             && (!diagonalBlocked(x, y, newX, newY, false) || !cellHasTMFlag(newX, newY, TM_PROMOTES_WITH_KEY))) {
1158 
1159             if (!(pmap[newX][newY].flags & DISCOVERED)) {
1160                 committed = true;
1161                 discoverCell(newX, newY);
1162                 refreshDungeonCell(newX, newY);
1163             }
1164 
1165             messageWithColor(tileCatalog[i].flavorText, &backgroundMessageColor, 0);
1166         }
1167     }
1168     return playerMoved;
1169 }
1170 
1171 // replaced in Dijkstra.c:
1172 /*
1173 // returns true if the cell value changed
1174 boolean updateDistanceCell(short **distanceMap, short x, short y) {
1175     short dir, newX, newY;
1176     boolean somethingChanged = false;
1177 
1178     if (distanceMap[x][y] >= 0 && distanceMap[x][y] < 30000) {
1179         for (dir=0; dir< DIRECTION_COUNT; dir++) {
1180             newX = x + nbDirs[dir][0];
1181             newY = y + nbDirs[dir][1];
1182             if (coordinatesAreInMap(newX, newY)
1183                 && distanceMap[newX][newY] >= distanceMap[x][y] + 2
1184                 && !diagonalBlocked(x, y, newX, newY)) {
1185                 distanceMap[newX][newY] = distanceMap[x][y] + 1;
1186                 somethingChanged = true;
1187             }
1188         }
1189     }
1190     return somethingChanged;
1191 }
1192 
1193 void dijkstraScan(short **distanceMap, char passMap[DCOLS][DROWS], boolean allowDiagonals) {
1194     short i, j, maxDir;
1195     enum directions dir;
1196     boolean somethingChanged;
1197 
1198     maxDir = (allowDiagonals ? 8 : 4);
1199 
1200     do {
1201         somethingChanged = false;
1202         for (i=1; i<DCOLS-1; i++) {
1203             for (j=1; j<DROWS-1; j++) {
1204                 if (!passMap || passMap[i][j]) {
1205                     for (dir = 0; dir < maxDir; dir++) {
1206                         if (coordinatesAreInMap(i + nbDirs[dir][0], j + nbDirs[dir][1])
1207                             && (!passMap || passMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]])
1208                             && distanceMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]] >= distanceMap[i][j] + 2) {
1209                             distanceMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]] = distanceMap[i][j] + 1;
1210                             somethingChanged = true;
1211                         }
1212                     }
1213                 }
1214             }
1215         }
1216 
1217 
1218         for (i = DCOLS - 1; i >= 0; i--) {
1219             for (j = DROWS - 1; j >= 0; j--) {
1220                 if (!passMap || passMap[i][j]) {
1221                     for (dir = 0; dir < maxDir; dir++) {
1222                         if (coordinatesAreInMap(i + nbDirs[dir][0], j + nbDirs[dir][1])
1223                             && (!passMap || passMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]])
1224                             && distanceMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]] >= distanceMap[i][j] + 2) {
1225                             distanceMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]] = distanceMap[i][j] + 1;
1226                             somethingChanged = true;
1227                         }
1228                     }
1229                 }
1230             }
1231         }
1232     } while (somethingChanged);
1233 }*/
1234 
1235 /*void enqueue(short x, short y, short val, distanceQueue *dQ) {
1236     short *qX2, *qY2, *qVal2;
1237 
1238     // if we need to allocate more memory:
1239     if (dQ->qLen + 1 > dQ->qMaxLen) {
1240         dQ->qMaxLen *= 2;
1241         qX2 = realloc(dQ->qX, dQ->qMaxLen);
1242         if (qX2) {
1243             free(dQ->qX);
1244             dQ->qX = qX2;
1245         } else {
1246             // out of memory
1247         }
1248         qY2 = realloc(dQ->qY, dQ->qMaxLen);
1249         if (qY2) {
1250             free(dQ->qY);
1251             dQ->qY = qY2;
1252         } else {
1253             // out of memory
1254         }
1255         qVal2 = realloc(dQ->qVal, dQ->qMaxLen);
1256         if (qVal2) {
1257             free(dQ->qVal);
1258             dQ->qVal = qVal2;
1259         } else {
1260             // out of memory
1261         }
1262     }
1263 
1264     dQ->qX[dQ->qLen] = x;
1265     dQ->qY[dQ->qLen] = y;
1266     (dQ->qVal)[dQ->qLen] = val;
1267 
1268     dQ->qLen++;
1269 
1270     if (val < dQ->qMinVal) {
1271         dQ->qMinVal = val;
1272         dQ->qMinCount = 1;
1273     } else if (val == dQ->qMinVal) {
1274         dQ->qMinCount++;
1275     }
1276 }
1277 
1278 void updateQueueMinCache(distanceQueue *dQ) {
1279     short i;
1280     dQ->qMinCount = 0;
1281     dQ->qMinVal = 30001;
1282     for (i = 0; i < dQ->qLen; i++) {
1283         if (dQ->qVal[i] < dQ->qMinVal) {
1284             dQ->qMinVal = dQ->qVal[i];
1285             dQ->qMinCount = 1;
1286         } else if (dQ->qVal[i] == dQ->qMinVal) {
1287             dQ->qMinCount++;
1288         }
1289     }
1290 }
1291 
1292 // removes the lowest value from the queue, populates x/y/value variables and updates min caching
1293 void dequeue(short *x, short *y, short *val, distanceQueue *dQ) {
1294     short i, minIndex;
1295 
1296     if (dQ->qMinCount <= 0) {
1297         updateQueueMinCache(dQ);
1298     }
1299 
1300     *val = dQ->qMinVal;
1301 
1302     // find the last instance of the minVal
1303     for (minIndex = dQ->qLen - 1; minIndex >= 0 && dQ->qVal[minIndex] != *val; minIndex--);
1304 
1305     // populate the return variables
1306     *x = dQ->qX[minIndex];
1307     *y = dQ->qY[minIndex];
1308 
1309     dQ->qLen--;
1310 
1311     // delete the minValue queue entry
1312     for (i = minIndex; i < dQ->qLen; i++) {
1313         dQ->qX[i] = dQ->qX[i+1];
1314         dQ->qY[i] = dQ->qY[i+1];
1315         dQ->qVal[i] = dQ->qVal[i+1];
1316     }
1317 
1318     // update min values
1319     dQ->qMinCount--;
1320     if (!dQ->qMinCount && dQ->qLen) {
1321         updateQueueMinCache(dQ);
1322     }
1323 
1324 }
1325 
1326 void dijkstraScan(short **distanceMap, char passMap[DCOLS][DROWS], boolean allowDiagonals) {
1327     short i, j, maxDir, val;
1328     enum directions dir;
1329     distanceQueue dQ;
1330 
1331     dQ.qMaxLen = DCOLS * DROWS * 1.5;
1332     dQ.qX = (short *) malloc(dQ.qMaxLen * sizeof(short));
1333     dQ.qY = (short *) malloc(dQ.qMaxLen * sizeof(short));
1334     dQ.qVal = (short *) malloc(dQ.qMaxLen * sizeof(short));
1335     dQ.qLen = 0;
1336     dQ.qMinVal = 30000;
1337     dQ.qMinCount = 0;
1338 
1339     maxDir = (allowDiagonals ? 8 : 4);
1340 
1341     // seed the queue with the entire map
1342     for (i=0; i<DCOLS; i++) {
1343         for (j=0; j<DROWS; j++) {
1344             if (!passMap || passMap[i][j]) {
1345                 enqueue(i, j, distanceMap[i][j], &dQ);
1346             }
1347         }
1348     }
1349 
1350     // iterate through queue updating lowest entries until the queue is empty
1351     while (dQ.qLen) {
1352         dequeue(&i, &j, &val, &dQ);
1353         if (distanceMap[i][j] == val) { // if it hasn't been improved since joining the queue
1354             for (dir = 0; dir < maxDir; dir++) {
1355                 if (coordinatesAreInMap(i + nbDirs[dir][0], j + nbDirs[dir][1])
1356                     && (!passMap || passMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]])
1357                     && distanceMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]] >= distanceMap[i][j] + 2) {
1358 
1359                     distanceMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]] = distanceMap[i][j] + 1;
1360 
1361                     enqueue(i + nbDirs[dir][0], j + nbDirs[dir][1], distanceMap[i][j] + 1, &dQ);
1362                 }
1363             }
1364         }
1365     }
1366 
1367     free(dQ.qX);
1368     free(dQ.qY);
1369     free(dQ.qVal);
1370 }*/
1371 
1372 /*
1373 void calculateDistances(short **distanceMap, short destinationX, short destinationY, unsigned long blockingTerrainFlags, creature *traveler) {
1374     short i, j;
1375     boolean somethingChanged;
1376 
1377     for (i=0; i<DCOLS; i++) {
1378         for (j=0; j<DROWS; j++) {
1379             distanceMap[i][j] = ((traveler && traveler == &player && !(pmap[i][j].flags & (DISCOVERED | MAGIC_MAPPED)))
1380                                  || ((traveler && monsterAvoids(traveler, i, j))
1381                                      || cellHasTerrainFlag(i, j, blockingTerrainFlags))) ? -1 : 30000;
1382         }
1383     }
1384 
1385     distanceMap[destinationX][destinationY] = 0;
1386 
1387 //  dijkstraScan(distanceMap);
1388     do {
1389         somethingChanged = false;
1390         for (i=0; i<DCOLS; i++) {
1391             for (j=0; j<DROWS; j++) {
1392                 if (updateDistanceCell(distanceMap, i, j)) {
1393                     somethingChanged = true;
1394                 }
1395             }
1396         }
1397 
1398 
1399         for (i = DCOLS - 1; i >= 0; i--) {
1400             for (j = DROWS - 1; j >= 0; j--) {
1401                 if (updateDistanceCell(distanceMap, i, j)) {
1402                     somethingChanged = true;
1403                 }
1404             }
1405         }
1406     } while (somethingChanged);
1407 }*/
1408 
1409 // Returns -1 if there are no beneficial moves.
1410 // If preferDiagonals is true, we will prefer diagonal moves.
1411 // Always rolls downhill on the distance map.
1412 // If monst is provided, do not return a direction pointing to
1413 // a cell that the monster avoids.
nextStep(short ** distanceMap,short x,short y,creature * monst,boolean preferDiagonals)1414 short nextStep(short **distanceMap, short x, short y, creature *monst, boolean preferDiagonals) {
1415     short newX, newY, bestScore;
1416     enum directions dir, bestDir;
1417     creature *blocker;
1418     boolean blocked;
1419 
1420     brogueAssert(coordinatesAreInMap(x, y));
1421 
1422     bestScore = 0;
1423     bestDir = NO_DIRECTION;
1424 
1425     for (dir = (preferDiagonals ? 7 : 0);
1426          (preferDiagonals ? dir >= 0 : dir < DIRECTION_COUNT);
1427          (preferDiagonals ? dir-- : dir++)) {
1428 
1429         newX = x + nbDirs[dir][0];
1430         newY = y + nbDirs[dir][1];
1431 
1432         brogueAssert(coordinatesAreInMap(newX, newY));
1433         if (coordinatesAreInMap(newX, newY)) {
1434             blocked = false;
1435             blocker = monsterAtLoc(newX, newY);
1436             if (monst
1437                 && monsterAvoids(monst, newX, newY)) {
1438 
1439                 blocked = true;
1440             } else if (monst
1441                        && blocker
1442                        && !canPass(monst, blocker)
1443                        && !monstersAreTeammates(monst, blocker)
1444                        && !monstersAreEnemies(monst, blocker)) {
1445                 blocked = true;
1446             }
1447             if ((distanceMap[x][y] - distanceMap[newX][newY]) > bestScore
1448                 && !diagonalBlocked(x, y, newX, newY, monst == &player)
1449                 && knownToPlayerAsPassableOrSecretDoor(newX, newY)
1450                 && !blocked) {
1451 
1452                 bestDir = dir;
1453                 bestScore = distanceMap[x][y] - distanceMap[newX][newY];
1454             }
1455         }
1456     }
1457     return bestDir;
1458 }
1459 
displayRoute(short ** distanceMap,boolean removeRoute)1460 void displayRoute(short **distanceMap, boolean removeRoute) {
1461     short currentX = player.xLoc, currentY = player.yLoc, dir, newX, newY;
1462     boolean advanced;
1463 
1464     if (distanceMap[player.xLoc][player.yLoc] < 0 || distanceMap[player.xLoc][player.yLoc] == 30000) {
1465         return;
1466     }
1467     do {
1468         if (removeRoute) {
1469             refreshDungeonCell(currentX, currentY);
1470         } else {
1471             hiliteCell(currentX, currentY, &hiliteColor, 50, true);
1472         }
1473         advanced = false;
1474         for (dir = 7; dir >= 0; dir--) {
1475             newX = currentX + nbDirs[dir][0];
1476             newY = currentY + nbDirs[dir][1];
1477             if (coordinatesAreInMap(newX, newY)
1478                 && distanceMap[newX][newY] >= 0 && distanceMap[newX][newY] < distanceMap[currentX][currentY]
1479                 && !diagonalBlocked(currentX, currentY, newX, newY, true)) {
1480 
1481                 currentX = newX;
1482                 currentY = newY;
1483                 advanced = true;
1484                 break;
1485             }
1486         }
1487     } while (advanced);
1488 }
1489 
travelRoute(short path[1000][2],short steps)1490 void travelRoute(short path[1000][2], short steps) {
1491     short i, j;
1492     short dir;
1493 
1494     brogueAssert(!rogue.playbackMode);
1495 
1496     rogue.disturbed = false;
1497     rogue.automationActive = true;
1498 
1499     for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
1500         creature *monst = nextCreature(&it);
1501         if (canSeeMonster(monst)) {
1502             monst->bookkeepingFlags |= MB_ALREADY_SEEN;
1503         } else {
1504             monst->bookkeepingFlags &= ~MB_ALREADY_SEEN;
1505         }
1506     }
1507 
1508     for (i=0; i < steps && !rogue.disturbed; i++) {
1509         for (j = i + 1; j < steps - 1; j++) {
1510             // Check to see if the path has become obstructed or avoided since the last time we saw it.
1511             if (diagonalBlocked(path[j-1][0], path[j-1][1], path[j][0], path[j][1], true)
1512                 || monsterAvoids(&player, path[j][0], path[j][1])) {
1513 
1514                 rogue.disturbed = true;
1515                 break;
1516             }
1517         }
1518         for (dir = 0; dir < DIRECTION_COUNT && !rogue.disturbed; dir++) {
1519             if (player.xLoc + nbDirs[dir][0] == path[i][0]
1520                 && player.yLoc + nbDirs[dir][1] == path[i][1]) {
1521 
1522                 if (!playerMoves(dir)) {
1523                     rogue.disturbed = true;
1524                 }
1525                 if (pauseBrogue(25)) {
1526                     rogue.disturbed = true;
1527                 }
1528                 break;
1529             }
1530         }
1531     }
1532     rogue.disturbed = true;
1533     rogue.automationActive = false;
1534     updateFlavorText();
1535 }
1536 
travelMap(short ** distanceMap)1537 void travelMap(short **distanceMap) {
1538     short currentX = player.xLoc, currentY = player.yLoc, dir, newX, newY;
1539     boolean advanced;
1540 
1541     rogue.disturbed = false;
1542     rogue.automationActive = true;
1543 
1544     if (distanceMap[player.xLoc][player.yLoc] < 0 || distanceMap[player.xLoc][player.yLoc] == 30000) {
1545         return;
1546     }
1547     do {
1548         advanced = false;
1549         for (dir = 7; dir >= 0; dir--) {
1550             newX = currentX + nbDirs[dir][0];
1551             newY = currentY + nbDirs[dir][1];
1552             if (coordinatesAreInMap(newX, newY)
1553                 && distanceMap[newX][newY] >= 0
1554                 && distanceMap[newX][newY] < distanceMap[currentX][currentY]
1555                 && !diagonalBlocked(currentX, currentY, newX, newY, true)) {
1556 
1557                 if (!playerMoves(dir)) {
1558                     rogue.disturbed = true;
1559                 }
1560                 if (pauseBrogue(500)) {
1561                     rogue.disturbed = true;
1562                 }
1563                 currentX = newX;
1564                 currentY = newY;
1565                 advanced = true;
1566                 break;
1567             }
1568         }
1569     } while (advanced && !rogue.disturbed);
1570     rogue.disturbed = true;
1571     rogue.automationActive = false;
1572     updateFlavorText();
1573 }
1574 
travel(short x,short y,boolean autoConfirm)1575 void travel(short x, short y, boolean autoConfirm) {
1576     short **distanceMap, i;
1577     rogueEvent theEvent;
1578     unsigned short staircaseConfirmKey;
1579 
1580     confirmMessages();
1581 
1582     if (D_WORMHOLING) {
1583         recordMouseClick(mapToWindowX(x), mapToWindowY(y), true, false);
1584         pmap[player.xLoc][player.yLoc].flags &= ~HAS_PLAYER;
1585         refreshDungeonCell(player.xLoc, player.yLoc);
1586         player.xLoc = x;
1587         player.yLoc = y;
1588         pmap[x][y].flags |= HAS_PLAYER;
1589         updatePlayerUnderwaterness();
1590         refreshDungeonCell(x, y);
1591         updateVision(true);
1592         return;
1593     }
1594 
1595     if (abs(player.xLoc - x) + abs(player.yLoc - y) == 1) {
1596         // targeting a cardinal neighbor
1597         for (i=0; i<4; i++) {
1598             if (nbDirs[i][0] == (x - player.xLoc) && nbDirs[i][1] == (y - player.yLoc)) {
1599                 playerMoves(i);
1600                 break;
1601             }
1602         }
1603         return;
1604     }
1605 
1606     if (!(pmap[x][y].flags & (DISCOVERED | MAGIC_MAPPED))) {
1607         message("You have not explored that location.", 0);
1608         return;
1609     }
1610 
1611     distanceMap = allocGrid();
1612 
1613     calculateDistances(distanceMap, x, y, 0, &player, false, false);
1614     if (distanceMap[player.xLoc][player.yLoc] < 30000) {
1615         if (autoConfirm) {
1616             travelMap(distanceMap);
1617             //refreshSideBar(-1, -1, false);
1618         } else {
1619             if (rogue.upLoc[0] == x && rogue.upLoc[1] == y) {
1620                 staircaseConfirmKey = ASCEND_KEY;
1621             } else if (rogue.downLoc[0] == x && rogue.downLoc[1] == y) {
1622                 staircaseConfirmKey = DESCEND_KEY;
1623             } else {
1624                 staircaseConfirmKey = 0;
1625             }
1626             displayRoute(distanceMap, false);
1627             message("Travel this route? (y/n)", 0);
1628 
1629             do {
1630                 nextBrogueEvent(&theEvent, true, false, false);
1631             } while (theEvent.eventType != MOUSE_UP && theEvent.eventType != KEYSTROKE);
1632 
1633             displayRoute(distanceMap, true); // clear route display
1634             confirmMessages();
1635 
1636             if ((theEvent.eventType == MOUSE_UP && windowToMapX(theEvent.param1) == x && windowToMapY(theEvent.param2) == y)
1637                 || (theEvent.eventType == KEYSTROKE && (theEvent.param1 == 'Y' || theEvent.param1 == 'y'
1638                                                         || theEvent.param1 == RETURN_KEY
1639                                                         || (theEvent.param1 == staircaseConfirmKey
1640                                                             && theEvent.param1 != 0)))) {
1641                 travelMap(distanceMap);
1642                 //refreshSideBar(-1, -1, false);
1643                 commitDraws();
1644             } else if (theEvent.eventType == MOUSE_UP) {
1645                 executeMouseClick(&theEvent);
1646             }
1647         }
1648 //      if (player.xLoc == x && player.yLoc == y) {
1649 //          rogue.cursorLoc[0] = rogue.cursorLoc[1] = 0;
1650 //      } else {
1651 //          rogue.cursorLoc[0] = x;
1652 //          rogue.cursorLoc[1] = y;
1653 //      }
1654     } else {
1655         rogue.cursorLoc[0] = rogue.cursorLoc[1] = -1;
1656         message("No path is available.", 0);
1657     }
1658     freeGrid(distanceMap);
1659 }
1660 
populateGenericCostMap(short ** costMap)1661 void populateGenericCostMap(short **costMap) {
1662     short i, j;
1663 
1664     for (i=0; i<DCOLS; i++) {
1665         for (j=0; j<DROWS; j++) {
1666             if (cellHasTerrainFlag(i, j, T_OBSTRUCTS_PASSABILITY)
1667                 && (!cellHasTMFlag(i, j, TM_IS_SECRET) || (discoveredTerrainFlagsAtLoc(i, j) & T_OBSTRUCTS_PASSABILITY))) {
1668 
1669                 costMap[i][j] = cellHasTerrainFlag(i, j, T_OBSTRUCTS_DIAGONAL_MOVEMENT) ? PDS_OBSTRUCTION : PDS_FORBIDDEN;
1670             } else if (cellHasTerrainFlag(i, j, T_PATHING_BLOCKER & ~T_OBSTRUCTS_PASSABILITY)) {
1671                 costMap[i][j] = PDS_FORBIDDEN;
1672             } else {
1673                 costMap[i][j] = 1;
1674             }
1675         }
1676     }
1677 }
1678 
getLocationFlags(const short x,const short y,unsigned long * tFlags,unsigned long * TMFlags,unsigned long * cellFlags,const boolean limitToPlayerKnowledge)1679 void getLocationFlags(const short x, const short y,
1680                       unsigned long *tFlags, unsigned long *TMFlags, unsigned long *cellFlags,
1681                       const boolean limitToPlayerKnowledge) {
1682     if (limitToPlayerKnowledge
1683         && (pmap[x][y].flags & (DISCOVERED | MAGIC_MAPPED))
1684         && !playerCanSee(x, y)) {
1685 
1686         if (tFlags) {
1687             *tFlags = pmap[x][y].rememberedTerrainFlags;
1688         }
1689         if (TMFlags) {
1690             *TMFlags = pmap[x][y].rememberedTMFlags;
1691         }
1692         if (cellFlags) {
1693             *cellFlags = pmap[x][y].rememberedCellFlags;
1694         }
1695     } else {
1696         if (tFlags) {
1697             *tFlags = terrainFlags(x, y);
1698         }
1699         if (TMFlags) {
1700             *TMFlags = terrainMechFlags(x, y);
1701         }
1702         if (cellFlags) {
1703             *cellFlags = pmap[x][y].flags;
1704         }
1705     }
1706 }
1707 
populateCreatureCostMap(short ** costMap,creature * monst)1708 void populateCreatureCostMap(short **costMap, creature *monst) {
1709     short i, j, unexploredCellCost;
1710     creature *currentTenant;
1711     item *theItem;
1712     unsigned long tFlags, cFlags;
1713 
1714     unexploredCellCost = 10 + (clamp(rogue.depthLevel, 5, 15) - 5) * 2;
1715 
1716     for (i=0; i<DCOLS; i++) {
1717         for (j=0; j<DROWS; j++) {
1718             if (monst == &player && !(pmap[i][j].flags & (DISCOVERED | MAGIC_MAPPED))) {
1719                 costMap[i][j] = PDS_OBSTRUCTION;
1720                 continue;
1721             }
1722 
1723             getLocationFlags(i, j, &tFlags, NULL, &cFlags, monst == &player);
1724 
1725             if ((tFlags & T_OBSTRUCTS_PASSABILITY)
1726                  && (!cellHasTMFlag(i, j, TM_IS_SECRET) || (discoveredTerrainFlagsAtLoc(i, j) & T_OBSTRUCTS_PASSABILITY) || monst == &player)) {
1727 
1728                 costMap[i][j] = (tFlags & T_OBSTRUCTS_DIAGONAL_MOVEMENT) ? PDS_OBSTRUCTION : PDS_FORBIDDEN;
1729                 continue;
1730             }
1731 
1732             if ((tFlags & T_LAVA_INSTA_DEATH)
1733                 && !(monst->info.flags & (MONST_IMMUNE_TO_FIRE | MONST_FLIES | MONST_INVULNERABLE))
1734                 && (monst->status[STATUS_LEVITATING] || monst->status[STATUS_IMMUNE_TO_FIRE])
1735                 && max(monst->status[STATUS_LEVITATING], monst->status[STATUS_IMMUNE_TO_FIRE]) < (rogue.mapToShore[i][j] + distanceBetween(i, j, monst->xLoc, monst->yLoc) * monst->movementSpeed / 100)) {
1736                 // Only a temporary effect will permit the monster to survive the lava, and the remaining duration either isn't
1737                 // enough to get it to the spot, or it won't suffice to let it return to shore if it does get there.
1738                 // Treat these locations as obstacles.
1739                 costMap[i][j] = PDS_FORBIDDEN;
1740                 continue;
1741             }
1742 
1743             if (((tFlags & T_AUTO_DESCENT) || (tFlags & T_IS_DEEP_WATER) && !(monst->info.flags & MONST_IMMUNE_TO_WATER))
1744                 && !(monst->info.flags & MONST_FLIES)
1745                 && (monst->status[STATUS_LEVITATING])
1746                 && monst->status[STATUS_LEVITATING] < (rogue.mapToShore[i][j] + distanceBetween(i, j, monst->xLoc, monst->yLoc) * monst->movementSpeed / 100)) {
1747                 // Only a temporary effect will permit the monster to levitate over the chasm/water, and the remaining duration either isn't
1748                 // enough to get it to the spot, or it won't suffice to let it return to shore if it does get there.
1749                 // Treat these locations as obstacles.
1750                 costMap[i][j] = PDS_FORBIDDEN;
1751                 continue;
1752             }
1753 
1754             if (monsterAvoids(monst, i, j)) {
1755                 costMap[i][j] = PDS_FORBIDDEN;
1756                 continue;
1757             }
1758 
1759             if (cFlags & HAS_MONSTER) {
1760                 currentTenant = monsterAtLoc(i, j);
1761                 if (currentTenant
1762                     && (currentTenant->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE))
1763                     && !canPass(monst, currentTenant)) {
1764 
1765                     costMap[i][j] = PDS_FORBIDDEN;
1766                     continue;
1767                 }
1768             }
1769 
1770             if ((cFlags & KNOWN_TO_BE_TRAP_FREE)
1771                 || (monst != &player && monst->creatureState != MONSTER_ALLY)) {
1772 
1773                 costMap[i][j] = 10;
1774             } else {
1775                 // Player and allies give locations that are known to be free of traps
1776                 // an advantage that increases with depth level, based on the depths
1777                 // at which traps are generated.
1778                 costMap[i][j] = unexploredCellCost;
1779             }
1780 
1781             if (!(monst->info.flags & MONST_INVULNERABLE)) {
1782                 if ((tFlags & T_CAUSES_NAUSEA)
1783                     || cellHasTMFlag(i, j, TM_PROMOTES_ON_ITEM_PICKUP)
1784                     || (tFlags & T_ENTANGLES) && !(monst->info.flags & MONST_IMMUNE_TO_WEBS)) {
1785 
1786                     costMap[i][j] += 20;
1787                 }
1788             }
1789 
1790             if (monst == &player) {
1791                 theItem = itemAtLoc(i, j);
1792                 if (theItem && (theItem->flags & ITEM_PLAYER_AVOIDS)) {
1793                     costMap[i][j] += 10;
1794                 }
1795             }
1796         }
1797     }
1798 }
1799 
adjacentFightingDir()1800 enum directions adjacentFightingDir() {
1801     short newX, newY;
1802     enum directions dir;
1803     creature *monst;
1804 
1805     if (cellHasTerrainFlag(player.xLoc, player.yLoc, T_OBSTRUCTS_PASSABILITY)) {
1806         return NO_DIRECTION;
1807     }
1808     for (dir = 0; dir < DIRECTION_COUNT; dir++) {
1809         newX = player.xLoc + nbDirs[dir][0];
1810         newY = player.yLoc + nbDirs[dir][1];
1811         monst = monsterAtLoc(newX, newY);
1812         if (monst
1813             && canSeeMonster(monst)
1814             && (!diagonalBlocked(player.xLoc, player.yLoc, newX, newY, false) || (monst->info.flags & MONST_ATTACKABLE_THRU_WALLS))
1815             && monstersAreEnemies(&player, monst)
1816             && !(monst->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE))) {
1817 
1818             return dir;
1819         }
1820     }
1821     return NO_DIRECTION;
1822 }
1823 
1824 #define exploreGoalValue(x, y)  (0 - abs((x) - DCOLS / 2) / 3 - abs((x) - DCOLS / 2) / 4)
1825 
getExploreMap(short ** map,boolean headingToStairs)1826 void getExploreMap(short **map, boolean headingToStairs) {// calculate explore map
1827     short i, j;
1828     short **costMap;
1829     item *theItem;
1830 
1831     costMap = allocGrid();
1832     populateCreatureCostMap(costMap, &player);
1833 
1834     for (i=0; i<DCOLS; i++) {
1835         for (j=0; j<DROWS; j++) {
1836             map[i][j] = 30000; // Can be overridden later.
1837             theItem = itemAtLoc(i, j);
1838             if (!(pmap[i][j].flags & DISCOVERED)) {
1839                 if ((pmap[i][j].flags & MAGIC_MAPPED)
1840                     && (tileCatalog[pmap[i][j].layers[DUNGEON]].flags | tileCatalog[pmap[i][j].layers[LIQUID]].flags) & T_PATHING_BLOCKER) {
1841                     // Magic-mapped cells revealed as obstructions should be treated as such even though they're not discovered.
1842                     costMap[i][j] = cellHasTerrainFlag(i, j, T_OBSTRUCTS_DIAGONAL_MOVEMENT) ? PDS_OBSTRUCTION : PDS_FORBIDDEN;
1843                 } else {
1844                     costMap[i][j] = 1;
1845                     map[i][j] = exploreGoalValue(i, j);
1846                 }
1847             } else if (theItem
1848                        && !monsterAvoids(&player, i, j)) {
1849                 if (theItem->flags & ITEM_PLAYER_AVOIDS) {
1850                     costMap[i][j] = 20;
1851                 } else {
1852                     costMap[i][j] = 1;
1853                     map[i][j] = exploreGoalValue(i, j) - 10;
1854                 }
1855             }
1856         }
1857     }
1858 
1859     costMap[rogue.downLoc[0]][rogue.downLoc[1]] = 100;
1860     costMap[rogue.upLoc[0]][rogue.upLoc[1]]     = 100;
1861 
1862     if (headingToStairs) {
1863         map[rogue.downLoc[0]][rogue.downLoc[1]] = 0; // head to the stairs
1864     }
1865 
1866     dijkstraScan(map, costMap, true);
1867 
1868     //displayGrid(costMap);
1869     freeGrid(costMap);
1870 }
1871 
explore(short frameDelay)1872 boolean explore(short frameDelay) {
1873     short **distanceMap;
1874     short path[1000][2], steps;
1875     boolean madeProgress, headingToStairs;
1876     enum directions dir;
1877 
1878     // Explore commands should never be written to a recording.
1879     // Instead, the elemental movement commands that compose it
1880     // should be written individually.
1881     brogueAssert(!rogue.playbackMode);
1882 
1883     clearCursorPath();
1884 
1885     madeProgress    = false;
1886     headingToStairs = false;
1887 
1888     if (player.status[STATUS_CONFUSED]) {
1889         message("Not while you're confused.", 0);
1890         return false;
1891     }
1892     if (cellHasTerrainFlag(player.xLoc, player.yLoc, T_OBSTRUCTS_PASSABILITY)) {
1893         message("Not while you're trapped.", 0);
1894         return false;
1895     }
1896 
1897     for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
1898         creature *monst = nextCreature(&it);
1899         if (canSeeMonster(monst)) {
1900             monst->bookkeepingFlags |= MB_ALREADY_SEEN;
1901         } else {
1902             monst->bookkeepingFlags &= ~MB_ALREADY_SEEN;
1903         }
1904     }
1905 
1906     // fight any adjacent enemies
1907     dir = adjacentFightingDir();
1908     if (dir != NO_DIRECTION
1909         && startFighting(dir, (player.status[STATUS_HALLUCINATING] ? true : false))) {
1910 
1911         return true;
1912     }
1913 
1914     if (!rogue.autoPlayingLevel) {
1915         message(KEYBOARD_LABELS ? "Exploring... press any key to stop." : "Exploring... touch anywhere to stop.",
1916                 0);
1917         // A little hack so the exploring message remains bright while exploring and then auto-dims when
1918         // another message is displayed:
1919         confirmMessages();
1920         printString(KEYBOARD_LABELS ? "Exploring... press any key to stop." : "Exploring... touch anywhere to stop.",
1921                     mapToWindowX(0), mapToWindowY(-1), &white, &black, NULL);
1922     }
1923     rogue.disturbed = false;
1924     rogue.automationActive = true;
1925 
1926     distanceMap = allocGrid();
1927     do {
1928         // fight any adjacent enemies
1929         dir = adjacentFightingDir();
1930         if (dir != NO_DIRECTION) {
1931             startFighting(dir, (player.status[STATUS_HALLUCINATING] ? true : false));
1932             if (rogue.disturbed) {
1933                 madeProgress = true;
1934                 continue;
1935             }
1936         }
1937         if (rogue.disturbed) {
1938             continue;
1939         }
1940 
1941         getExploreMap(distanceMap, headingToStairs);
1942 
1943         // hilite path
1944         steps = getPlayerPathOnMap(path, distanceMap, player.xLoc, player.yLoc);
1945         hilitePath(path, steps, false);
1946 
1947         // take a step
1948         dir = nextStep(distanceMap, player.xLoc, player.yLoc, NULL, false);
1949 
1950         if (!headingToStairs && rogue.autoPlayingLevel && dir == NO_DIRECTION) {
1951             headingToStairs = true;
1952             continue;
1953         }
1954 
1955         refreshSideBar(-1, -1, false);
1956 
1957         if (dir == NO_DIRECTION) {
1958             rogue.disturbed = true;
1959         } else if (!playerMoves(dir)) {
1960             rogue.disturbed = true;
1961         } else {
1962             madeProgress = true;
1963             if (pauseBrogue(frameDelay)) {
1964                 rogue.disturbed = true;
1965                 rogue.autoPlayingLevel = false;
1966             }
1967         }
1968         hilitePath(path, steps, true);
1969     } while (!rogue.disturbed);
1970     //clearCursorPath();
1971     rogue.automationActive = false;
1972     refreshSideBar(-1, -1, false);
1973     freeGrid(distanceMap);
1974     return madeProgress;
1975 }
1976 
autoPlayLevel(boolean fastForward)1977 void autoPlayLevel(boolean fastForward) {
1978     boolean madeProgress;
1979 
1980     rogue.autoPlayingLevel = true;
1981 
1982     confirmMessages();
1983     message(KEYBOARD_LABELS ? "Playing... press any key to stop." : "Playing... touch anywhere to stop.", 0);
1984 
1985     // explore until we are not making progress
1986     do {
1987         madeProgress = explore(fastForward ? 1 : 50);
1988         //refreshSideBar(-1, -1, false);
1989 
1990         if (!madeProgress && rogue.downLoc[0] == player.xLoc && rogue.downLoc[1] == player.yLoc) {
1991             useStairs(1);
1992             madeProgress = true;
1993         }
1994     } while (madeProgress && rogue.autoPlayingLevel);
1995 
1996     confirmMessages();
1997 
1998     rogue.autoPlayingLevel = false;
1999 }
2000 
directionOfKeypress(unsigned short ch)2001 short directionOfKeypress(unsigned short ch) {
2002     switch (ch) {
2003         case LEFT_KEY:
2004         case LEFT_ARROW:
2005         case NUMPAD_4:
2006             return LEFT;
2007         case RIGHT_KEY:
2008         case RIGHT_ARROW:
2009         case NUMPAD_6:
2010             return RIGHT;
2011         case UP_KEY:
2012         case UP_ARROW:
2013         case NUMPAD_8:
2014             return UP;
2015         case DOWN_KEY:
2016         case DOWN_ARROW:
2017         case NUMPAD_2:
2018             return DOWN;
2019         case UPLEFT_KEY:
2020         case NUMPAD_7:
2021             return UPLEFT;
2022         case UPRIGHT_KEY:
2023         case NUMPAD_9:
2024             return UPRIGHT;
2025         case DOWNLEFT_KEY:
2026         case NUMPAD_1:
2027             return DOWNLEFT;
2028         case DOWNRIGHT_KEY:
2029         case NUMPAD_3:
2030             return DOWNRIGHT;
2031         default:
2032             return -1;
2033     }
2034 }
2035 
startFighting(enum directions dir,boolean tillDeath)2036 boolean startFighting(enum directions dir, boolean tillDeath) {
2037     short x, y, expectedDamage;
2038     creature *monst;
2039 
2040     x = player.xLoc + nbDirs[dir][0];
2041     y = player.yLoc + nbDirs[dir][1];
2042     monst = monsterAtLoc(x, y);
2043     if (monst->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE)) {
2044         return false;
2045     }
2046     expectedDamage = monst->info.damage.upperBound * monsterDamageAdjustmentAmount(monst) / FP_FACTOR;
2047     if (rogue.easyMode) {
2048         expectedDamage /= 5;
2049     }
2050     rogue.blockCombatText = true;
2051     rogue.disturbed = false;
2052     do {
2053         if (!playerMoves(dir)) {
2054             break;
2055         }
2056         if (pauseBrogue(1)) {
2057             break;
2058         }
2059     } while (!rogue.disturbed && !rogue.gameHasEnded && (tillDeath || player.currentHP > expectedDamage)
2060              && (pmap[x][y].flags & HAS_MONSTER) && monsterAtLoc(x, y) == monst);
2061 
2062     rogue.blockCombatText = false;
2063     return rogue.disturbed;
2064 }
2065 
isDisturbed(short x,short y)2066 boolean isDisturbed(short x, short y) {
2067     short i;
2068     creature *monst;
2069     for (i=0; i< DIRECTION_COUNT; i++) {
2070         monst = monsterAtLoc(x + nbDirs[i][0], y + nbDirs[i][1]);
2071         if (pmap[x + nbDirs[i][0]][y + nbDirs[i][1]].flags & (HAS_ITEM)) {
2072             // Do not trigger for submerged or invisible or unseen monsters.
2073             return true;
2074         }
2075         if (monst
2076             && !(monst->creatureState == MONSTER_ALLY)
2077             && (canSeeMonster(monst) || monsterRevealed(monst))) {
2078             // Do not trigger for submerged or invisible or unseen monsters.
2079             return true;
2080         }
2081     }
2082     return false;
2083 }
2084 
discover(short x,short y)2085 void discover(short x, short y) {
2086     enum dungeonLayers layer;
2087     dungeonFeature *feat;
2088     if (cellHasTMFlag(x, y, TM_IS_SECRET)) {
2089 
2090         for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
2091             if (tileCatalog[pmap[x][y].layers[layer]].mechFlags & TM_IS_SECRET) {
2092                 feat = &dungeonFeatureCatalog[tileCatalog[pmap[x][y].layers[layer]].discoverType];
2093                 pmap[x][y].layers[layer] = (layer == DUNGEON ? FLOOR : NOTHING);
2094                 spawnDungeonFeature(x, y, feat, true, false);
2095             }
2096         }
2097         refreshDungeonCell(x, y);
2098 
2099         if (playerCanSee(x, y)) {
2100             rogue.disturbed = true;
2101         }
2102     }
2103 }
2104 
2105 // returns true if found anything
search(short searchStrength)2106 boolean search(short searchStrength) {
2107     short i, j, radius, x, y, percent;
2108     boolean foundSomething = false;
2109 
2110     radius = searchStrength / 10;
2111     x = player.xLoc;
2112     y = player.yLoc;
2113 
2114     for (i = x - radius; i <= x + radius; i++) {
2115         for (j = y - radius; j <= y + radius; j++) {
2116             if (coordinatesAreInMap(i, j)
2117                 && playerCanDirectlySee(i, j)) {
2118 
2119                 percent = searchStrength - distanceBetween(x, y, i, j) * 10;
2120                 if (cellHasTerrainFlag(i, j, T_OBSTRUCTS_PASSABILITY)) {
2121                     percent = percent * 2/3;
2122                 }
2123                 if (percent >= 100) {
2124                     pmap[i][j].flags |= KNOWN_TO_BE_TRAP_FREE;
2125                 }
2126                 percent = min(percent, 100);
2127                 if (cellHasTMFlag(i, j, TM_IS_SECRET)) {
2128                     if (rand_percent(percent)) {
2129                         discover(i, j);
2130                         foundSomething = true;
2131                     }
2132                 }
2133             }
2134         }
2135     }
2136     return foundSomething;
2137 }
2138 
proposeOrConfirmLocation(short x,short y,char * failureMessage)2139 boolean proposeOrConfirmLocation(short x, short y, char *failureMessage) {
2140     boolean retval = false;
2141     if (player.xLoc == x && player.yLoc == y) {
2142         message("you are already there.", 0);
2143     } else if (pmap[x][y].flags & (DISCOVERED | MAGIC_MAPPED)) {
2144         if (rogue.cursorLoc[0] == x && rogue.cursorLoc[1] == y) {
2145             retval = true;
2146         } else {
2147             rogue.cursorLoc[0] = x;
2148             rogue.cursorLoc[1] = y;
2149         }
2150     } else {
2151         message(failureMessage, 0);
2152     }
2153     return retval;
2154 }
2155 
useStairs(short stairDirection)2156 boolean useStairs(short stairDirection) {
2157     boolean succeeded = false;
2158     //cellDisplayBuffer fromBuf[COLS][ROWS], toBuf[COLS][ROWS];
2159 
2160     if (stairDirection == 1) {
2161         if (rogue.depthLevel < DEEPEST_LEVEL) {
2162             //copyDisplayBuffer(fromBuf, displayBuffer);
2163             rogue.cursorLoc[0] = rogue.cursorLoc[1] = -1;
2164             rogue.depthLevel++;
2165             message("You descend.", 0);
2166             startLevel(rogue.depthLevel - 1, stairDirection);
2167             if (rogue.depthLevel > rogue.deepestLevel) {
2168                 rogue.deepestLevel = rogue.depthLevel;
2169             }
2170             //copyDisplayBuffer(toBuf, displayBuffer);
2171             //irisFadeBetweenBuffers(fromBuf, toBuf, mapToWindowX(player.xLoc), mapToWindowY(player.yLoc), 20, false);
2172         } else if (numberOfMatchingPackItems(AMULET, 0, 0, false)) {
2173             victory(true);
2174         } else {
2175             confirmMessages();
2176             messageWithColor("the crystal archway repels you with a mysterious force!", &lightBlue, 0);
2177             messageWithColor("(Only the bearer of the Amulet of Yendor may pass.)", &backgroundMessageColor, 0);
2178         }
2179         succeeded = true;
2180     } else {
2181         if (rogue.depthLevel > 1 || numberOfMatchingPackItems(AMULET, 0, 0, false)) {
2182             rogue.cursorLoc[0] = rogue.cursorLoc[1] = -1;
2183             rogue.depthLevel--;
2184             if (rogue.depthLevel == 0) {
2185                 victory(false);
2186             } else {
2187                 //copyDisplayBuffer(fromBuf, displayBuffer);
2188                 message("You ascend.", 0);
2189                 startLevel(rogue.depthLevel + 1, stairDirection);
2190                 //copyDisplayBuffer(toBuf, displayBuffer);
2191                 //irisFadeBetweenBuffers(fromBuf, toBuf, mapToWindowX(player.xLoc), mapToWindowY(player.yLoc), 20, true);
2192             }
2193             succeeded = true;
2194         } else {
2195             confirmMessages();
2196             messageWithColor("The dungeon exit is magically sealed!", &lightBlue, 0);
2197             messageWithColor("(Only the bearer of the Amulet of Yendor may pass.)", &backgroundMessageColor, 0);
2198         }
2199     }
2200 
2201     if (succeeded) {
2202         updatePlayerUnderwaterness();
2203     }
2204 
2205     return succeeded;
2206 }
2207 
storeMemories(const short x,const short y)2208 void storeMemories(const short x, const short y) {
2209     pmap[x][y].rememberedTerrainFlags = terrainFlags(x, y);
2210     pmap[x][y].rememberedTMFlags = terrainMechFlags(x, y);
2211     pmap[x][y].rememberedCellFlags = pmap[x][y].flags;
2212     pmap[x][y].rememberedTerrain = pmap[x][y].layers[highestPriorityLayer(x, y, false)];
2213 }
2214 
updateFieldOfViewDisplay(boolean updateDancingTerrain,boolean refreshDisplay)2215 void updateFieldOfViewDisplay(boolean updateDancingTerrain, boolean refreshDisplay) {
2216     short i, j;
2217     item *theItem;
2218     char buf[COLS*3], name[COLS*3];
2219 
2220     assureCosmeticRNG;
2221 
2222     for (i=0; i<DCOLS; i++) {
2223         for (j = DROWS-1; j >= 0; j--) {
2224             if (pmap[i][j].flags & IN_FIELD_OF_VIEW
2225                 && (max(0, tmap[i][j].light[0])
2226                     + max(0, tmap[i][j].light[1])
2227                     + max(0, tmap[i][j].light[2]) > VISIBILITY_THRESHOLD)
2228                 && !(pmap[i][j].flags & CLAIRVOYANT_DARKENED)) {
2229 
2230                 pmap[i][j].flags |= VISIBLE;
2231             }
2232 
2233             if ((pmap[i][j].flags & VISIBLE) && !(pmap[i][j].flags & WAS_VISIBLE)) { // if the cell became visible this move
2234                 if (!(pmap[i][j].flags & DISCOVERED) && rogue.automationActive) {
2235                     if (pmap[i][j].flags & HAS_ITEM) {
2236                         theItem = itemAtLoc(i, j);
2237                         if (theItem && (theItem->category & KEY)) {
2238                             itemName(theItem, name, false, true, NULL);
2239                             sprintf(buf, "you see %s.", name);
2240                             messageWithColor(buf, &itemMessageColor, 0);
2241                         }
2242                     }
2243                     if (!(pmap[i][j].flags & MAGIC_MAPPED)
2244                         && cellHasTMFlag(i, j, TM_INTERRUPT_EXPLORATION_WHEN_SEEN)) {
2245 
2246                         strcpy(name, tileCatalog[pmap[i][j].layers[layerWithTMFlag(i, j, TM_INTERRUPT_EXPLORATION_WHEN_SEEN)]].description);
2247                         sprintf(buf, "you see %s.", name);
2248                         messageWithColor(buf, &backgroundMessageColor, 0);
2249                     }
2250                 }
2251                 discoverCell(i, j);
2252                 if (refreshDisplay) {
2253                     refreshDungeonCell(i, j);
2254                 }
2255             } else if (!(pmap[i][j].flags & VISIBLE) && (pmap[i][j].flags & WAS_VISIBLE)) { // if the cell ceased being visible this move
2256                 storeMemories(i, j);
2257                 if (refreshDisplay) {
2258                     refreshDungeonCell(i, j);
2259                 }
2260             } else if (!(pmap[i][j].flags & CLAIRVOYANT_VISIBLE) && (pmap[i][j].flags & WAS_CLAIRVOYANT_VISIBLE)) { // ceased being clairvoyantly visible
2261                 storeMemories(i, j);
2262                 if (refreshDisplay) {
2263                     refreshDungeonCell(i, j);
2264                 }
2265             } else if (!(pmap[i][j].flags & WAS_CLAIRVOYANT_VISIBLE) && (pmap[i][j].flags & CLAIRVOYANT_VISIBLE)) { // became clairvoyantly visible
2266                 pmap[i][j].flags &= ~STABLE_MEMORY;
2267                 if (refreshDisplay) {
2268                     refreshDungeonCell(i, j);
2269                 }
2270             } else if (!(pmap[i][j].flags & TELEPATHIC_VISIBLE) && (pmap[i][j].flags & WAS_TELEPATHIC_VISIBLE)) { // ceased being telepathically visible
2271                 storeMemories(i, j);
2272                 if (refreshDisplay) {
2273                     refreshDungeonCell(i, j);
2274                 }
2275             } else if (!(pmap[i][j].flags & WAS_TELEPATHIC_VISIBLE) && (pmap[i][j].flags & TELEPATHIC_VISIBLE)) { // became telepathically visible
2276                 if (!(pmap[i][j].flags & DISCOVERED)
2277                     && !cellHasTerrainFlag(i, j, T_PATHING_BLOCKER)) {
2278                     rogue.xpxpThisTurn++;
2279                 }
2280 
2281                 pmap[i][j].flags &= ~STABLE_MEMORY;
2282                 if (refreshDisplay) {
2283                     refreshDungeonCell(i, j);
2284                 }
2285             } else if (playerCanSeeOrSense(i, j)
2286                        && (tmap[i][j].light[0] != tmap[i][j].oldLight[0] ||
2287                            tmap[i][j].light[1] != tmap[i][j].oldLight[1] ||
2288                            tmap[i][j].light[2] != tmap[i][j].oldLight[2])) { // if the cell's light color changed this move
2289 
2290                            if (refreshDisplay) {
2291                                refreshDungeonCell(i, j);
2292                            }
2293                        } else if (updateDancingTerrain
2294                                   && playerCanSee(i, j)
2295                                   && (!rogue.automationActive || !(rogue.playerTurnNumber % 5))
2296                                   && ((tileCatalog[pmap[i][j].layers[DUNGEON]].backColor)       && tileCatalog[pmap[i][j].layers[DUNGEON]].backColor->colorDances
2297                                       || (tileCatalog[pmap[i][j].layers[DUNGEON]].foreColor)    && tileCatalog[pmap[i][j].layers[DUNGEON]].foreColor->colorDances
2298                                       || (tileCatalog[pmap[i][j].layers[LIQUID]].backColor)     && tileCatalog[pmap[i][j].layers[LIQUID]].backColor->colorDances
2299                                       || (tileCatalog[pmap[i][j].layers[LIQUID]].foreColor)     && tileCatalog[pmap[i][j].layers[LIQUID]].foreColor->colorDances
2300                                       || (tileCatalog[pmap[i][j].layers[SURFACE]].backColor)    && tileCatalog[pmap[i][j].layers[SURFACE]].backColor->colorDances
2301                                       || (tileCatalog[pmap[i][j].layers[SURFACE]].foreColor)    && tileCatalog[pmap[i][j].layers[SURFACE]].foreColor->colorDances
2302                                       || (tileCatalog[pmap[i][j].layers[GAS]].backColor)        && tileCatalog[pmap[i][j].layers[GAS]].backColor->colorDances
2303                                       || (tileCatalog[pmap[i][j].layers[GAS]].foreColor)        && tileCatalog[pmap[i][j].layers[GAS]].foreColor->colorDances
2304                                       || player.status[STATUS_HALLUCINATING])) {
2305 
2306                                       pmap[i][j].flags &= ~STABLE_MEMORY;
2307                                       if (refreshDisplay) {
2308                                           refreshDungeonCell(i, j);
2309                                       }
2310                                   }
2311         }
2312     }
2313     restoreRNG;
2314 }
2315 
2316 //         Octants:      //
2317 //          \7|8/        //
2318 //          6\|/1        //
2319 //          --@--        //
2320 //          5/|\2        //
2321 //          /4|3\        //
2322 
betweenOctant1andN(short * x,short * y,short x0,short y0,short n)2323 void betweenOctant1andN(short *x, short *y, short x0, short y0, short n) {
2324     short x1 = *x, y1 = *y;
2325     short dx = x1 - x0, dy = y1 - y0;
2326     switch (n) {
2327         case 1:
2328             return;
2329         case 2:
2330             *y = y0 - dy;
2331             return;
2332         case 5:
2333             *x = x0 - dx;
2334             *y = y0 - dy;
2335             return;
2336         case 6:
2337             *x = x0 - dx;
2338             return;
2339         case 8:
2340             *x = x0 - dy;
2341             *y = y0 - dx;
2342             return;
2343         case 3:
2344             *x = x0 - dy;
2345             *y = y0 + dx;
2346             return;
2347         case 7:
2348             *x = x0 + dy;
2349             *y = y0 - dx;
2350             return;
2351         case 4:
2352             *x = x0 + dy;
2353             *y = y0 + dx;
2354             return;
2355     }
2356 }
2357 
2358 // Returns a boolean grid indicating whether each square is in the field of view of (xLoc, yLoc).
2359 // forbiddenTerrain is the set of terrain flags that will block vision (but the blocking cell itself is
2360 // illuminated); forbiddenFlags is the set of map flags that will block vision.
2361 // If cautiousOnWalls is set, we will not illuminate blocking tiles unless the tile one space closer to the origin
2362 // is visible to the player; this is to prevent lights from illuminating a wall when the player is on the other
2363 // side of the wall.
getFOVMask(char grid[DCOLS][DROWS],short xLoc,short yLoc,fixpt maxRadius,unsigned long forbiddenTerrain,unsigned long forbiddenFlags,boolean cautiousOnWalls)2364 void getFOVMask(char grid[DCOLS][DROWS], short xLoc, short yLoc, fixpt maxRadius,
2365                 unsigned long forbiddenTerrain, unsigned long forbiddenFlags, boolean cautiousOnWalls) {
2366     short i;
2367 
2368     for (i=1; i<=8; i++) {
2369         scanOctantFOV(grid, xLoc, yLoc, i, maxRadius, 1, LOS_SLOPE_GRANULARITY * -1, 0,
2370                       forbiddenTerrain, forbiddenFlags, cautiousOnWalls);
2371     }
2372 }
2373 
2374 // This is a custom implementation of recursive shadowcasting.
scanOctantFOV(char grid[DCOLS][DROWS],short xLoc,short yLoc,short octant,fixpt maxRadius,short columnsRightFromOrigin,long startSlope,long endSlope,unsigned long forbiddenTerrain,unsigned long forbiddenFlags,boolean cautiousOnWalls)2375 void scanOctantFOV(char grid[DCOLS][DROWS], short xLoc, short yLoc, short octant, fixpt maxRadius,
2376                    short columnsRightFromOrigin, long startSlope, long endSlope, unsigned long forbiddenTerrain,
2377                    unsigned long forbiddenFlags, boolean cautiousOnWalls) {
2378 
2379     if (columnsRightFromOrigin * FP_FACTOR >= maxRadius) return;
2380 
2381     short i, a, b, iStart, iEnd, x, y, x2, y2; // x and y are temporary variables on which we do the octant transform
2382     long newStartSlope, newEndSlope;
2383     boolean cellObstructed;
2384 
2385     newStartSlope = startSlope;
2386 
2387     a = ((LOS_SLOPE_GRANULARITY / -2 + 1) + startSlope * columnsRightFromOrigin) / LOS_SLOPE_GRANULARITY;
2388     b = ((LOS_SLOPE_GRANULARITY / -2 + 1) + endSlope * columnsRightFromOrigin) / LOS_SLOPE_GRANULARITY;
2389 
2390     iStart = min(a, b);
2391     iEnd = max(a, b);
2392 
2393     // restrict vision to a circle of radius maxRadius
2394     if ((columnsRightFromOrigin*columnsRightFromOrigin + iEnd*iEnd) >= maxRadius*maxRadius / FP_FACTOR / FP_FACTOR) {
2395         return;
2396     }
2397     if ((columnsRightFromOrigin*columnsRightFromOrigin + iStart*iStart) >= maxRadius*maxRadius / FP_FACTOR / FP_FACTOR) {
2398         iStart = (int) (-1 * fp_sqrt((maxRadius*maxRadius / FP_FACTOR) - (columnsRightFromOrigin*columnsRightFromOrigin * FP_FACTOR)) / FP_FACTOR);
2399     }
2400 
2401     x = xLoc + columnsRightFromOrigin;
2402     y = yLoc + iStart;
2403     betweenOctant1andN(&x, &y, xLoc, yLoc, octant);
2404     boolean currentlyLit = coordinatesAreInMap(x, y) && !(cellHasTerrainFlag(x, y, forbiddenTerrain) ||
2405                                                           (pmap[x][y].flags & forbiddenFlags));
2406     for (i = iStart; i <= iEnd; i++) {
2407         x = xLoc + columnsRightFromOrigin;
2408         y = yLoc + i;
2409         betweenOctant1andN(&x, &y, xLoc, yLoc, octant);
2410         if (!coordinatesAreInMap(x, y)) {
2411             // We're off the map -- here there be memory corruption.
2412             continue;
2413         }
2414         cellObstructed = (cellHasTerrainFlag(x, y, forbiddenTerrain) || (pmap[x][y].flags & forbiddenFlags));
2415         // if we're cautious on walls and this is a wall:
2416         if (cautiousOnWalls && cellObstructed) {
2417             // (x2, y2) is the tile one space closer to the origin from the tile we're on:
2418             x2 = xLoc + columnsRightFromOrigin - 1;
2419             y2 = yLoc + i;
2420             if (i < 0) {
2421                 y2++;
2422             } else if (i > 0) {
2423                 y2--;
2424             }
2425             betweenOctant1andN(&x2, &y2, xLoc, yLoc, octant);
2426 
2427             if (pmap[x2][y2].flags & IN_FIELD_OF_VIEW) {
2428                 // previous tile is visible, so illuminate
2429                 grid[x][y] = 1;
2430             }
2431         } else {
2432             // illuminate
2433             grid[x][y] = 1;
2434         }
2435         if (!cellObstructed && !currentlyLit) { // next column slope starts here
2436             newStartSlope = (long int) ((LOS_SLOPE_GRANULARITY * (i) - LOS_SLOPE_GRANULARITY / 2) / (columnsRightFromOrigin * 2 + 1) * 2);
2437             currentlyLit = true;
2438         } else if (cellObstructed && currentlyLit) { // next column slope ends here
2439             newEndSlope = (long int) ((LOS_SLOPE_GRANULARITY * (i) - LOS_SLOPE_GRANULARITY / 2)
2440                             / (columnsRightFromOrigin * 2 - 1) * 2);
2441             if (newStartSlope <= newEndSlope) {
2442                 // run next column
2443                 scanOctantFOV(grid, xLoc, yLoc, octant, maxRadius, columnsRightFromOrigin + 1, newStartSlope, newEndSlope,
2444                               forbiddenTerrain, forbiddenFlags, cautiousOnWalls);
2445             }
2446             currentlyLit = false;
2447         }
2448     }
2449     if (currentlyLit) { // got to the bottom of the scan while lit
2450         newEndSlope = endSlope;
2451         if (newStartSlope <= newEndSlope) {
2452             // run next column
2453             scanOctantFOV(grid, xLoc, yLoc, octant, maxRadius, columnsRightFromOrigin + 1, newStartSlope, newEndSlope,
2454                           forbiddenTerrain, forbiddenFlags, cautiousOnWalls);
2455         }
2456     }
2457 }
2458 
addScentToCell(short x,short y,short distance)2459 void addScentToCell(short x, short y, short distance) {
2460     unsigned short value;
2461     if (!cellHasTerrainFlag(x, y, T_OBSTRUCTS_SCENT) || !cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY)) {
2462         value = rogue.scentTurnNumber - distance;
2463         scentMap[x][y] = max(value, (unsigned short) scentMap[x][y]);
2464     }
2465 }
2466