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