1 /*
2  *  Time.c
3  *  Brogue
4  *
5  *  Created by Brian Walker on 6/21/13.
6  *  Copyright 2013. 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 
exposeCreatureToFire(creature * monst)27 void exposeCreatureToFire(creature *monst) {
28     char buf[COLS], buf2[COLS];
29     if ((monst->bookkeepingFlags & MB_IS_DYING)
30         || monst->status[STATUS_IMMUNE_TO_FIRE]
31         || (monst->info.flags & MONST_INVULNERABLE)
32         || (monst->bookkeepingFlags & MB_SUBMERGED)
33         || ((!monst->status[STATUS_LEVITATING]) && cellHasTMFlag(monst->xLoc, monst->yLoc, TM_EXTINGUISHES_FIRE))) {
34         return;
35     }
36     if (monst->status[STATUS_BURNING] == 0) {
37         if (monst == &player) {
38             rogue.minersLight.lightColor = &fireForeColor;
39             player.info.foreColor = &torchLightColor;
40             refreshDungeonCell(player.xLoc, player.yLoc);
41             //updateVision(); // this screws up the firebolt visual effect by erasing it while a message is displayed
42             combatMessage("you catch fire", &badMessageColor);
43         } else if (canDirectlySeeMonster(monst)) {
44             monsterName(buf, monst, true);
45             sprintf(buf2, "%s catches fire", buf);
46             combatMessage(buf2, messageColorFromVictim(monst));
47         }
48     }
49     monst->status[STATUS_BURNING] = monst->maxStatus[STATUS_BURNING] = max(monst->status[STATUS_BURNING], 7);
50 }
51 
updateFlavorText()52 void updateFlavorText() {
53     char buf[DCOLS * 3];
54     if (rogue.disturbed && !rogue.gameHasEnded) {
55         if (rogue.armor
56             && (rogue.armor->flags & ITEM_RUNIC)
57             && rogue.armor->enchant2 == A_RESPIRATION
58             && tileCatalog[pmap[player.xLoc][player.yLoc].layers[highestPriorityLayer(player.xLoc, player.yLoc, false)]].flags & T_RESPIRATION_IMMUNITIES) {
59 
60             flavorMessage("A pocket of cool, clean air swirls around you.");
61         } else if (player.status[STATUS_LEVITATING]) {
62             describeLocation(buf, player.xLoc, player.yLoc);
63             flavorMessage(buf);
64         } else {
65             flavorMessage(tileFlavor(player.xLoc, player.yLoc));
66         }
67     }
68 }
69 
updatePlayerUnderwaterness()70 void updatePlayerUnderwaterness() {
71     if (rogue.inWater) {
72         if (!cellHasTerrainFlag(player.xLoc, player.yLoc, T_IS_DEEP_WATER) || player.status[STATUS_LEVITATING]
73             || cellHasTerrainFlag(player.xLoc, player.yLoc, (T_ENTANGLES | T_OBSTRUCTS_PASSABILITY))) {
74 
75             rogue.inWater = false;
76             updateMinersLightRadius();
77             updateVision(true);
78             displayLevel();
79         }
80     } else {
81         if (cellHasTerrainFlag(player.xLoc, player.yLoc, T_IS_DEEP_WATER) && !player.status[STATUS_LEVITATING]
82             && !cellHasTerrainFlag(player.xLoc, player.yLoc, (T_ENTANGLES | T_OBSTRUCTS_PASSABILITY))) {
83 
84             rogue.inWater = true;
85             updateMinersLightRadius();
86             updateVision(true);
87             displayLevel();
88         }
89     }
90 }
91 
monsterShouldFall(creature * monst)92 boolean monsterShouldFall(creature *monst) {
93     return (!(monst->status[STATUS_LEVITATING])
94             && cellHasTerrainFlag(monst->xLoc, monst->yLoc, T_AUTO_DESCENT)
95             && !cellHasTerrainFlag(monst->xLoc, monst->yLoc, T_ENTANGLES | T_OBSTRUCTS_PASSABILITY)
96             && !(monst->bookkeepingFlags & MB_PREPLACED));
97 }
98 
99 // Called at least every 100 ticks; may be called more frequently.
applyInstantTileEffectsToCreature(creature * monst)100 void applyInstantTileEffectsToCreature(creature *monst) {
101     char buf[COLS], buf2[COLS], buf3[COLS];
102     char *s;
103     short *x = &(monst->xLoc), *y = &(monst->yLoc), damage;
104     enum dungeonLayers layer;
105     item *theItem;
106 
107     if (monst->bookkeepingFlags & MB_IS_DYING) {
108         return; // the monster is already dead.
109     }
110 
111     if (monst == &player) {
112         if (!player.status[STATUS_LEVITATING]) {
113             pmap[*x][*y].flags |= KNOWN_TO_BE_TRAP_FREE;
114         }
115     } else if (!player.status[STATUS_HALLUCINATING]
116                && !monst->status[STATUS_LEVITATING]
117                && canSeeMonster(monst)
118                && !(cellHasTerrainFlag(*x, *y, T_IS_DF_TRAP))) {
119         pmap[*x][*y].flags |= KNOWN_TO_BE_TRAP_FREE;
120     }
121 
122     // You will discover the secrets of any tile you stand on.
123     if (monst == &player
124         && !(monst->status[STATUS_LEVITATING])
125         && cellHasTMFlag(*x, *y, TM_IS_SECRET)
126         && playerCanSee(*x, *y)) {
127 
128         discover(*x, *y);
129     }
130 
131     // Submerged monsters in terrain that doesn't permit submersion should immediately surface.
132     if ((monst->bookkeepingFlags & MB_SUBMERGED) && !cellHasTMFlag(*x, *y, TM_ALLOWS_SUBMERGING)) {
133         monst->bookkeepingFlags &= ~MB_SUBMERGED;
134     }
135 
136     // Visual effect for submersion in water.
137     if (monst == &player) {
138         updatePlayerUnderwaterness();
139     }
140 
141     // Obstructed krakens can't seize their prey.
142     if ((monst->bookkeepingFlags & MB_SEIZING)
143         && (cellHasTerrainFlag(*x, *y, T_OBSTRUCTS_PASSABILITY))
144         && !(monst->info.flags & MONST_ATTACKABLE_THRU_WALLS)) {
145 
146         monst->bookkeepingFlags &= ~MB_SEIZING;
147     }
148 
149     // Creatures plunge into chasms and through trap doors.
150     if (monsterShouldFall(monst)) {
151         if (monst == &player) {
152             // player falling takes place at the end of the turn
153             if (!(monst->bookkeepingFlags & MB_IS_FALLING)) {
154                 monst->bookkeepingFlags |= MB_IS_FALLING;
155             }
156             return;
157         } else { // it's a monster
158             monst->bookkeepingFlags |= MB_IS_FALLING; // handled at end of turn
159         }
160     }
161 
162     // lava
163     if (!(monst->status[STATUS_LEVITATING])
164         && !(monst->status[STATUS_IMMUNE_TO_FIRE])
165         && !(monst->info.flags & MONST_INVULNERABLE)
166         && !cellHasTerrainFlag(*x, *y, (T_ENTANGLES | T_OBSTRUCTS_PASSABILITY))
167         && !cellHasTMFlag(*x, *y, TM_EXTINGUISHES_FIRE)
168         && cellHasTerrainFlag(*x, *y, T_LAVA_INSTA_DEATH)) {
169 
170         if (monst == &player) {
171             sprintf(buf, "you plunge into %s!",
172                     tileCatalog[pmap[*x][*y].layers[layerWithFlag(*x, *y, T_LAVA_INSTA_DEATH)]].description);
173             message(buf, REQUIRE_ACKNOWLEDGMENT);
174             sprintf(buf, "Killed by %s",
175                     tileCatalog[pmap[*x][*y].layers[layerWithFlag(*x, *y, T_LAVA_INSTA_DEATH)]].description);
176             gameOver(buf, true);
177             return;
178         } else { // it's a monster
179             if (canSeeMonster(monst)) {
180                 monsterName(buf, monst, true);
181                 s = tileCatalog[pmap[*x][*y].layers[layerWithFlag(*x, *y, T_LAVA_INSTA_DEATH)]].description;
182                 // Skip over articles
183                 if (strncmp(s, "a ", 2) == 0) {
184                     s += 2;
185                 } else if (strncmp(s, "an ", 3) == 0) {
186                     s += 3;
187                 }
188                 sprintf(buf2, "%s is consumed by the %s instantly!", buf, s);
189                 messageWithColor(buf2, messageColorFromVictim(monst), 0);
190             }
191             killCreature(monst, false);
192             spawnDungeonFeature(*x, *y, &(dungeonFeatureCatalog[DF_CREATURE_FIRE]), true, false);
193             refreshDungeonCell(*x, *y);
194             return;
195         }
196     }
197 
198     // Water puts out fire.
199     if (cellHasTMFlag(*x, *y, TM_EXTINGUISHES_FIRE)
200         && monst->status[STATUS_BURNING]
201         && !monst->status[STATUS_LEVITATING]
202         && !(monst->info.flags & MONST_ATTACKABLE_THRU_WALLS)
203         && !(monst->info.flags & MONST_FIERY)) {
204         extinguishFireOnCreature(monst);
205     }
206 
207     // If you see a monster use a secret door, you discover it.
208     if (playerCanSee(*x, *y)
209         && cellHasTMFlag(*x, *y, TM_IS_SECRET)
210         && (cellHasTerrainFlag(*x, *y, T_OBSTRUCTS_PASSABILITY))) {
211         discover(*x, *y);
212     }
213 
214     // Pressure plates.
215     if (!(monst->status[STATUS_LEVITATING])
216         && !(monst->bookkeepingFlags & MB_SUBMERGED)
217         && (!cellHasTMFlag(*x, *y, TM_ALLOWS_SUBMERGING) || !(monst->info.flags & MONST_SUBMERGES))
218         && cellHasTerrainFlag(*x, *y, T_IS_DF_TRAP)
219         && !(pmap[*x][*y].flags & PRESSURE_PLATE_DEPRESSED)) {
220 
221         pmap[*x][*y].flags |= PRESSURE_PLATE_DEPRESSED;
222         if (playerCanSee(*x, *y) && cellHasTMFlag(*x, *y, TM_IS_SECRET)) {
223             discover(*x, *y);
224             refreshDungeonCell(*x, *y);
225         }
226         if (canSeeMonster(monst)) {
227             monsterName(buf, monst, true);
228             sprintf(buf2, "a pressure plate clicks underneath %s!", buf);
229             message(buf2, REQUIRE_ACKNOWLEDGMENT);
230         } else if (playerCanSee(*x, *y)) {
231             // usually means an invisible monster
232             message("a pressure plate clicks!", 0);
233         }
234         for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
235             if (tileCatalog[pmap[*x][*y].layers[layer]].flags & T_IS_DF_TRAP) {
236                 spawnDungeonFeature(*x, *y, &(dungeonFeatureCatalog[tileCatalog[pmap[*x][*y].layers[layer]].fireType]), true, false);
237                 promoteTile(*x, *y, layer, false);
238             }
239         }
240     }
241 
242     if (cellHasTMFlag(*x, *y, TM_PROMOTES_ON_CREATURE)) { // flying creatures activate too
243         // Because this uses no pressure plate to keep track of whether it's already depressed,
244         // it will trigger every time this function is called while the monster or player is on the tile.
245         // Because this function can be called several times per turn, multiple promotions can
246         // happen unpredictably if the tile does not promote to a tile without the T_PROMOTES_ON_STEP
247         // attribute. That's acceptable for some effects, e.g. doors opening,
248         // but not for others, e.g. magical glyphs activating.
249         for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
250             if (tileCatalog[pmap[*x][*y].layers[layer]].mechFlags & TM_PROMOTES_ON_CREATURE) {
251                 promoteTile(*x, *y, layer, false);
252             }
253         }
254     }
255 
256     if (cellHasTMFlag(*x, *y, TM_PROMOTES_ON_PLAYER_ENTRY) && monst == &player) {
257         // Subject to same caveats as T_PROMOTES_ON_STEP above.
258         for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
259             if (tileCatalog[pmap[*x][*y].layers[layer]].mechFlags & TM_PROMOTES_ON_PLAYER_ENTRY) {
260                 promoteTile(*x, *y, layer, false);
261             }
262         }
263     }
264 
265     if (cellHasTMFlag(*x, *y, TM_PROMOTES_ON_SACRIFICE_ENTRY)
266         && monst->machineHome == pmap[*x][*y].machineNumber
267         && (monst->bookkeepingFlags & MB_MARKED_FOR_SACRIFICE)) {
268         // Subject to same caveats as T_PROMOTES_ON_STEP above.
269         for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
270             if (tileCatalog[pmap[*x][*y].layers[layer]].mechFlags & TM_PROMOTES_ON_SACRIFICE_ENTRY) {
271                 promoteTile(*x, *y, layer, false);
272             }
273         }
274     }
275 
276     // spiderwebs
277     if (cellHasTerrainFlag(*x, *y, T_ENTANGLES) && !monst->status[STATUS_STUCK]
278         && !(monst->info.flags & (MONST_IMMUNE_TO_WEBS | MONST_INVULNERABLE))
279         && !(monst->bookkeepingFlags & MB_SUBMERGED)) {
280 
281         monst->status[STATUS_STUCK] = monst->maxStatus[STATUS_STUCK] = rand_range(3, 7);
282         if (monst == &player) {
283             if (!rogue.automationActive) {
284                 // Don't interrupt exploration with this message.
285                 sprintf(buf2, "you are stuck fast in %s!",
286                         tileCatalog[pmap[*x][*y].layers[layerWithFlag(*x, *y, T_ENTANGLES)]].description);
287                 message(buf2, 0);
288             }
289         } else if (canDirectlySeeMonster(monst)) { // it's a monster
290             if (!rogue.automationActive) {
291                 monsterName(buf, monst, true);
292                 sprintf(buf2, "%s is stuck fast in %s!", buf,
293                         tileCatalog[pmap[*x][*y].layers[layerWithFlag(*x, *y, T_ENTANGLES)]].description);
294                 message(buf2, 0);
295             }
296         }
297     }
298 
299     // explosions
300     if (cellHasTerrainFlag(*x, *y, T_CAUSES_EXPLOSIVE_DAMAGE) && !monst->status[STATUS_EXPLOSION_IMMUNITY]
301         && !(monst->bookkeepingFlags & MB_SUBMERGED)) {
302         damage = rand_range(15, 20);
303         damage = max(damage, monst->info.maxHP / 2);
304         monst->status[STATUS_EXPLOSION_IMMUNITY] = 5;
305         if (monst == &player) {
306             rogue.disturbed = true;
307             for (layer = 0; layer < NUMBER_TERRAIN_LAYERS && !(tileCatalog[pmap[*x][*y].layers[layer]].flags & T_CAUSES_EXPLOSIVE_DAMAGE); layer++);
308             message(tileCatalog[pmap[*x][*y].layers[layer]].flavorText, 0);
309             if (rogue.armor && (rogue.armor->flags & ITEM_RUNIC) && rogue.armor->enchant2 == A_DAMPENING) {
310                 itemName(rogue.armor, buf2, false, false, NULL);
311                 sprintf(buf, "Your %s pulses and absorbs the damage.", buf2);
312                 messageWithColor(buf, &goodMessageColor, 0);
313                 autoIdentify(rogue.armor);
314             } else if (inflictDamage(NULL, &player, damage, &yellow, false)) {
315                 strcpy(buf2, tileCatalog[pmap[*x][*y].layers[layerWithFlag(*x, *y, T_CAUSES_EXPLOSIVE_DAMAGE)]].description);
316                 sprintf(buf, "Killed by %s", buf2);
317                 gameOver(buf, true);
318                 return;
319             }
320         } else { // it's a monster
321             if (monst->creatureState == MONSTER_SLEEPING) {
322                 monst->creatureState = MONSTER_TRACKING_SCENT;
323             }
324             monsterName(buf, monst, true);
325 
326             // Get explosive layer before damage in case a death DF replaces the explosion
327             strcpy(buf3, tileCatalog[pmap[*x][*y].layers[layerWithFlag(*x, *y, T_CAUSES_EXPLOSIVE_DAMAGE)]].description);
328             if (inflictDamage(NULL, monst, damage, &yellow, false)) {
329                 // if killed
330                 sprintf(buf2, "%s %s %s.", buf,
331                         (monst->info.flags & MONST_INANIMATE) ? "is destroyed by" : "dies in",
332                         buf3);
333                 messageWithColor(buf2, messageColorFromVictim(monst), 0);
334                 refreshDungeonCell(*x, *y);
335                 return;
336             } else {
337                 // if survived
338                 sprintf(buf2, "%s engulfs %s.",
339                         tileCatalog[pmap[*x][*y].layers[layerWithFlag(*x, *y, T_CAUSES_EXPLOSIVE_DAMAGE)]].description, buf);
340                 messageWithColor(buf2, messageColorFromVictim(monst), 0);
341             }
342         }
343     }
344 
345     // Toxic gases!
346     // If it's the player, and he's wearing armor of respiration, then no effect from toxic gases.
347     if (monst == &player
348         && cellHasTerrainFlag(*x, *y, T_RESPIRATION_IMMUNITIES)
349         && rogue.armor
350         && (rogue.armor->flags & ITEM_RUNIC)
351         && rogue.armor->enchant2 == A_RESPIRATION) {
352         if (!(rogue.armor->flags & ITEM_RUNIC_IDENTIFIED)) {
353             message("Your armor trembles and a pocket of clean air swirls around you.", 0);
354             autoIdentify(rogue.armor);
355         }
356     } else {
357 
358         // zombie gas
359         if (cellHasTerrainFlag(*x, *y, T_CAUSES_NAUSEA)
360             && !(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))
361             && !(monst->bookkeepingFlags & MB_SUBMERGED)) {
362             if (monst == &player) {
363                 rogue.disturbed = true;
364             }
365             if (canDirectlySeeMonster(monst) && !(monst->status[STATUS_NAUSEOUS])) {
366                 if (monst->creatureState == MONSTER_SLEEPING) {
367                     monst->creatureState = MONSTER_TRACKING_SCENT;
368                 }
369                 flashMonster(monst, &brown, 100);
370                 monsterName(buf, monst, true);
371                 sprintf(buf2, "%s choke%s and gag%s on the overpowering stench of decay.", buf,
372                         (monst == &player ? "": "s"), (monst == &player ? "": "s"));
373                 message(buf2, 0);
374             }
375             monst->status[STATUS_NAUSEOUS] = monst->maxStatus[STATUS_NAUSEOUS] = max(monst->status[STATUS_NAUSEOUS], 20);
376         }
377 
378         // confusion gas
379         if (cellHasTerrainFlag(*x, *y, T_CAUSES_CONFUSION) && !(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
380             if (monst == &player) {
381                 rogue.disturbed = true;
382             }
383             if (canDirectlySeeMonster(monst) && !(monst->status[STATUS_CONFUSED])) {
384                 if (monst->creatureState == MONSTER_SLEEPING) {
385                     monst->creatureState = MONSTER_TRACKING_SCENT;
386                 }
387                 flashMonster(monst, &confusionGasColor, 100);
388                 monsterName(buf, monst, true);
389                 sprintf(buf2, "%s %s very confused!", buf, (monst == &player ? "feel": "looks"));
390                 message(buf2, 0);
391             }
392             monst->status[STATUS_CONFUSED] = monst->maxStatus[STATUS_CONFUSED] = max(monst->status[STATUS_CONFUSED], 25);
393         }
394 
395         // paralysis gas
396         if (cellHasTerrainFlag(*x, *y, T_CAUSES_PARALYSIS)
397             && !(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))
398             && !(monst->bookkeepingFlags & MB_SUBMERGED)) {
399 
400             if (canDirectlySeeMonster(monst) && !monst->status[STATUS_PARALYZED]) {
401                 flashMonster(monst, &pink, 100);
402                 monsterName(buf, monst, true);
403                 sprintf(buf2, "%s %s paralyzed!", buf, (monst == &player ? "are": "is"));
404                 message(buf2, (monst == &player) ? REQUIRE_ACKNOWLEDGMENT : 0);
405             }
406             monst->status[STATUS_PARALYZED] = monst->maxStatus[STATUS_PARALYZED] = max(monst->status[STATUS_PARALYZED], 20);
407             if (monst == &player) {
408                 rogue.disturbed = true;
409             }
410         }
411     }
412 
413     // poisonous lichen
414     if (cellHasTerrainFlag(*x, *y, T_CAUSES_POISON)
415         && !(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))
416         && !monst->status[STATUS_LEVITATING]) {
417 
418         if (monst == &player && !player.status[STATUS_POISONED]) {
419             rogue.disturbed = true;
420         }
421         if (canDirectlySeeMonster(monst) && !(monst->status[STATUS_POISONED])) {
422             if (monst->creatureState == MONSTER_SLEEPING) {
423                 monst->creatureState = MONSTER_TRACKING_SCENT;
424             }
425             flashMonster(monst, &green, 100);
426             monsterName(buf, monst, true);
427             sprintf(buf2, "the lichen's grasping tendrils poison %s.", buf);
428             messageWithColor(buf2, messageColorFromVictim(monst), 0);
429         }
430         damage = max(0, 5 - monst->status[STATUS_POISONED]);
431         addPoison(monst, damage, 0); // Lichen doesn't increase poison concentration above 1.
432     }
433 
434     // fire
435     if (cellHasTerrainFlag(*x, *y, T_IS_FIRE)) {
436         exposeCreatureToFire(monst);
437     } else if (cellHasTerrainFlag(*x, *y, T_IS_FLAMMABLE)
438             // We should only expose to fire if it is flammable and not on fire. However, when
439             // gas burns, it only sets the volume to 0 and doesn't clear the layer (for visual
440             // reasons). This can cause crashes if the fire tile fails to spawn, so we also exclude it.
441                && !(pmap[*x][*y].layers[GAS] != NOTHING && pmap[*x][*y].volume == 0)
442                && !cellHasTerrainFlag(*x, *y, T_IS_FIRE)
443                && monst->status[STATUS_BURNING]
444                && !(monst->bookkeepingFlags & (MB_SUBMERGED | MB_IS_FALLING))) {
445         exposeTileToFire(*x, *y, true);
446     }
447 
448     // keys
449     if (cellHasTMFlag(*x, *y, TM_PROMOTES_WITH_KEY) && (theItem = keyOnTileAt(*x, *y))) {
450         useKeyAt(theItem, *x, *y);
451     }
452 }
453 
applyGradualTileEffectsToCreature(creature * monst,short ticks)454 void applyGradualTileEffectsToCreature(creature *monst, short ticks) {
455     short itemCandidates, randItemIndex;
456     short x = monst->xLoc, y = monst->yLoc, damage;
457     char buf[COLS * 5], buf2[COLS * 3];
458     item *theItem;
459     enum dungeonLayers layer;
460 
461     if (!(monst->status[STATUS_LEVITATING])
462         && cellHasTerrainFlag(x, y, T_IS_DEEP_WATER)
463         && !cellHasTerrainFlag(x, y, (T_ENTANGLES | T_OBSTRUCTS_PASSABILITY))
464         && !(monst->info.flags & MONST_IMMUNE_TO_WATER)) {
465         if (monst == &player) {
466             if (!(pmap[x][y].flags & HAS_ITEM) && rand_percent(ticks * 50 / 100)) {
467                 itemCandidates = numberOfMatchingPackItems(ALL_ITEMS, 0, (ITEM_EQUIPPED), false);
468                 if (itemCandidates) {
469                     randItemIndex = rand_range(1, itemCandidates);
470                     for (theItem = packItems->nextItem; theItem != NULL; theItem = theItem->nextItem) {
471                         if (!(theItem->flags & (ITEM_EQUIPPED))) {
472                             if (randItemIndex == 1) {
473                                 break;
474                             } else {
475                                 randItemIndex--;
476                             }
477                         }
478                     }
479                     theItem = dropItem(theItem);
480                     if (theItem) {
481                         itemName(theItem, buf2, false, true, NULL);
482                         sprintf(buf, "%s float%s away in the current!",
483                                 buf2,
484                                 (theItem->quantity == 1 ? "s" : ""));
485                         messageWithColor(buf, &itemMessageColor, 0);
486                     }
487                 }
488             }
489         } else if (monst->carriedItem && !(pmap[x][y].flags & HAS_ITEM) && rand_percent(ticks * 50 / 100)) { // it's a monster with an item
490             makeMonsterDropItem(monst);
491         }
492     }
493 
494     if (cellHasTerrainFlag(x, y, T_CAUSES_DAMAGE)
495         && !(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))
496         && !(monst->bookkeepingFlags & MB_SUBMERGED)) {
497 
498         damage = (monst->info.maxHP / 15) * ticks / 100;
499         damage = max(1, damage);
500         for (layer = 0; layer < NUMBER_TERRAIN_LAYERS && !(tileCatalog[pmap[x][y].layers[layer]].flags & T_CAUSES_DAMAGE); layer++);
501         if (monst == &player) {
502             if (rogue.armor && (rogue.armor->flags & ITEM_RUNIC) && rogue.armor->enchant2 == A_RESPIRATION) {
503                 if (!(rogue.armor->flags & ITEM_RUNIC_IDENTIFIED)) {
504                     message("Your armor trembles and a pocket of clean air swirls around you.", 0);
505                     autoIdentify(rogue.armor);
506                 }
507             } else {
508                 rogue.disturbed = true;
509                 messageWithColor(tileCatalog[pmap[x][y].layers[layer]].flavorText, &badMessageColor, 0);
510                 if (inflictDamage(NULL, &player, damage, tileCatalog[pmap[x][y].layers[layer]].backColor, true)) {
511                     sprintf(buf, "Killed by %s", tileCatalog[pmap[x][y].layers[layer]].description);
512                     gameOver(buf, true);
513                     return;
514                 }
515             }
516         } else { // it's a monster
517             if (monst->creatureState == MONSTER_SLEEPING) {
518                 monst->creatureState = MONSTER_TRACKING_SCENT;
519             }
520             if (inflictDamage(NULL, monst, damage, tileCatalog[pmap[x][y].layers[layer]].backColor, true)) {
521                 if (canSeeMonster(monst)) {
522                     monsterName(buf, monst, true);
523                     sprintf(buf2, "%s dies.", buf);
524                     messageWithColor(buf2, messageColorFromVictim(monst), 0);
525                 }
526                 refreshDungeonCell(x, y);
527                 return;
528             }
529         }
530     }
531 
532     if (cellHasTerrainFlag(x, y, T_CAUSES_HEALING)
533         && !(monst->info.flags & MONST_INANIMATE)
534         && !(monst->bookkeepingFlags & MB_SUBMERGED)) {
535 
536         damage = (monst->info.maxHP / 15) * ticks / 100;
537         damage = max(1, damage);
538         if (monst->currentHP < monst->info.maxHP) {
539             monst->currentHP = min(monst->currentHP + damage, monst->info.maxHP);
540             if (monst == &player) {
541                 messageWithColor("you feel much better.", &goodMessageColor, 0);
542             }
543         }
544     }
545 }
546 
updateClairvoyance()547 void updateClairvoyance() {
548     short i, j, clairvoyanceRadius, dx, dy;
549     boolean cursed;
550     unsigned long cFlags;
551 
552     for (i=0; i<DCOLS; i++) {
553         for (j=0; j<DROWS; j++) {
554 
555             pmap[i][j].flags &= ~WAS_CLAIRVOYANT_VISIBLE;
556 
557             if (pmap[i][j].flags & CLAIRVOYANT_VISIBLE) {
558                 pmap[i][j].flags |= WAS_CLAIRVOYANT_VISIBLE;
559             }
560 
561             pmap[i][j].flags &= ~(CLAIRVOYANT_VISIBLE | CLAIRVOYANT_DARKENED);
562         }
563     }
564 
565     cursed = (rogue.clairvoyance < 0);
566     if (cursed) {
567         clairvoyanceRadius = (rogue.clairvoyance - 1) * -1;
568         cFlags = CLAIRVOYANT_DARKENED;
569     } else {
570         clairvoyanceRadius = (rogue.clairvoyance > 0) ? rogue.clairvoyance + 1 : 0;
571         cFlags = CLAIRVOYANT_VISIBLE | DISCOVERED;
572     }
573 
574     for (i = max(0, player.xLoc - clairvoyanceRadius); i < min(DCOLS, player.xLoc + clairvoyanceRadius + 1); i++) {
575         for (j = max(0, player.yLoc - clairvoyanceRadius); j < min(DROWS, player.yLoc + clairvoyanceRadius + 1); j++) {
576 
577             dx = (player.xLoc - i);
578             dy = (player.yLoc - j);
579 
580             if (dx*dx + dy*dy < clairvoyanceRadius*clairvoyanceRadius + clairvoyanceRadius
581                 && (pmap[i][j].layers[DUNGEON] != GRANITE || pmap[i][j].flags & DISCOVERED)) {
582 
583                 if (cFlags & DISCOVERED) {
584                     discoverCell(i, j);
585                 }
586                 pmap[i][j].flags |= cFlags;
587                 if (!(pmap[i][j].flags & HAS_PLAYER) && !cursed) {
588                     pmap[i][j].flags &= ~STABLE_MEMORY;
589                 }
590             }
591         }
592     }
593 }
594 
updateTelepathy()595 void updateTelepathy() {
596     short i, j;
597     boolean grid[DCOLS][DROWS];
598 
599     for (i=0; i<DCOLS; i++) {
600         for (j=0; j<DROWS; j++) {
601             pmap[i][j].flags &= ~WAS_TELEPATHIC_VISIBLE;
602             if (pmap[i][j].flags & TELEPATHIC_VISIBLE) {
603                 pmap[i][j].flags |= WAS_TELEPATHIC_VISIBLE;
604             }
605             pmap[i][j].flags &= ~(TELEPATHIC_VISIBLE);
606         }
607     }
608 
609     zeroOutGrid(grid);
610     for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
611         creature *monst = nextCreature(&it);
612         if (monsterRevealed(monst)) {
613             getFOVMask(grid, monst->xLoc, monst->yLoc, 2 * FP_FACTOR, T_OBSTRUCTS_VISION, 0, false);
614             pmap[monst->xLoc][monst->yLoc].flags |= TELEPATHIC_VISIBLE;
615             discoverCell(monst->xLoc, monst->yLoc);
616         }
617     }
618     for (creatureIterator it = iterateCreatures(dormantMonsters); hasNextCreature(it);) {
619         creature *monst = nextCreature(&it);
620         if (monsterRevealed(monst)) {
621             getFOVMask(grid, monst->xLoc, monst->yLoc, 2 * FP_FACTOR, T_OBSTRUCTS_VISION, 0, false);
622             pmap[monst->xLoc][monst->yLoc].flags |= TELEPATHIC_VISIBLE;
623             discoverCell(monst->xLoc, monst->yLoc);
624         }
625     }
626     for (i = 0; i < DCOLS; i++) {
627         for (j = 0; j < DROWS; j++) {
628             if (grid[i][j]) {
629                 pmap[i][j].flags |= TELEPATHIC_VISIBLE;
630                 discoverCell(i, j);
631             }
632         }
633     }
634 }
635 
scentDistance(short x1,short y1,short x2,short y2)636 short scentDistance(short x1, short y1, short x2, short y2) {
637     if (abs(x1 - x2) > abs(y1 - y2)) {
638         return 2 * abs(x1 - x2) + abs(y1 - y2);
639     } else {
640         return abs(x1 - x2) + 2 * abs(y1 - y2);
641     }
642 }
643 
updateScent()644 void updateScent() {
645     short i, j;
646     char grid[DCOLS][DROWS];
647 
648     zeroOutGrid(grid);
649 
650     getFOVMask(grid, player.xLoc, player.yLoc, DCOLS * FP_FACTOR, T_OBSTRUCTS_SCENT, 0, false);
651 
652     for (i=0; i<DCOLS; i++) {
653         for (j=0; j<DROWS; j++) {
654             if (grid[i][j]) {
655                 addScentToCell(i, j, scentDistance(player.xLoc, player.yLoc, i, j));
656             }
657         }
658     }
659     addScentToCell(player.xLoc, player.yLoc, 0);
660 }
661 
armorAggroAdjustment(item * theArmor)662 short armorAggroAdjustment(item *theArmor) {
663     if (!theArmor
664         || !(theArmor->category & ARMOR)) {
665 
666         return 0;
667     }
668     return max(0, armorTable[theArmor->kind].strengthRequired - 12);
669 }
670 
currentAggroValue()671 short currentAggroValue() {
672     // Default value of 14 in the light.
673     short stealthVal = 14;
674 
675     if (player.status[STATUS_INVISIBLE]) {
676         stealthVal = 1; // Invisibility means stealth range of 1, no matter what.
677     } else {
678         if (playerInDarkness()) {
679             // In darkness, halve, rounded down.
680             stealthVal = stealthVal / 2;
681         }
682         if (pmap[player.xLoc][player.yLoc].flags & IS_IN_SHADOW) {
683             // When not standing in a lit area, halve, rounded down (stacks with darkness halving).
684             stealthVal = stealthVal / 2;
685         }
686 
687         // Add 1 for each point of your armor's natural (unenchanted) strength requirement above 12.
688         stealthVal += armorAggroAdjustment(rogue.armor);
689 
690         // Halve (rounded up) if you just rested.
691         if (rogue.justRested) {
692             stealthVal = (stealthVal + 1) / 2;
693         }
694 
695         if (player.status[STATUS_AGGRAVATING] > 0) {
696             stealthVal += player.status[STATUS_AGGRAVATING];
697         }
698 
699         // Subtract your bonuses from rings of stealth.
700         // (Cursed rings of stealth will end up adding here.)
701         stealthVal -= rogue.stealthBonus;
702 
703         // Can't go below 2 unless you just rested.
704         if (stealthVal < 2 && !rogue.justRested) {
705             stealthVal = 2;
706         } else if (stealthVal < 1) { // Can't go below 1, ever.
707             stealthVal = 1;
708         }
709     }
710     return stealthVal;
711 }
712 
demoteVisibility()713 void demoteVisibility() {
714     short i, j;
715 
716     for (i=0; i<DCOLS; i++) {
717         for (j=0; j<DROWS; j++) {
718             pmap[i][j].flags &= ~WAS_VISIBLE;
719             if (pmap[i][j].flags & VISIBLE) {
720                 pmap[i][j].flags &= ~VISIBLE;
721                 pmap[i][j].flags |= WAS_VISIBLE;
722             }
723         }
724     }
725 }
726 
discoverCell(const short x,const short y)727 void discoverCell(const short x, const short y) {
728     pmap[x][y].flags &= ~STABLE_MEMORY;
729     if (!(pmap[x][y].flags & DISCOVERED)) {
730         pmap[x][y].flags |= DISCOVERED;
731         if (!cellHasTerrainFlag(x, y, T_PATHING_BLOCKER)) {
732             rogue.xpxpThisTurn++;
733         }
734     }
735 }
736 
updateVision(boolean refreshDisplay)737 void updateVision(boolean refreshDisplay) {
738     short i, j;
739     char grid[DCOLS][DROWS];
740     item *theItem;
741 
742     demoteVisibility();
743     for (i=0; i<DCOLS; i++) {
744         for (j=0; j<DROWS; j++) {
745             pmap[i][j].flags &= ~IN_FIELD_OF_VIEW;
746         }
747     }
748 
749     // Calculate player's field of view (distinct from what is visible, as lighting hasn't been done yet).
750     zeroOutGrid(grid);
751     getFOVMask(grid, player.xLoc, player.yLoc, (DCOLS + DROWS) * FP_FACTOR, (T_OBSTRUCTS_VISION), 0, false);
752     for (i=0; i<DCOLS; i++) {
753         for (j=0; j<DROWS; j++) {
754             if (grid[i][j]) {
755                 pmap[i][j].flags |= IN_FIELD_OF_VIEW;
756             }
757         }
758     }
759     pmap[player.xLoc][player.yLoc].flags |= IN_FIELD_OF_VIEW | VISIBLE;
760 
761     if (rogue.clairvoyance < 0) {
762         discoverCell(player.xLoc, player.yLoc);
763     }
764 
765     if (rogue.clairvoyance != 0) {
766         updateClairvoyance();
767     }
768 
769     updateTelepathy();
770     updateLighting();
771     updateFieldOfViewDisplay(true, refreshDisplay);
772 
773     //  for (i=0; i<DCOLS; i++) {
774     //      for (j=0; j<DROWS; j++) {
775     //          if (pmap[i][j].flags & VISIBLE) {
776     //              plotCharWithColor(' ', mapToWindowX(i), mapToWindowY(j), &yellow, &yellow);
777     //          } else if (pmap[i][j].flags & IN_FIELD_OF_VIEW) {
778     //              plotCharWithColor(' ', mapToWindowX(i), mapToWindowY(j), &blue, &blue);
779     //          }
780     //      }
781     //  }
782     //  displayMoreSign();
783 
784     if (player.status[STATUS_HALLUCINATING] > 0) {
785         for (theItem = floorItems->nextItem; theItem != NULL; theItem = theItem->nextItem) {
786             if ((pmap[theItem->xLoc][theItem->yLoc].flags & DISCOVERED) && refreshDisplay) {
787                 refreshDungeonCell(theItem->xLoc, theItem->yLoc);
788             }
789         }
790         for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
791             creature *monst = nextCreature(&it);
792             if ((pmap[monst->xLoc][monst->yLoc].flags & DISCOVERED) && refreshDisplay) {
793                 refreshDungeonCell(monst->xLoc, monst->yLoc);
794             }
795         }
796     }
797 }
798 
checkNutrition()799 void checkNutrition() {
800     item *theItem;
801     char buf[DCOLS*3], foodWarning[DCOLS*3];
802 
803     if (numberOfMatchingPackItems(FOOD, 0, 0, false) == 0) {
804         sprintf(foodWarning, " and have no food");
805     } else {
806         foodWarning[0] = '\0';
807     }
808 
809     if (player.status[STATUS_NUTRITION] == HUNGER_THRESHOLD) {
810         player.status[STATUS_NUTRITION]--;
811         sprintf(buf, "you are hungry%s.", foodWarning);
812         message(buf, foodWarning[0] ? REQUIRE_ACKNOWLEDGMENT : 0);
813     } else if (player.status[STATUS_NUTRITION] == WEAK_THRESHOLD) {
814         player.status[STATUS_NUTRITION]--;
815         sprintf(buf, "you feel weak with hunger%s.", foodWarning);
816         message(buf, REQUIRE_ACKNOWLEDGMENT);
817     } else if (player.status[STATUS_NUTRITION] == FAINT_THRESHOLD) {
818         player.status[STATUS_NUTRITION]--;
819         sprintf(buf, "you feel faint with hunger%s.", foodWarning);
820         message(buf, REQUIRE_ACKNOWLEDGMENT);
821     } else if (player.status[STATUS_NUTRITION] <= 1) {
822         // Force the player to eat something if he has it
823         for (theItem = packItems->nextItem; theItem != NULL; theItem = theItem->nextItem) {
824             if (theItem->category == FOOD) {
825                 sprintf(buf, "unable to control your hunger, you eat a %s.", (theItem->kind == FRUIT ? "mango" : "ration of food"));
826                 messageWithColor(buf, &itemMessageColor, REQUIRE_ACKNOWLEDGMENT);
827                 apply(theItem, false);
828                 break;
829             }
830         }
831     }
832 
833     if (player.status[STATUS_NUTRITION] == 1) { // Didn't manage to eat any food above.
834         player.status[STATUS_NUTRITION] = 0;    // So the status bar changes in time for the message:
835         message("you are starving to death!", REQUIRE_ACKNOWLEDGMENT);
836     }
837 }
838 
burnItem(item * theItem)839 void burnItem(item *theItem) {
840     short x, y;
841     char buf1[COLS * 3], buf2[COLS * 3];
842     itemName(theItem, buf1, false, true, NULL);
843     sprintf(buf2, "%s burn%s up!",
844             buf1,
845             theItem->quantity == 1 ? "s" : "");
846     x = theItem->xLoc;
847     y = theItem->yLoc;
848     removeItemFromChain(theItem, floorItems);
849     deleteItem(theItem);
850     pmap[x][y].flags &= ~(HAS_ITEM | ITEM_DETECTED);
851     if (pmap[x][y].flags & (ANY_KIND_OF_VISIBLE | DISCOVERED | ITEM_DETECTED)) {
852         refreshDungeonCell(x, y);
853     }
854     if (playerCanSee(x, y)) {
855         messageWithColor(buf2, &itemMessageColor, 0);
856     }
857     spawnDungeonFeature(x, y, &(dungeonFeatureCatalog[DF_ITEM_FIRE]), true, false);
858 }
859 
flashCreatureAlert(creature * monst,char msg[200],color * foreColor,color * backColor)860 void flashCreatureAlert(creature *monst, char msg[200], color *foreColor, color *backColor) {
861     short x, y;
862     if (monst->yLoc > DROWS / 2) {
863         y = mapToWindowY(monst->yLoc - 2);
864     } else {
865         y = mapToWindowY(monst->yLoc + 2);
866     }
867     x = mapToWindowX(monst->xLoc - strLenWithoutEscapes(msg) / 2);
868     if (x > COLS - strLenWithoutEscapes(msg)) {
869         x = COLS - strLenWithoutEscapes(msg);
870     }
871     flashMessage(msg, x, y, (rogue.playbackMode ? 100 : 1000), foreColor, backColor);
872     rogue.disturbed = true;
873 }
874 
handleHealthAlerts()875 void handleHealthAlerts() {
876     short i, currentPercent, previousPercent,
877     thresholds[] = {5, 10, 25, 40},
878     pThresholds[] = {100, 90, 50};
879     char buf[DCOLS];
880 
881     const short healthThresholdsCount = 4,
882     poisonThresholdsCount = 3;
883 
884     assureCosmeticRNG;
885 
886     currentPercent = player.currentHP * 100 / player.info.maxHP;
887     previousPercent = player.previousHealthPoints * 100 / player.info.maxHP;
888 
889     if (currentPercent < previousPercent && !rogue.gameHasEnded) {
890         for (i=0; i < healthThresholdsCount; i++) {
891             if (currentPercent < thresholds[i] && previousPercent >= thresholds[i]) {
892                 sprintf(buf, " <%i%% health ", thresholds[i]);
893                 flashCreatureAlert(&player, buf, &badMessageColor, &darkRed);
894                 break;
895             }
896         }
897     }
898 
899     if (!rogue.gameHasEnded) {
900         currentPercent = player.status[STATUS_POISONED] * player.poisonAmount * 100 / player.currentHP;
901 
902         if (currentPercent > rogue.previousPoisonPercent && !rogue.gameHasEnded) {
903             for (i=0; i < poisonThresholdsCount; i++) {
904                 if (currentPercent > pThresholds[i] && rogue.previousPoisonPercent <= pThresholds[i]) {
905                     if (currentPercent < 100) {
906                         sprintf(buf, " >%i%% poisoned ", pThresholds[i]);
907                     } else {
908                         strcpy(buf, " Fatally poisoned ");
909                     }
910                     flashCreatureAlert(&player, buf, &yellow, &darkGreen);
911                     break;
912                 }
913             }
914         }
915         rogue.previousPoisonPercent = currentPercent;
916     }
917 
918     restoreRNG;
919 }
920 
addXPXPToAlly(short XPXP,creature * monst)921 void addXPXPToAlly(short XPXP, creature *monst) {
922     char theMonsterName[100], buf[200];
923     if (!(monst->info.flags & (MONST_INANIMATE | MONST_IMMOBILE))
924         && !(monst->bookkeepingFlags & MB_TELEPATHICALLY_REVEALED)
925         && monst->creatureState == MONSTER_ALLY
926         && monst->spawnDepth <= rogue.depthLevel
927         && rogue.depthLevel <= AMULET_LEVEL) {
928 
929         monst->xpxp += XPXP;
930         //printf("\n%i xpxp added to your %s this turn.", rogue.xpxpThisTurn, monst->info.monsterName);
931         if (monst->xpxp >= XPXP_NEEDED_FOR_TELEPATHIC_BOND
932             && !(monst->bookkeepingFlags & MB_TELEPATHICALLY_REVEALED)) {
933 
934             monst->bookkeepingFlags |= MB_TELEPATHICALLY_REVEALED;
935             updateVision(true);
936             monsterName(theMonsterName, monst, false);
937             sprintf(buf, "you have developed a telepathic bond with your %s.", theMonsterName);
938             messageWithColor(buf, &advancementMessageColor, 0);
939         }
940         if (monst->xpxp > 1500 * 20) {
941             rogue.featRecord[FEAT_COMPANION] = true;
942         }
943     }
944 }
945 
handleXPXP()946 void handleXPXP() {
947     //char buf[DCOLS*2], theMonsterName[50];
948 
949     for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
950         creature *monst = nextCreature(&it);
951         addXPXPToAlly(rogue.xpxpThisTurn, monst);
952     }
953     if (rogue.depthLevel > 1) {
954         for (creatureIterator it = iterateCreatures(&levels[rogue.depthLevel - 2].monsters); hasNextCreature(it);) {
955             creature *monst = nextCreature(&it);
956             addXPXPToAlly(rogue.xpxpThisTurn, monst);
957         }
958     }
959     if (rogue.depthLevel < DEEPEST_LEVEL) {
960         for (creatureIterator it = iterateCreatures(&levels[rogue.depthLevel].monsters); hasNextCreature(it);) {
961             creature *monst = nextCreature(&it);
962             addXPXPToAlly(rogue.xpxpThisTurn, monst);
963         }
964     }
965     rogue.xpxpThisTurn = 0;
966 }
967 
playerFalls()968 void playerFalls() {
969     short damage;
970     short layer;
971 
972     if (cellHasTMFlag(player.xLoc, player.yLoc, TM_IS_SECRET)
973         && playerCanSee(player.xLoc, player.yLoc)) {
974 
975         discover(player.xLoc, player.yLoc);
976     }
977 
978     monstersFall(); // Monsters must fall with the player rather than getting suspended on the previous level.
979     updateFloorItems(); // Likewise, items should fall with the player rather than getting suspended above.
980 
981     layer = layerWithFlag(player.xLoc, player.yLoc, T_AUTO_DESCENT);
982     if (layer >= 0) {
983         message(tileCatalog[pmap[player.xLoc][player.yLoc].layers[layer]].flavorText, REQUIRE_ACKNOWLEDGMENT);
984     } else if (layer == -1) {
985         message("You plunge downward!", REQUIRE_ACKNOWLEDGMENT);
986     }
987 
988     player.bookkeepingFlags &= ~(MB_IS_FALLING | MB_SEIZED | MB_SEIZING);
989     rogue.disturbed = true;
990 
991     if (rogue.depthLevel < DEEPEST_LEVEL) {
992         rogue.depthLevel++;
993         startLevel(rogue.depthLevel - 1, 0);
994         damage = randClumpedRange(FALL_DAMAGE_MIN, FALL_DAMAGE_MAX, 2);
995         boolean killed = false;
996         if (terrainFlags(player.xLoc, player.yLoc) & T_IS_DEEP_WATER) {
997             messageWithColor("You fall into deep water, unharmed.", &badMessageColor, 0);
998         } else {
999             if (cellHasTMFlag(player.xLoc, player.yLoc, TM_ALLOWS_SUBMERGING)) {
1000                 damage /= 2; // falling into liquid (shallow water, bog, etc.) hurts less than hitting hard floor
1001             }
1002             messageWithColor("You are injured by the fall.", &badMessageColor, 0);
1003             if (inflictDamage(NULL, &player, damage, &red, false)) {
1004                 gameOver("Killed by a fall", true);
1005                 killed = true;
1006             }
1007         }
1008         if (!killed && rogue.depthLevel > rogue.deepestLevel) {
1009             rogue.deepestLevel = rogue.depthLevel;
1010         }
1011     } else {
1012         message("A strange force seizes you as you fall.", 0);
1013         teleport(&player, -1, -1, true);
1014     }
1015     createFlare(player.xLoc, player.yLoc, GENERIC_FLASH_LIGHT);
1016     animateFlares(rogue.flares, rogue.flareCount);
1017     rogue.flareCount = 0;
1018 }
1019 
1020 
1021 
activateMachine(short machineNumber)1022 void activateMachine(short machineNumber) {
1023     short i, j, x, y, layer, sRows[DROWS], sCols[DCOLS], monsterCount, maxMonsters;
1024 
1025     fillSequentialList(sCols, DCOLS);
1026     shuffleList(sCols, DCOLS);
1027     fillSequentialList(sRows, DROWS);
1028     shuffleList(sRows, DROWS);
1029 
1030     for (i=0; i<DCOLS; i++) {
1031         for (j=0; j<DROWS; j++) {
1032             x = sCols[i];
1033             y = sRows[j];
1034             if ((pmap[x][y].flags & IS_IN_MACHINE)
1035                 && pmap[x][y].machineNumber == machineNumber
1036                 && !(pmap[x][y].flags & IS_POWERED)
1037                 && cellHasTMFlag(x, y, TM_IS_WIRED)) {
1038 
1039                 pmap[x][y].flags |= IS_POWERED;
1040                 for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
1041                     if (tileCatalog[pmap[x][y].layers[layer]].mechFlags & TM_IS_WIRED) {
1042                         promoteTile(x, y, layer, false);
1043                     }
1044                 }
1045             }
1046         }
1047     }
1048 
1049     monsterCount = maxMonsters = 0;
1050     creature **activatedMonsterList = NULL;
1051     for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
1052         creature *monst = nextCreature(&it);
1053         if (monst->machineHome == machineNumber
1054             && monst->spawnDepth == rogue.depthLevel
1055             && (monst->info.flags & MONST_GETS_TURN_ON_ACTIVATION)) {
1056 
1057             monsterCount++;
1058 
1059             if (monsterCount > maxMonsters) {
1060                 maxMonsters += 10;
1061                 activatedMonsterList = realloc(activatedMonsterList, sizeof(creature *) * maxMonsters);
1062             }
1063             activatedMonsterList[monsterCount - 1] = monst;
1064         }
1065     }
1066     for (i=0; i<monsterCount; i++) {
1067         if (!(activatedMonsterList[i]->bookkeepingFlags & MB_IS_DYING)) {
1068             monstersTurn(activatedMonsterList[i]);
1069         }
1070     }
1071 
1072     if (activatedMonsterList) {
1073         free(activatedMonsterList);
1074     }
1075 }
1076 
circuitBreakersPreventActivation(short machineNumber)1077 boolean circuitBreakersPreventActivation(short machineNumber) {
1078     short i, j;
1079     for (i=0; i<DCOLS; i++) {
1080         for (j=0; j<DROWS; j++) {
1081             if (pmap[i][j].machineNumber == machineNumber
1082                 && cellHasTMFlag(i, j, TM_IS_CIRCUIT_BREAKER)) {
1083 
1084                 return true;
1085             }
1086         }
1087     }
1088     return false;
1089 }
1090 
promoteTile(short x,short y,enum dungeonLayers layer,boolean useFireDF)1091 void promoteTile(short x, short y, enum dungeonLayers layer, boolean useFireDF) {
1092     short i, j;
1093     enum dungeonFeatureTypes DFType;
1094     floorTileType *tile;
1095 
1096     tile = &(tileCatalog[pmap[x][y].layers[layer]]);
1097 
1098     DFType = (useFireDF ? tile->fireType : tile->promoteType);
1099 
1100     if ((tile->mechFlags & TM_VANISHES_UPON_PROMOTION)) {
1101         if (tileCatalog[pmap[x][y].layers[layer]].flags & T_PATHING_BLOCKER) {
1102             rogue.staleLoopMap = true;
1103         }
1104         pmap[x][y].layers[layer] = (layer == DUNGEON ? FLOOR : NOTHING); // even the dungeon layer implicitly has floor underneath it
1105         if (layer == GAS) {
1106             pmap[x][y].volume = 0;
1107         }
1108         refreshDungeonCell(x, y);
1109     }
1110     if (DFType) {
1111         spawnDungeonFeature(x, y, &dungeonFeatureCatalog[DFType], true, false);
1112     }
1113 
1114     if (!useFireDF && (tile->mechFlags & TM_IS_WIRED)
1115         && !(pmap[x][y].flags & IS_POWERED)
1116         && !circuitBreakersPreventActivation(pmap[x][y].machineNumber)) {
1117         // Send power through all cells in the same machine that are not IS_POWERED,
1118         // and on any such cell, promote each terrain layer that is T_IS_WIRED.
1119         // Note that machines need not be contiguous.
1120         pmap[x][y].flags |= IS_POWERED;
1121         activateMachine(pmap[x][y].machineNumber); // It lives!!!
1122 
1123         // Power fades from the map immediately after we finish.
1124         for (i=0; i<DCOLS; i++) {
1125             for (j=0; j<DROWS; j++) {
1126                 pmap[i][j].flags &= ~IS_POWERED;
1127             }
1128         }
1129     }
1130 }
1131 
exposeTileToElectricity(short x,short y)1132 boolean exposeTileToElectricity(short x, short y) {
1133     enum dungeonLayers layer;
1134     boolean promotedSomething = false;
1135 
1136     if (!cellHasTMFlag(x, y, TM_PROMOTES_ON_ELECTRICITY)) {
1137         return false;
1138     }
1139     for (layer=0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
1140         if (tileCatalog[pmap[x][y].layers[layer]].mechFlags & TM_PROMOTES_ON_ELECTRICITY) {
1141             promoteTile(x, y, layer, false);
1142             promotedSomething = true;
1143         }
1144     }
1145     return promotedSomething;
1146 }
1147 
exposeTileToFire(short x,short y,boolean alwaysIgnite)1148 boolean exposeTileToFire(short x, short y, boolean alwaysIgnite) {
1149     enum dungeonLayers layer;
1150     short ignitionChance = 0, bestExtinguishingPriority = 1000, explosiveNeighborCount = 0;
1151     short newX, newY;
1152     enum directions dir;
1153     boolean fireIgnited = false, explosivePromotion = false;
1154 
1155     if (!cellHasTerrainFlag(x, y, T_IS_FLAMMABLE) || pmap[x][y].exposedToFire >= 12) {
1156         return false;
1157     }
1158 
1159     pmap[x][y].exposedToFire++;
1160 
1161     // Pick the extinguishing layer with the best priority.
1162     for (layer=0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
1163         if ((tileCatalog[pmap[x][y].layers[layer]].mechFlags & TM_EXTINGUISHES_FIRE)
1164             && tileCatalog[pmap[x][y].layers[layer]].drawPriority < bestExtinguishingPriority) {
1165             bestExtinguishingPriority = tileCatalog[pmap[x][y].layers[layer]].drawPriority;
1166         }
1167     }
1168 
1169     // Pick the fire type of the most flammable layer that is either gas or equal-or-better priority than the best extinguishing layer.
1170     for (layer=0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
1171         if ((tileCatalog[pmap[x][y].layers[layer]].flags & T_IS_FLAMMABLE)
1172             && (layer == GAS || tileCatalog[pmap[x][y].layers[layer]].drawPriority <= bestExtinguishingPriority)
1173             && tileCatalog[pmap[x][y].layers[layer]].chanceToIgnite > ignitionChance) {
1174             ignitionChance = tileCatalog[pmap[x][y].layers[layer]].chanceToIgnite;
1175         }
1176     }
1177 
1178     if (alwaysIgnite || (ignitionChance && rand_percent(ignitionChance))) { // If it ignites...
1179         fireIgnited = true;
1180 
1181         // Count explosive neighbors.
1182         if (cellHasTMFlag(x, y, TM_EXPLOSIVE_PROMOTE)) {
1183             for (dir = 0, explosiveNeighborCount = 0; dir < DIRECTION_COUNT; dir++) {
1184                 newX = x + nbDirs[dir][0];
1185                 newY = y + nbDirs[dir][1];
1186                 if (coordinatesAreInMap(newX, newY)
1187                     && (cellHasTerrainFlag(newX, newY, T_IS_FIRE | T_OBSTRUCTS_GAS) || cellHasTMFlag(newX, newY, TM_EXPLOSIVE_PROMOTE))) {
1188 
1189                     explosiveNeighborCount++;
1190                 }
1191             }
1192             if (explosiveNeighborCount >= 8) {
1193                 explosivePromotion = true;
1194             }
1195         }
1196 
1197         // Flammable layers are consumed.
1198         for (layer=0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
1199             if (tileCatalog[pmap[x][y].layers[layer]].flags & T_IS_FLAMMABLE) {
1200                 // pmap[x][y].layers[GAS] is not cleared here, which is a bug.
1201                 // We preserve the layer anyways because this results in nicer gas burning behavior.
1202                 if (layer == GAS) {
1203                     pmap[x][y].volume = 0; // Flammable gas burns its volume away.
1204                 }
1205                 promoteTile(x, y, layer, !explosivePromotion);
1206             }
1207         }
1208         refreshDungeonCell(x, y);
1209     }
1210     return fireIgnited;
1211 }
1212 
1213 // Only the gas layer can be volumetric.
updateVolumetricMedia()1214 void updateVolumetricMedia() {
1215     short i, j, newX, newY, numSpaces;
1216     unsigned long highestNeighborVolume;
1217     unsigned long sum;
1218     enum tileType gasType;
1219     enum directions dir;
1220     unsigned short newGasVolume[DCOLS][DROWS];
1221 
1222     for (i=0; i<DCOLS; i++) {
1223         for (j=0; j<DROWS; j++) {
1224             newGasVolume[i][j] = 0;
1225         }
1226     }
1227 
1228     for (i=0; i<DCOLS; i++) {
1229         for (j=0; j<DROWS; j++) {
1230             if (!cellHasTerrainFlag(i, j, T_OBSTRUCTS_GAS)) {
1231                 sum = pmap[i][j].volume;
1232                 numSpaces = 1;
1233                 highestNeighborVolume = pmap[i][j].volume;
1234                 gasType = pmap[i][j].layers[GAS];
1235                 for (dir=0; dir< DIRECTION_COUNT; dir++) {
1236                     newX = i + nbDirs[dir][0];
1237                     newY = j + nbDirs[dir][1];
1238                     if (coordinatesAreInMap(newX, newY)
1239                         && !cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_GAS)) {
1240 
1241                         sum += pmap[newX][newY].volume;
1242                         numSpaces++;
1243                         if (pmap[newX][newY].volume > highestNeighborVolume) {
1244                             highestNeighborVolume = pmap[newX][newY].volume;
1245                             gasType = pmap[newX][newY].layers[GAS];
1246                         }
1247                     }
1248                 }
1249                 if (cellHasTerrainFlag(i, j, T_AUTO_DESCENT)) { // if it's a chasm tile or trap door,
1250                     numSpaces++; // this will allow gas to escape from the level entirely
1251                 }
1252                 newGasVolume[i][j] += sum / max(1, numSpaces);
1253                 if ((unsigned) rand_range(0, numSpaces - 1) < (sum % numSpaces)) {
1254                     newGasVolume[i][j]++; // stochastic rounding
1255                 }
1256                 if (pmap[i][j].layers[GAS] != gasType && newGasVolume[i][j] > 3) {
1257                     if (pmap[i][j].layers[GAS] != NOTHING) {
1258                         newGasVolume[i][j] = min(3, newGasVolume[i][j]); // otherwise interactions between gases are crazy
1259                     }
1260                     pmap[i][j].layers[GAS] = gasType;
1261                 } else if (pmap[i][j].layers[GAS] && newGasVolume[i][j] < 1) {
1262                     pmap[i][j].layers[GAS] = NOTHING;
1263                     refreshDungeonCell(i, j);
1264                 }
1265                 if (pmap[i][j].volume > 0) {
1266                     if (tileCatalog[pmap[i][j].layers[GAS]].mechFlags & TM_GAS_DISSIPATES_QUICKLY) {
1267                         newGasVolume[i][j] -= (rand_percent(50) ? 1 : 0);
1268                     } else if (tileCatalog[pmap[i][j].layers[GAS]].mechFlags & TM_GAS_DISSIPATES) {
1269                         newGasVolume[i][j] -= (rand_percent(20) ? 1 : 0);
1270                     }
1271                 }
1272             } else if (pmap[i][j].volume > 0) { // if has gas but can't hold gas,
1273                 // disperse gas instantly into neighboring tiles that can hold gas
1274                 numSpaces = 0;
1275                 for (dir = 0; dir < DIRECTION_COUNT; dir++) {
1276                     newX = i + nbDirs[dir][0];
1277                     newY = j + nbDirs[dir][1];
1278                     if (coordinatesAreInMap(newX, newY)
1279                         && !cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_GAS)) {
1280 
1281                         numSpaces++;
1282                     }
1283                 }
1284                 if (numSpaces > 0) {
1285                     for (dir = 0; dir < DIRECTION_COUNT; dir++) {
1286                         newX = i + nbDirs[dir][0];
1287                         newY = j + nbDirs[dir][1];
1288                         if (coordinatesAreInMap(newX, newY)
1289                             && !cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_GAS)) {
1290 
1291                             newGasVolume[newX][newY] += (pmap[i][j].volume / numSpaces);
1292                             if (pmap[i][j].volume / numSpaces) {
1293                                 pmap[newX][newY].layers[GAS] = pmap[i][j].layers[GAS];
1294                             }
1295                         }
1296                     }
1297                 }
1298                 newGasVolume[i][j] = 0;
1299                 pmap[i][j].layers[GAS] = NOTHING;
1300             }
1301         }
1302     }
1303 
1304     for (i=0; i<DCOLS; i++) {
1305         for (j=0; j<DROWS; j++) {
1306             if (pmap[i][j].volume != newGasVolume[i][j]) {
1307                 pmap[i][j].volume = newGasVolume[i][j];
1308                 refreshDungeonCell(i, j);
1309             }
1310         }
1311     }
1312 }
1313 
updateYendorWardenTracking()1314 void updateYendorWardenTracking() {
1315     short n;
1316 
1317     if (!rogue.yendorWarden) {
1318         return;
1319     }
1320     if (rogue.yendorWarden->depth == rogue.depthLevel) {
1321         return;
1322     }
1323     if (!(rogue.yendorWarden->bookkeepingFlags & MB_PREPLACED)) {
1324         levels[rogue.yendorWarden->depth - 1].mapStorage[rogue.yendorWarden->xLoc][rogue.yendorWarden->yLoc].flags &= ~HAS_MONSTER;
1325     }
1326     n = rogue.yendorWarden->depth - 1;
1327 
1328     // remove traversing monster from other level monster chain
1329     removeCreature(&levels[n].monsters, rogue.yendorWarden);
1330 
1331     if (rogue.yendorWarden->depth > rogue.depthLevel) {
1332         rogue.yendorWarden->depth = rogue.depthLevel + 1;
1333         n = rogue.yendorWarden->depth - 1;
1334         rogue.yendorWarden->bookkeepingFlags |= MB_APPROACHING_UPSTAIRS;
1335         rogue.yendorWarden->xLoc = levels[n].downStairsLoc[0];
1336         rogue.yendorWarden->yLoc = levels[n].downStairsLoc[1];
1337     } else {
1338         rogue.yendorWarden->depth = rogue.depthLevel - 1;
1339         n = rogue.yendorWarden->depth - 1;
1340         rogue.yendorWarden->bookkeepingFlags |= MB_APPROACHING_DOWNSTAIRS;
1341         rogue.yendorWarden->xLoc = levels[n].upStairsLoc[0];
1342         rogue.yendorWarden->yLoc = levels[n].upStairsLoc[1];
1343     }
1344     prependCreature(&levels[rogue.yendorWarden->depth - 1].monsters, rogue.yendorWarden);
1345     rogue.yendorWarden->bookkeepingFlags |= MB_PREPLACED;
1346     rogue.yendorWarden->status[STATUS_ENTERS_LEVEL_IN] = 50;
1347 }
1348 
1349 // Monsters who are over chasms or other descent tiles won't fall until this is called.
1350 // This is to avoid having the monster chain change unpredictably in the middle of a turn.
monstersFall()1351 void monstersFall() {
1352     short x, y;
1353     char buf[DCOLS], buf2[DCOLS];
1354 
1355     // monsters plunge into chasms at the end of the turn
1356     for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
1357         creature *monst = nextCreature(&it);
1358         if ((monst->bookkeepingFlags & MB_IS_FALLING) || monsterShouldFall(monst)) {
1359             monst->bookkeepingFlags |= MB_IS_FALLING;
1360 
1361             x = monst->xLoc;
1362             y = monst->yLoc;
1363 
1364             if (canSeeMonster(monst)) {
1365                 monsterName(buf, monst, true);
1366                 sprintf(buf2, "%s plunges out of sight!", buf);
1367                 messageWithColor(buf2, messageColorFromVictim(monst), 0);
1368             }
1369 
1370             if (monst->info.flags & MONST_GETS_TURN_ON_ACTIVATION) {
1371                 // Guardians and mirrored totems never survive the fall. If they did, they might block the level below.
1372                 killCreature(monst, false);
1373             } else if (!inflictDamage(NULL, monst, randClumpedRange(6, 12, 2), &red, false)) {
1374                 demoteMonsterFromLeadership(monst);
1375 
1376                 monst->status[STATUS_ENTRANCED] = 0;
1377                 monst->bookkeepingFlags |= MB_PREPLACED;
1378                 monst->bookkeepingFlags &= ~(MB_IS_FALLING | MB_SEIZED | MB_SEIZING);
1379                 monst->targetCorpseLoc[0] = monst->targetCorpseLoc[1] = 0;
1380 
1381                 // remove from monster chain
1382                 removeCreature(monsters, monst);
1383 
1384                 // add to next level's chain
1385                 prependCreature(&levels[rogue.depthLevel-1 + 1].monsters, monst);
1386 
1387                 monst->depth = rogue.depthLevel + 1;
1388 
1389                 if (monst == rogue.yendorWarden) {
1390                     updateYendorWardenTracking();
1391                 }
1392             }
1393 
1394             pmap[x][y].flags &= ~HAS_MONSTER;
1395             refreshDungeonCell(x, y);
1396         }
1397     }
1398 }
1399 
updateEnvironment()1400 void updateEnvironment() {
1401     short i, j, direction, newX, newY, promotions[DCOLS][DROWS];
1402     long promoteChance;
1403     enum dungeonLayers layer;
1404     floorTileType *tile;
1405     boolean isVolumetricGas = false;
1406 
1407     monstersFall();
1408 
1409     // reset exposedToFire
1410     for (i=0; i<DCOLS; i++) {
1411         for (j=0; j<DROWS; j++) {
1412             pmap[i][j].exposedToFire = 0;
1413         }
1414     }
1415 
1416     // update gases twice
1417     for (i=0; i<DCOLS && !isVolumetricGas; i++) {
1418         for (j=0; j<DROWS && !isVolumetricGas; j++) {
1419             if (!isVolumetricGas && pmap[i][j].layers[GAS]) {
1420                 isVolumetricGas = true;
1421             }
1422         }
1423     }
1424     if (isVolumetricGas) {
1425         updateVolumetricMedia();
1426         updateVolumetricMedia();
1427     }
1428 
1429     // Do random tile promotions in two passes to keep generations distinct.
1430     // First pass, make a note of each terrain layer at each coordinate that is going to promote:
1431     for (i=0; i<DCOLS; i++) {
1432         for (j=0; j<DROWS; j++) {
1433             promotions[i][j] = 0;
1434             for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
1435                 tile = &(tileCatalog[pmap[i][j].layers[layer]]);
1436                 if (tile->promoteChance < 0) {
1437                     promoteChance = 0;
1438                     for (direction = 0; direction < 4; direction++) {
1439                         if (coordinatesAreInMap(i + nbDirs[direction][0], j + nbDirs[direction][1])
1440                             && !cellHasTerrainFlag(i + nbDirs[direction][0], j + nbDirs[direction][1], T_OBSTRUCTS_PASSABILITY)
1441                             && pmap[i + nbDirs[direction][0]][j + nbDirs[direction][1]].layers[layer] != pmap[i][j].layers[layer]
1442                             && !(pmap[i][j].flags & CAUGHT_FIRE_THIS_TURN)) {
1443                             promoteChance += -1 * tile->promoteChance;
1444                         }
1445                     }
1446                 } else {
1447                     promoteChance = tile->promoteChance;
1448                 }
1449                 if (promoteChance
1450                     && !(pmap[i][j].flags & CAUGHT_FIRE_THIS_TURN)
1451                     && rand_range(0, 10000) < promoteChance) {
1452                     promotions[i][j] |= Fl(layer);
1453                     //promoteTile(i, j, layer, false);
1454                 }
1455             }
1456         }
1457     }
1458     // Second pass, do the promotions:
1459     for (i=0; i<DCOLS; i++) {
1460         for (j=0; j<DROWS; j++) {
1461             for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
1462                 if ((promotions[i][j] & Fl(layer))) {
1463                     //&& (tileCatalog[pmap[i][j].layers[layer]].promoteChance != 0)){
1464                     // make sure that it's still a promotable layer
1465                     promoteTile(i, j, layer, false);
1466                 }
1467             }
1468         }
1469     }
1470 
1471     // Bookkeeping for fire, pressure plates and key-activated tiles.
1472     for (i=0; i<DCOLS; i++) {
1473         for (j=0; j<DROWS; j++) {
1474             pmap[i][j].flags &= ~(CAUGHT_FIRE_THIS_TURN);
1475             if (!(pmap[i][j].flags & (HAS_PLAYER | HAS_MONSTER | HAS_ITEM))
1476                 && (pmap[i][j].flags & PRESSURE_PLATE_DEPRESSED)) {
1477 
1478                 pmap[i][j].flags &= ~PRESSURE_PLATE_DEPRESSED;
1479             }
1480             if (cellHasTMFlag(i, j, TM_PROMOTES_WITHOUT_KEY) && !keyOnTileAt(i, j)) {
1481                 for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
1482                     if (tileCatalog[pmap[i][j].layers[layer]].mechFlags & TM_PROMOTES_WITHOUT_KEY) {
1483                         promoteTile(i, j, layer, false);
1484                     }
1485                 }
1486             }
1487         }
1488     }
1489 
1490     // Update fire.
1491     for (i=0; i<DCOLS; i++) {
1492         for (j=0; j<DROWS; j++) {
1493             if (cellHasTerrainFlag(i, j, T_IS_FIRE) && !(pmap[i][j].flags & CAUGHT_FIRE_THIS_TURN)) {
1494                 exposeTileToFire(i, j, false);
1495                 for (direction=0; direction<4; direction++) {
1496                     newX = i + nbDirs[direction][0];
1497                     newY = j + nbDirs[direction][1];
1498                     if (coordinatesAreInMap(newX, newY)) {
1499                         exposeTileToFire(newX, newY, false);
1500                     }
1501                 }
1502             }
1503         }
1504     }
1505 
1506     // Terrain that affects items and vice versa
1507     updateFloorItems();
1508 }
1509 
updateAllySafetyMap()1510 void updateAllySafetyMap() {
1511     short i, j;
1512     short **playerCostMap, **monsterCostMap;
1513 
1514     rogue.updatedAllySafetyMapThisTurn = true;
1515 
1516     playerCostMap = allocGrid();
1517     monsterCostMap = allocGrid();
1518 
1519     for (i=0; i<DCOLS; i++) {
1520         for (j=0; j<DROWS; j++) {
1521             allySafetyMap[i][j] = 30000;
1522 
1523             playerCostMap[i][j] = monsterCostMap[i][j] = 1;
1524 
1525             if (cellHasTerrainFlag(i, j, T_OBSTRUCTS_PASSABILITY)
1526                 && (!cellHasTMFlag(i, j, TM_IS_SECRET) || (discoveredTerrainFlagsAtLoc(i, j) & T_OBSTRUCTS_PASSABILITY))) {
1527 
1528                 playerCostMap[i][j] = monsterCostMap[i][j] = cellHasTerrainFlag(i, j, T_OBSTRUCTS_DIAGONAL_MOVEMENT) ? PDS_OBSTRUCTION : PDS_FORBIDDEN;
1529             } else if (cellHasTerrainFlag(i, j, T_PATHING_BLOCKER & ~T_OBSTRUCTS_PASSABILITY)) {
1530                 playerCostMap[i][j] = monsterCostMap[i][j] = PDS_FORBIDDEN;
1531             } else if (cellHasTerrainFlag(i, j, T_SACRED)) {
1532                 playerCostMap[i][j] = 1;
1533                 monsterCostMap[i][j] = PDS_FORBIDDEN;
1534             } else if ((pmap[i][j].flags & HAS_MONSTER) && monstersAreEnemies(&player, monsterAtLoc(i, j))) {
1535                 playerCostMap[i][j] = 1;
1536                 monsterCostMap[i][j] = PDS_FORBIDDEN;
1537                 allySafetyMap[i][j] = 0;
1538             }
1539         }
1540     }
1541 
1542     playerCostMap[player.xLoc][player.yLoc] = PDS_FORBIDDEN;
1543     monsterCostMap[player.xLoc][player.yLoc] = PDS_FORBIDDEN;
1544 
1545     dijkstraScan(allySafetyMap, playerCostMap, false);
1546 
1547     for (i=0; i<DCOLS; i++) {
1548         for (j=0; j<DROWS; j++) {
1549             if (monsterCostMap[i][j] < 0) {
1550                 continue;
1551             }
1552 
1553             if (allySafetyMap[i][j] == 30000) {
1554                 allySafetyMap[i][j] = 150;
1555             }
1556 
1557             allySafetyMap[i][j] = 50 * allySafetyMap[i][j] / (50 + allySafetyMap[i][j]);
1558 
1559             allySafetyMap[i][j] *= -3;
1560 
1561             if (pmap[i][j].flags & IN_LOOP) {
1562                 allySafetyMap[i][j] -= 10;
1563             }
1564         }
1565     }
1566     dijkstraScan(allySafetyMap, monsterCostMap, false);
1567 
1568     freeGrid(playerCostMap);
1569     freeGrid(monsterCostMap);
1570 }
1571 
resetDistanceCellInGrid(short ** grid,short x,short y)1572 void resetDistanceCellInGrid(short **grid, short x, short y) {
1573     enum directions dir;
1574     short newX, newY;
1575     for (dir = 0; dir < 4; dir++) {
1576         newX = x + nbDirs[dir][0];
1577         newY = y + nbDirs[dir][1];
1578         if (coordinatesAreInMap(newX, newY)
1579             && grid[x][y] > grid[newX][newY] + 1) {
1580 
1581             grid[x][y] = grid[newX][newY] + 1;
1582         }
1583     }
1584 }
1585 
updateSafetyMap()1586 void updateSafetyMap() {
1587     short i, j;
1588     short **playerCostMap, **monsterCostMap;
1589     creature *monst;
1590 
1591     rogue.updatedSafetyMapThisTurn = true;
1592 
1593     playerCostMap = allocGrid();
1594     monsterCostMap = allocGrid();
1595 
1596     for (i=0; i<DCOLS; i++) {
1597         for (j=0; j<DROWS; j++) {
1598             safetyMap[i][j] = 30000;
1599 
1600             playerCostMap[i][j] = monsterCostMap[i][j] = 1; // prophylactic
1601 
1602             if (cellHasTerrainFlag(i, j, T_OBSTRUCTS_PASSABILITY)
1603                 && (!cellHasTMFlag(i, j, TM_IS_SECRET) || (discoveredTerrainFlagsAtLoc(i, j) & T_OBSTRUCTS_PASSABILITY))) {
1604 
1605                 playerCostMap[i][j] = monsterCostMap[i][j] = cellHasTerrainFlag(i, j, T_OBSTRUCTS_DIAGONAL_MOVEMENT) ? PDS_OBSTRUCTION : PDS_FORBIDDEN;
1606             } else if (cellHasTerrainFlag(i, j, T_SACRED)) {
1607                 playerCostMap[i][j] = 1;
1608                 monsterCostMap[i][j] = PDS_FORBIDDEN;
1609             } else if (cellHasTerrainFlag(i, j, T_LAVA_INSTA_DEATH)) {
1610                 monsterCostMap[i][j] = PDS_FORBIDDEN;
1611                 if (player.status[STATUS_LEVITATING] || !player.status[STATUS_IMMUNE_TO_FIRE]) {
1612                     playerCostMap[i][j] = 1;
1613                 } else {
1614                     playerCostMap[i][j] = PDS_FORBIDDEN;
1615                 }
1616             } else {
1617                 if (pmap[i][j].flags & HAS_MONSTER) {
1618                     monst = monsterAtLoc(i, j);
1619                     if ((monst->creatureState == MONSTER_SLEEPING
1620                          || monst->turnsSpentStationary > 2
1621                          || (monst->info.flags & MONST_GETS_TURN_ON_ACTIVATION)
1622                          || monst->creatureState == MONSTER_ALLY)
1623                         && monst->creatureState != MONSTER_FLEEING) {
1624 
1625                         playerCostMap[i][j] = 1;
1626                         monsterCostMap[i][j] = PDS_FORBIDDEN;
1627                         continue;
1628                     }
1629                 }
1630 
1631                 if (cellHasTerrainFlag(i, j, (T_AUTO_DESCENT | T_IS_DF_TRAP))) {
1632                     monsterCostMap[i][j] = PDS_FORBIDDEN;
1633                     if (player.status[STATUS_LEVITATING]) {
1634                         playerCostMap[i][j] = 1;
1635                     } else {
1636                         playerCostMap[i][j] = PDS_FORBIDDEN;
1637                     }
1638                 } else if (cellHasTerrainFlag(i, j, T_IS_FIRE)) {
1639                     monsterCostMap[i][j] = PDS_FORBIDDEN;
1640                     if (player.status[STATUS_IMMUNE_TO_FIRE]) {
1641                         playerCostMap[i][j] = 1;
1642                     } else {
1643                         playerCostMap[i][j] = PDS_FORBIDDEN;
1644                     }
1645                 } else if (cellHasTerrainFlag(i, j, (T_IS_DEEP_WATER | T_SPONTANEOUSLY_IGNITES))) {
1646                     if (player.status[STATUS_LEVITATING]) {
1647                         playerCostMap[i][j] = 1;
1648                     } else {
1649                         playerCostMap[i][j] = 5;
1650                     }
1651                     monsterCostMap[i][j] = 5;
1652                 } else if (cellHasTerrainFlag(i, j, T_OBSTRUCTS_PASSABILITY)
1653                            && cellHasTMFlag(i, j, TM_IS_SECRET) && !(discoveredTerrainFlagsAtLoc(i, j) & T_OBSTRUCTS_PASSABILITY)
1654                            && !(pmap[i][j].flags & IN_FIELD_OF_VIEW)) {
1655                     // Secret door that the player can't currently see
1656                     playerCostMap[i][j] = 100;
1657                     monsterCostMap[i][j] = 1;
1658                 } else {
1659                     playerCostMap[i][j] = monsterCostMap[i][j] = 1;
1660                 }
1661             }
1662         }
1663     }
1664 
1665     safetyMap[player.xLoc][player.yLoc] = 0;
1666     playerCostMap[player.xLoc][player.yLoc] = 1;
1667     monsterCostMap[player.xLoc][player.yLoc] = PDS_FORBIDDEN;
1668 
1669     playerCostMap[rogue.upLoc[0]][rogue.upLoc[1]] = PDS_FORBIDDEN;
1670     monsterCostMap[rogue.upLoc[0]][rogue.upLoc[1]] = PDS_FORBIDDEN;
1671     playerCostMap[rogue.downLoc[0]][rogue.downLoc[1]] = PDS_FORBIDDEN;
1672     monsterCostMap[rogue.downLoc[0]][rogue.downLoc[1]] = PDS_FORBIDDEN;
1673 
1674     dijkstraScan(safetyMap, playerCostMap, false);
1675 
1676     for (i=0; i<DCOLS; i++) {
1677         for (j=0; j<DROWS; j++) {
1678             if (cellHasTerrainFlag(i, j, T_OBSTRUCTS_PASSABILITY)
1679                 && cellHasTMFlag(i, j, TM_IS_SECRET) && !(discoveredTerrainFlagsAtLoc(i, j) & T_OBSTRUCTS_PASSABILITY)
1680                 && !(pmap[i][j].flags & IN_FIELD_OF_VIEW)) {
1681 
1682                 // Secret doors that the player can't see are not particularly safe themselves;
1683                 // the areas behind them are.
1684                 resetDistanceCellInGrid(safetyMap, i, j);
1685             }
1686         }
1687     }
1688 
1689     for (i=0; i<DCOLS; i++) {
1690         for (j=0; j<DROWS; j++) {
1691             if (monsterCostMap[i][j] < 0) {
1692                 continue;
1693             }
1694 
1695             if (safetyMap[i][j] == 30000) {
1696                 safetyMap[i][j] = 150;
1697             }
1698 
1699             safetyMap[i][j] = 50 * safetyMap[i][j] / (50 + safetyMap[i][j]);
1700 
1701             safetyMap[i][j] *= -3;
1702 
1703             if (pmap[i][j].flags & IN_LOOP) {
1704                 safetyMap[i][j] -= 10;
1705             }
1706         }
1707     }
1708     dijkstraScan(safetyMap, monsterCostMap, false);
1709     for (i=0; i<DCOLS; i++) {
1710         for (j=0; j<DROWS; j++) {
1711             if (monsterCostMap[i][j] < 0) {
1712                 safetyMap[i][j] = 30000;
1713             }
1714         }
1715     }
1716     freeGrid(playerCostMap);
1717     freeGrid(monsterCostMap);
1718 }
1719 
updateSafeTerrainMap()1720 void updateSafeTerrainMap() {
1721     short i, j;
1722     short **costMap;
1723     creature *monst;
1724 
1725     rogue.updatedMapToSafeTerrainThisTurn = true;
1726     costMap = allocGrid();
1727 
1728     for (i=0; i<DCOLS; i++) {
1729         for (j=0; j<DROWS; j++) {
1730             monst = monsterAtLoc(i, j);
1731             if (cellHasTerrainFlag(i, j, T_OBSTRUCTS_PASSABILITY)
1732                 && (!cellHasTMFlag(i, j, TM_IS_SECRET) || (discoveredTerrainFlagsAtLoc(i, j) & T_OBSTRUCTS_PASSABILITY))) {
1733 
1734                 costMap[i][j] = cellHasTerrainFlag(i, j, T_OBSTRUCTS_DIAGONAL_MOVEMENT) ? PDS_OBSTRUCTION : PDS_FORBIDDEN;
1735                 rogue.mapToSafeTerrain[i][j] = 30000; // OOS prophylactic
1736             } else if ((monst && (monst->turnsSpentStationary > 1 || (monst->info.flags & MONST_GETS_TURN_ON_ACTIVATION)))
1737                        || (cellHasTerrainFlag(i, j, T_PATHING_BLOCKER & ~T_HARMFUL_TERRAIN) && !cellHasTMFlag(i, j, TM_IS_SECRET))) {
1738 
1739                 costMap[i][j] = PDS_FORBIDDEN;
1740                 rogue.mapToSafeTerrain[i][j] = 30000;
1741             } else if (cellHasTerrainFlag(i, j, T_HARMFUL_TERRAIN) || pmap[i][j].layers[DUNGEON] == DOOR) {
1742                 // The door thing is an aesthetically offensive but necessary hack to make sure
1743                 // that monsters trying to find their way out of caustic gas do not sprint for
1744                 // the doors. Doors are superficially free of gas, but as soon as they are opened,
1745                 // gas will fill their tile, so they are not actually safe. Without this fix,
1746                 // allies will fidget back and forth in a doorway while they asphyxiate.
1747                 // This will have to do. It's a difficult problem to solve elegantly.
1748                 costMap[i][j] = 1;
1749                 rogue.mapToSafeTerrain[i][j] = 30000;
1750             } else {
1751                 costMap[i][j] = 1;
1752                 rogue.mapToSafeTerrain[i][j] = 0;
1753             }
1754         }
1755     }
1756     dijkstraScan(rogue.mapToSafeTerrain, costMap, false);
1757     freeGrid(costMap);
1758 }
1759 
processIncrementalAutoID()1760 void processIncrementalAutoID() {
1761     item *theItem, *autoIdentifyItems[3] = {rogue.armor, rogue.ringLeft, rogue.ringRight};
1762     char buf[DCOLS*3], theItemName[DCOLS*3];
1763     short i;
1764 
1765     for (i=0; i<3; i++) {
1766         theItem = autoIdentifyItems[i];
1767         if (theItem
1768             && theItem->charges > 0
1769             && (!(theItem->flags & ITEM_IDENTIFIED) || ((theItem->category & RING) && !ringTable[theItem->kind].identified))) {
1770 
1771             theItem->charges--;
1772             if (theItem->charges <= 0) {
1773                 itemName(theItem, theItemName, false, false, NULL);
1774                 sprintf(buf, "you are now familiar enough with your %s to identify it.", theItemName);
1775                 messageWithColor(buf, &itemMessageColor, 0);
1776 
1777                 if (theItem->category & ARMOR) {
1778                     // Don't necessarily reveal the armor's runic specifically, just that it has one.
1779                     theItem->flags |= ITEM_IDENTIFIED;
1780                 } else if (theItem->category & RING) {
1781                     identify(theItem);
1782                 }
1783                 updateIdentifiableItems();
1784 
1785                 itemName(theItem, theItemName, true, true, NULL);
1786                 sprintf(buf, "%s %s.", (theItem->quantity > 1 ? "they are" : "it is"), theItemName);
1787                 messageWithColor(buf, &itemMessageColor, 0);
1788             }
1789         }
1790     }
1791 }
1792 
staffChargeDuration(const item * theItem)1793 short staffChargeDuration(const item *theItem) {
1794     // staffs of blinking and obstruction recharge half as fast so they're less powerful
1795     return (theItem->kind == STAFF_BLINKING || theItem->kind == STAFF_OBSTRUCTION ? 10000 : 5000) / theItem->enchant1;
1796 }
1797 
1798 // Multiplier can be negative, in which case staffs and charms will be drained instead of recharged.
rechargeItemsIncrementally(short multiplier)1799 void rechargeItemsIncrementally(short multiplier) {
1800     item *theItem;
1801     char buf[DCOLS*3], theItemName[DCOLS*3];
1802     short rechargeIncrement, staffRechargeDuration;
1803 
1804     if (rogue.wisdomBonus) {
1805         // at level 27, you recharge anything to full in one turn
1806         rechargeIncrement = 10 * ringWisdomMultiplier(rogue.wisdomBonus * FP_FACTOR) / FP_FACTOR;
1807     } else {
1808         rechargeIncrement = 10;
1809     }
1810 
1811     rechargeIncrement *= multiplier;
1812 
1813     for (theItem = packItems->nextItem; theItem != NULL; theItem = theItem->nextItem) {
1814         if (theItem->category & STAFF) {
1815             if (theItem->charges < theItem->enchant1 && rechargeIncrement > 0
1816                 || theItem->charges > 0 && rechargeIncrement < 0) {
1817 
1818                 theItem->enchant2 -= rechargeIncrement;
1819             }
1820             staffRechargeDuration = staffChargeDuration(theItem);
1821             while (theItem->enchant2 <= 0) {
1822                 // if it's time to add a staff charge
1823                 if (theItem->charges < theItem->enchant1) {
1824                     theItem->charges++;
1825                 }
1826                 theItem->enchant2 += randClumpedRange(max(staffRechargeDuration / 3, 1), staffRechargeDuration * 5 / 3, 3);
1827             }
1828             while (theItem->enchant2 > staffRechargeDuration * 5 / 3) {
1829                 // if it's time to drain a staff charge
1830                 if (theItem->charges > 0) {
1831                     theItem->charges--;
1832                 }
1833                 theItem->enchant2 -= staffRechargeDuration;
1834             }
1835         } else if ((theItem->category & CHARM) && (theItem->charges > 0)) {
1836             theItem->charges = clamp(theItem->charges - multiplier, 0, charmRechargeDelay(theItem->kind, theItem->enchant1));
1837             if (theItem->charges == 0) {
1838                 itemName(theItem, theItemName, false, false, NULL);
1839                 sprintf(buf, "your %s has recharged.", theItemName);
1840                 message(buf, 0);
1841             }
1842         }
1843     }
1844 }
1845 
extinguishFireOnCreature(creature * monst)1846 void extinguishFireOnCreature(creature *monst) {
1847 
1848     monst->status[STATUS_BURNING] = 0;
1849     if (monst == &player) {
1850         player.info.foreColor = &white;
1851         rogue.minersLight.lightColor = &minersLightColor;
1852         refreshDungeonCell(player.xLoc, player.yLoc);
1853         updateVision(true);
1854         message("you are no longer on fire.", 0);
1855     }
1856 }
1857 
1858 // n is the monster's depthLevel - 1.
monsterEntersLevel(creature * monst,short n)1859 void monsterEntersLevel(creature *monst, short n) {
1860     char monstName[COLS], buf[COLS];
1861     boolean pit = false;
1862 
1863     levels[n].mapStorage[monst->xLoc][monst->yLoc].flags &= ~HAS_MONSTER;
1864 
1865     // place traversing monster near the stairs on this level
1866     if (monst->bookkeepingFlags & MB_APPROACHING_DOWNSTAIRS) {
1867         monst->xLoc = rogue.upLoc[0];
1868         monst->yLoc = rogue.upLoc[1];
1869     } else if (monst->bookkeepingFlags & MB_APPROACHING_UPSTAIRS) {
1870         monst->xLoc = rogue.downLoc[0];
1871         monst->yLoc = rogue.downLoc[1];
1872     } else if (monst->bookkeepingFlags & MB_APPROACHING_PIT) { // jumping down pit
1873         pit = true;
1874         monst->xLoc = levels[n].playerExitedVia[0];
1875         monst->yLoc = levels[n].playerExitedVia[1];
1876     } else {
1877         brogueAssert(false);
1878     }
1879     monst->depth = rogue.depthLevel;
1880     monst->targetCorpseLoc[0] = monst->targetCorpseLoc[1] = 0;
1881 
1882     if (!pit) {
1883         getQualifyingPathLocNear(&(monst->xLoc), &(monst->yLoc), monst->xLoc, monst->yLoc, true,
1884                                  T_DIVIDES_LEVEL & avoidedFlagsForMonster(&(monst->info)), 0,
1885                                  avoidedFlagsForMonster(&(monst->info)), HAS_STAIRS, false);
1886     }
1887     if (!pit
1888         && (pmap[monst->xLoc][monst->yLoc].flags & (HAS_PLAYER | HAS_MONSTER))
1889         && !(terrainFlags(monst->xLoc, monst->yLoc) & avoidedFlagsForMonster(&(monst->info)))) {
1890         // Monsters using the stairs will displace any creatures already located there, to thwart stair-dancing.
1891         creature *prevMonst = monsterAtLoc(monst->xLoc, monst->yLoc);
1892         brogueAssert(prevMonst);
1893         getQualifyingPathLocNear(&(prevMonst->xLoc), &(prevMonst->yLoc), monst->xLoc, monst->yLoc, true,
1894                                  T_DIVIDES_LEVEL & avoidedFlagsForMonster(&(prevMonst->info)), 0,
1895                                  avoidedFlagsForMonster(&(prevMonst->info)), (HAS_MONSTER | HAS_PLAYER | HAS_STAIRS), false);
1896         pmap[monst->xLoc][monst->yLoc].flags &= ~(HAS_PLAYER | HAS_MONSTER);
1897         pmap[prevMonst->xLoc][prevMonst->yLoc].flags |= (prevMonst == &player ? HAS_PLAYER : HAS_MONSTER);
1898         refreshDungeonCell(prevMonst->xLoc, prevMonst->yLoc);
1899         //DEBUG printf("\nBumped a creature (%s) from (%i, %i) to (%i, %i).", prevMonst->info.monsterName, monst->xLoc, monst->yLoc, prevMonst->xLoc, prevMonst->yLoc);
1900     }
1901 
1902     // remove traversing monster from other level monster chain
1903     removeCreature(&levels[n].monsters, monst);
1904     // prepend traversing monster to current level monster chain
1905     prependCreature(monsters, monst);
1906 
1907     monst->status[STATUS_ENTERS_LEVEL_IN] = 0;
1908     monst->bookkeepingFlags |= MB_PREPLACED;
1909     monst->bookkeepingFlags &= ~MB_IS_FALLING;
1910     restoreMonster(monst, NULL, NULL);
1911     //DEBUG printf("\nPlaced a creature (%s) at (%i, %i).", monst->info.monsterName, monst->xLoc, monst->yLoc);
1912     monst->ticksUntilTurn = monst->movementSpeed;
1913     refreshDungeonCell(monst->xLoc, monst->yLoc);
1914 
1915     if (pit) {
1916         monsterName(monstName, monst, true);
1917         if (!monst->status[STATUS_LEVITATING]) {
1918             if (inflictDamage(NULL, monst, randClumpedRange(6, 12, 2), &red, false)) {
1919                 if (canSeeMonster(monst)) {
1920                     sprintf(buf, "%s plummets from above and splatters against the ground!", monstName);
1921                     messageWithColor(buf, messageColorFromVictim(monst), 0);
1922                 }
1923             } else {
1924                 if (canSeeMonster(monst)) {
1925                     sprintf(buf, "%s falls from above and crashes to the ground!", monstName);
1926                     message(buf, 0);
1927                 }
1928             }
1929         } else if (canSeeMonster(monst)) {
1930             sprintf(buf, "%s swoops into the cavern from above.", monstName);
1931             message(buf, 0);
1932         }
1933     }
1934 }
1935 
monstersApproachStairs()1936 void monstersApproachStairs() {
1937     short n;
1938 
1939     for (n = rogue.depthLevel - 2; n <= rogue.depthLevel; n += 2) { // cycle through previous and next level
1940         if (n >= 0 && n < DEEPEST_LEVEL && levels[n].visited) {
1941             for (creatureIterator it = iterateCreatures(&levels[n].monsters); hasNextCreature(it);) {
1942                 creature *monst = nextCreature(&it);
1943                 if (monst->status[STATUS_ENTERS_LEVEL_IN] > 1) {
1944                     monst->status[STATUS_ENTERS_LEVEL_IN]--;
1945                 } else if (monst->status[STATUS_ENTERS_LEVEL_IN] == 1) {
1946                     monsterEntersLevel(monst, n);
1947                 }
1948             }
1949         }
1950     }
1951 
1952     if (rogue.yendorWarden
1953         && abs(rogue.depthLevel - rogue.yendorWarden->depth) > 1) {
1954 
1955         updateYendorWardenTracking();
1956     }
1957 }
1958 
decrementPlayerStatus()1959 void decrementPlayerStatus() {
1960     // Handle hunger.
1961     if (!player.status[STATUS_PARALYZED]) {
1962         // No nutrition is expended while paralyzed.
1963         if (player.status[STATUS_NUTRITION] > 0) {
1964             if (!numberOfMatchingPackItems(AMULET, 0, 0, false) || rand_percent(20)) {
1965                 player.status[STATUS_NUTRITION]--;
1966             }
1967         }
1968         checkNutrition();
1969     }
1970 
1971     if (player.status[STATUS_TELEPATHIC] > 0 && !--player.status[STATUS_TELEPATHIC]) {
1972         updateVision(true);
1973         message("your preternatural mental sensitivity fades.", 0);
1974     }
1975 
1976     if (player.status[STATUS_DARKNESS] > 0) {
1977         player.status[STATUS_DARKNESS]--;
1978         updateMinersLightRadius();
1979         //updateVision();
1980     }
1981 
1982     if (player.status[STATUS_HALLUCINATING] > 0 && !--player.status[STATUS_HALLUCINATING]) {
1983         displayLevel();
1984         message("your hallucinations fade.", 0);
1985     }
1986 
1987     if (player.status[STATUS_LEVITATING] > 0 && !--player.status[STATUS_LEVITATING]) {
1988         message("you are no longer levitating.", 0);
1989     }
1990 
1991     if (player.status[STATUS_CONFUSED] > 0 && !--player.status[STATUS_CONFUSED]) {
1992         message("you no longer feel confused.", 0);
1993     }
1994 
1995     if (player.status[STATUS_NAUSEOUS] > 0 && !--player.status[STATUS_NAUSEOUS]) {
1996         message("you feel less nauseous.", 0);
1997     }
1998 
1999     if (player.status[STATUS_PARALYZED] > 0 && !--player.status[STATUS_PARALYZED]) {
2000         message("you can move again.", 0);
2001     }
2002 
2003     if (player.status[STATUS_HASTED] > 0 && !--player.status[STATUS_HASTED]) {
2004         player.movementSpeed = player.info.movementSpeed;
2005         player.attackSpeed = player.info.attackSpeed;
2006         synchronizePlayerTimeState();
2007         message("your supernatural speed fades.", 0);
2008     }
2009 
2010     if (player.status[STATUS_SLOWED] > 0 && !--player.status[STATUS_SLOWED]) {
2011         player.movementSpeed = player.info.movementSpeed;
2012         player.attackSpeed = player.info.attackSpeed;
2013         synchronizePlayerTimeState();
2014         message("your normal speed resumes.", 0);
2015     }
2016 
2017     if (player.status[STATUS_WEAKENED] > 0 && !--player.status[STATUS_WEAKENED]) {
2018         player.weaknessAmount = 0;
2019         message("strength returns to your muscles as the weakening toxin wears off.", 0);
2020         updateEncumbrance();
2021     }
2022 
2023     if (player.status[STATUS_DONNING]) {
2024         player.status[STATUS_DONNING]--;
2025         recalculateEquipmentBonuses();
2026     }
2027 
2028     if (player.status[STATUS_IMMUNE_TO_FIRE] > 0 && !--player.status[STATUS_IMMUNE_TO_FIRE]) {
2029         message("you no longer feel immune to fire.", 0);
2030     }
2031 
2032     if (player.status[STATUS_STUCK] && !cellHasTerrainFlag(player.xLoc, player.yLoc, T_ENTANGLES)) {
2033         player.status[STATUS_STUCK] = 0;
2034     }
2035 
2036     if (player.status[STATUS_EXPLOSION_IMMUNITY]) {
2037         player.status[STATUS_EXPLOSION_IMMUNITY]--;
2038     }
2039 
2040     if (player.status[STATUS_DISCORDANT]) {
2041         player.status[STATUS_DISCORDANT]--;
2042     }
2043 
2044     if (player.status[STATUS_AGGRAVATING]) {
2045         player.status[STATUS_AGGRAVATING]--;
2046     }
2047 
2048     if (player.status[STATUS_SHIELDED]) {
2049         player.status[STATUS_SHIELDED] -= player.maxStatus[STATUS_SHIELDED] / 20;
2050         if (player.status[STATUS_SHIELDED] <= 0) {
2051             player.status[STATUS_SHIELDED] = player.maxStatus[STATUS_SHIELDED] = 0;
2052         }
2053     }
2054 
2055     if (player.status[STATUS_INVISIBLE] > 0 && !--player.status[STATUS_INVISIBLE]) {
2056         message("you are no longer invisible.", 0);
2057     }
2058 
2059     if (rogue.monsterSpawnFuse <= 0) {
2060         spawnPeriodicHorde();
2061         rogue.monsterSpawnFuse = rand_range(125, 175);
2062     }
2063 }
2064 
dangerChanged(boolean danger[4])2065 boolean dangerChanged(boolean danger[4]) {
2066     enum directions dir;
2067     short newX, newY;
2068     for (dir = 0; dir < 4; dir++) {
2069         newX = player.xLoc + nbDirs[dir][0];
2070         newY = player.yLoc + nbDirs[dir][1];
2071         if (danger[dir] != monsterAvoids(&player, newX, newY)) {
2072             return true;
2073         }
2074     }
2075     return false;
2076 }
2077 
autoRest()2078 void autoRest() {
2079     short i = 0;
2080     boolean initiallyEmbedded; // Stop as soon as we're free from crystal.
2081     boolean danger[4];
2082     short newX, newY;
2083     enum directions dir;
2084 
2085     for (dir = 0; dir < 4; dir++) {
2086         newX = player.xLoc + nbDirs[dir][0];
2087         newY = player.yLoc + nbDirs[dir][1];
2088         danger[dir] = monsterAvoids(&player, newX, newY);
2089     }
2090 
2091     rogue.disturbed = false;
2092     rogue.automationActive = true;
2093     initiallyEmbedded = cellHasTerrainFlag(player.xLoc, player.yLoc, T_OBSTRUCTS_PASSABILITY);
2094 
2095     if ((player.currentHP < player.info.maxHP
2096          || player.status[STATUS_HALLUCINATING]
2097          || player.status[STATUS_CONFUSED]
2098          || player.status[STATUS_NAUSEOUS]
2099          || player.status[STATUS_POISONED]
2100          || player.status[STATUS_DARKNESS]
2101          || initiallyEmbedded)
2102         && !rogue.disturbed) {
2103         while (i++ < TURNS_FOR_FULL_REGEN
2104                && (player.currentHP < player.info.maxHP
2105                    || player.status[STATUS_HALLUCINATING]
2106                    || player.status[STATUS_CONFUSED]
2107                    || player.status[STATUS_NAUSEOUS]
2108                    || player.status[STATUS_POISONED]
2109                    || player.status[STATUS_DARKNESS]
2110                    || cellHasTerrainFlag(player.xLoc, player.yLoc, T_OBSTRUCTS_PASSABILITY))
2111                && !rogue.disturbed
2112                && (!initiallyEmbedded || cellHasTerrainFlag(player.xLoc, player.yLoc, T_OBSTRUCTS_PASSABILITY))) {
2113 
2114             recordKeystroke(REST_KEY, false, false);
2115             rogue.justRested = true;
2116             playerTurnEnded();
2117             if (dangerChanged(danger) || pauseBrogue(1)) {
2118                 rogue.disturbed = true;
2119             }
2120         }
2121     } else {
2122         for (i=0; i<100 && !rogue.disturbed; i++) {
2123             recordKeystroke(REST_KEY, false, false);
2124             rogue.justRested = true;
2125             playerTurnEnded();
2126             if (dangerChanged(danger) || pauseBrogue(1)) {
2127                 rogue.disturbed = true;
2128             }
2129         }
2130     }
2131     rogue.automationActive = false;
2132 }
2133 
manualSearch()2134 void manualSearch() {
2135     recordKeystroke(SEARCH_KEY, false, false);
2136 
2137     if (player.status[STATUS_SEARCHING] <= 0) {
2138         player.status[STATUS_SEARCHING] = 0;
2139         player.maxStatus[STATUS_SEARCHING] = 5;
2140     }
2141 
2142     player.status[STATUS_SEARCHING] += 1;
2143 
2144     /* The search strength values were chosen based on equating the expected
2145     number of cells discovered by 5x 80 searches (1.7.4) and 1x 200 search
2146     (1.7.5). 1x200 discovers an average of 932 cells; 5.65 times more cells than
2147     the 165 of 5x80. This factor is intepreted as the advantage of undelayed
2148     searching. Hence, we chose a short radius r and a long radius s such that
2149 
2150         4 * 5.65 * E_r + E_s ~= 932
2151 
2152     where E_x is the expected no. of cells discovered with radius x. We choose
2153     r=60, s=160, giving 852 < 932 (under to account for removal of 1.7.5 stealth
2154     range doubling).
2155     */
2156     short searchStrength = 0;
2157     if (player.status[STATUS_SEARCHING] < 5) {
2158         searchStrength = (rogue.awarenessBonus >= 0 ? 60 : 30);
2159     } else {
2160         // Do a final, larger-radius search on the fifth search in a row
2161         searchStrength = 160;
2162         message("you finish your detailed search of the area.", 0);
2163         player.status[STATUS_SEARCHING] = 0;
2164     }
2165 
2166     // ensure our search is no weaker than the current passive search
2167     search(max(searchStrength, rogue.awarenessBonus + 30));
2168 
2169     rogue.justSearched = true;
2170     playerTurnEnded();
2171 }
2172 
2173 // Call this periodically (when haste/slow wears off and when moving between depths)
2174 // to keep environmental updates in sync with player turns.
synchronizePlayerTimeState()2175 void synchronizePlayerTimeState() {
2176     rogue.ticksTillUpdateEnvironment = player.ticksUntilTurn;
2177 }
2178 
playerRecoversFromAttacking(boolean anAttackHit)2179 void playerRecoversFromAttacking(boolean anAttackHit) {
2180     if (player.ticksUntilTurn >= 0) {
2181         // Don't do this if the player's weapon of speed just fired.
2182         if (rogue.weapon && (rogue.weapon->flags & ITEM_ATTACKS_STAGGER) && anAttackHit) {
2183             player.ticksUntilTurn += 2 * player.attackSpeed;
2184         } else if (rogue.weapon && (rogue.weapon->flags & ITEM_ATTACKS_QUICKLY)) {
2185             player.ticksUntilTurn += player.attackSpeed / 2;
2186         } else {
2187             player.ticksUntilTurn += player.attackSpeed;
2188         }
2189     }
2190 }
2191 
2192 
recordCurrentCreatureHealths()2193 static void recordCurrentCreatureHealths() {
2194 
2195     boolean handledPlayer = false;
2196     for (creatureIterator it = iterateCreatures(monsters); !handledPlayer || hasNextCreature(it);) {
2197         creature *monst = !handledPlayer ? &player : nextCreature(&it);
2198         handledPlayer = true;
2199         monst->previousHealthPoints = monst->currentHP;
2200     }
2201 }
2202 
2203 // This is the dungeon schedule manager, called every time the player's turn comes to an end.
2204 // It hands control over to monsters until they've all expended their accumulated ticks,
2205 // updating the environment (gas spreading, flames spreading and burning out, etc.) every
2206 // 100 ticks.
playerTurnEnded()2207 void playerTurnEnded() {
2208     short soonestTurn, damage, turnsRequiredToShore, turnsToShore;
2209     char buf[COLS], buf2[COLS];
2210     boolean fastForward = false;
2211     short oldRNG;
2212 
2213     brogueAssert(rogue.RNG == RNG_SUBSTANTIVE);
2214 
2215     handleXPXP();
2216     resetDFMessageEligibility();
2217     recordCurrentCreatureHealths();
2218 
2219     if (player.bookkeepingFlags & MB_IS_FALLING) {
2220         playerFalls();
2221         if (!rogue.gameHasEnded) {
2222             handleHealthAlerts();
2223         }
2224         return;
2225     }
2226 
2227     // This happens in updateEnvironment, but some monsters move faster than the
2228     // environment updates in the loop below. This means they need to fall at
2229     // the start of the turn to avoid them being able to act while suspended
2230     // over a chasm
2231     monstersFall();
2232 
2233     do {
2234         if (rogue.gameHasEnded) {
2235             return;
2236         }
2237 
2238         if (!player.status[STATUS_PARALYZED]) {
2239             rogue.playerTurnNumber++; // So recordings don't register more turns than you actually have.
2240         }
2241         rogue.absoluteTurnNumber++;
2242 
2243         if (player.status[STATUS_INVISIBLE]) {
2244             rogue.scentTurnNumber += 10; // Your scent fades very quickly while you are invisible.
2245         } else {
2246             rogue.scentTurnNumber += 3; // this must happen per subjective player time
2247         }
2248         if (rogue.scentTurnNumber > 20000) {
2249             resetScentTurnNumber();
2250         }
2251 
2252         //updateFlavorText();
2253 
2254         // Regeneration/starvation:
2255         if (player.status[STATUS_NUTRITION] <= 0) {
2256             player.currentHP--;
2257             if (player.currentHP <= 0) {
2258                 gameOver("Starved to death", true);
2259                 return;
2260             }
2261         } else if (player.currentHP < player.info.maxHP
2262                    && !player.status[STATUS_POISONED]) {
2263             if ((player.turnsUntilRegen -= 1000) <= 0) {
2264                 player.currentHP++;
2265                 if (player.previousHealthPoints < player.currentHP) {
2266                     player.previousHealthPoints++; // Regeneration doesn't display on the status bar.
2267                 }
2268                 player.turnsUntilRegen += player.info.turnsBetweenRegen;
2269             }
2270             if (player.regenPerTurn) {
2271                 player.currentHP += player.regenPerTurn;
2272                 if (player.previousHealthPoints < player.currentHP) {
2273                     player.previousHealthPoints = min(player.currentHP, player.previousHealthPoints + player.regenPerTurn);
2274                 }
2275             }
2276         }
2277 
2278         if (rogue.awarenessBonus > -30 && !(pmap[player.xLoc][player.yLoc].flags & SEARCHED_FROM_HERE)) {
2279             // Low-grade auto-search wherever you step, but only once per tile.
2280             search(rogue.awarenessBonus + 30);
2281             pmap[player.xLoc][player.yLoc].flags |= SEARCHED_FROM_HERE;
2282         }
2283         if (!rogue.justSearched && player.status[STATUS_SEARCHING] > 0) {
2284             // Searching only "charges up" when done on consecutive turns
2285             player.status[STATUS_SEARCHING] = 0;
2286         }
2287         if (rogue.staleLoopMap) {
2288             analyzeMap(false); // Don't need to update the chokemap.
2289         }
2290 
2291         for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
2292             creature *monst = nextCreature(&it);
2293             if ((monst->bookkeepingFlags & MB_BOUND_TO_LEADER)
2294                 && (!monst->leader || !(monst->bookkeepingFlags & MB_FOLLOWER))
2295                 && (monst->creatureState != MONSTER_ALLY)) {
2296 
2297                 killCreature(monst, false);
2298                 if (canSeeMonster(monst)) {
2299                     monsterName(buf2, monst, true);
2300                     sprintf(buf, "%s dissipates into thin air", buf2);
2301                     combatMessage(buf, messageColorFromVictim(monst));
2302                 }
2303             }
2304         }
2305 
2306         if (player.status[STATUS_BURNING] > 0) {
2307             damage = rand_range(1, 3);
2308             if (!(player.status[STATUS_IMMUNE_TO_FIRE]) && inflictDamage(NULL, &player, damage, &orange, true)) {
2309                 gameOver("Burned to death", true);
2310             }
2311             if (!--player.status[STATUS_BURNING]) {
2312                 extinguishFireOnCreature(&player);
2313             }
2314         }
2315 
2316         if (player.status[STATUS_POISONED] > 0) {
2317             player.status[STATUS_POISONED]--;
2318             if (inflictDamage(NULL, &player, player.poisonAmount, &green, true)) {
2319                 gameOver("Died from poison", true);
2320             }
2321             if (!player.status[STATUS_POISONED]) {
2322                 player.poisonAmount = 0;
2323             }
2324         }
2325 
2326         if (player.ticksUntilTurn == 0) { // attacking adds ticks elsewhere
2327             player.ticksUntilTurn += player.movementSpeed;
2328         } else if (player.ticksUntilTurn < 0) { // if he gets a free turn
2329             player.ticksUntilTurn = 0;
2330         }
2331 
2332         updateScent();
2333 //      updateVision(true);
2334 //        rogue.aggroRange = currentAggroValue();
2335 //        if (rogue.displayAggroRangeMode) {
2336 //            displayLevel();
2337 //        }
2338         rogue.updatedSafetyMapThisTurn          = false;
2339         rogue.updatedAllySafetyMapThisTurn      = false;
2340         rogue.updatedMapToSafeTerrainThisTurn   = false;
2341 
2342         for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
2343             creature *monst = nextCreature(&it);
2344             if (D_SAFETY_VISION || monst->creatureState == MONSTER_FLEEING && pmap[monst->xLoc][monst->yLoc].flags & IN_FIELD_OF_VIEW) {
2345                 updateSafetyMap(); // only if there is a fleeing monster who can see the player
2346                 break;
2347             }
2348         }
2349 
2350         if (D_BULLET_TIME && !rogue.justRested) {
2351             player.ticksUntilTurn = 0;
2352         }
2353 
2354         applyGradualTileEffectsToCreature(&player, player.ticksUntilTurn);
2355 
2356         if (rogue.gameHasEnded) {
2357             return;
2358         }
2359 
2360         rogue.heardCombatThisTurn = false;
2361 
2362         while (player.ticksUntilTurn > 0) {
2363             soonestTurn = 10000;
2364             for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
2365                 creature *monst = nextCreature(&it);
2366                 soonestTurn = min(soonestTurn, monst->ticksUntilTurn);
2367             }
2368             soonestTurn = min(soonestTurn, player.ticksUntilTurn);
2369             soonestTurn = min(soonestTurn, rogue.ticksTillUpdateEnvironment);
2370             for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
2371                 creature *monst = nextCreature(&it);
2372                 monst->ticksUntilTurn -= soonestTurn;
2373             }
2374             rogue.ticksTillUpdateEnvironment -= soonestTurn;
2375             if (rogue.ticksTillUpdateEnvironment <= 0) {
2376                 rogue.ticksTillUpdateEnvironment += 100;
2377 
2378                 // stuff that happens periodically according to an objective time measurement goes here:
2379                 rechargeItemsIncrementally(1); // staffs recharge every so often
2380                 processIncrementalAutoID();   // become more familiar with worn armor and rings
2381                 rogue.monsterSpawnFuse--; // monsters spawn in the level every so often
2382 
2383                 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
2384                     creature *monst = nextCreature(&it);
2385                     applyInstantTileEffectsToCreature(monst);
2386                 }
2387 
2388                 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
2389                     creature *monst = nextCreature(&it);
2390                     decrementMonsterStatus(monst);
2391                 }
2392 
2393                 // monsters with a dungeon feature spawn it every so often
2394                 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
2395                     creature *monst = nextCreature(&it);
2396 
2397                     if (monst->info.DFChance
2398                         && !(monst->info.flags & MONST_GETS_TURN_ON_ACTIVATION)
2399                         && rand_percent(monst->info.DFChance)) {
2400 
2401                         spawnDungeonFeature(monst->xLoc, monst->yLoc, &dungeonFeatureCatalog[monst->info.DFType], true, false);
2402                     }
2403                 }
2404 
2405                 updateEnvironment(); // Update fire and gas, items floating around in water, monsters falling into chasms, etc.
2406                 decrementPlayerStatus();
2407                 applyInstantTileEffectsToCreature(&player);
2408                 if (rogue.gameHasEnded) { // caustic gas, lava, trapdoor, etc.
2409                     return;
2410                 }
2411                 monstersApproachStairs();
2412 
2413                 if (player.ticksUntilTurn > 100 && !fastForward) {
2414                     fastForward = rogue.playbackFastForward || pauseBrogue(25);
2415                 }
2416 
2417                 // Rolling waypoint refresh:
2418                 rogue.wpRefreshTicker++;
2419                 if (rogue.wpRefreshTicker >= rogue.wpCount) {
2420                     rogue.wpRefreshTicker = 0;
2421                 }
2422                 refreshWaypoint(rogue.wpRefreshTicker);
2423             }
2424 
2425             for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it) && rogue.gameHasEnded == false;) {
2426                 creature *monst = nextCreature(&it);
2427                 if (monst->ticksUntilTurn <= 0) {
2428                     if (monst->currentHP > monst->info.maxHP) {
2429                         monst->currentHP = monst->info.maxHP;
2430                     }
2431 
2432                     if ((monst->info.flags & MONST_GETS_TURN_ON_ACTIVATION)
2433                         || monst->status[STATUS_PARALYZED]
2434                         || monst->status[STATUS_ENTRANCED]
2435                         || (monst->bookkeepingFlags & MB_CAPTIVE)) {
2436 
2437                         // Do not pass go; do not collect 200 gold.
2438                         monst->ticksUntilTurn = monst->movementSpeed;
2439                     } else {
2440                         monstersTurn(monst);
2441                     }
2442 
2443                     for (creatureIterator it2 = iterateCreatures(monsters); hasNextCreature(it2);) {
2444                         creature *monst2 = nextCreature(&it2);
2445                         if (monst2 == monst) { // monst still alive and on the level
2446                             applyGradualTileEffectsToCreature(monst, monst->ticksUntilTurn);
2447                             break;
2448                         }
2449                     }
2450                     restartIterator(&it); // loop through from the beginning to be safe
2451                 }
2452             }
2453 
2454             player.ticksUntilTurn -= soonestTurn;
2455 
2456             if (rogue.gameHasEnded) {
2457                 return;
2458             }
2459         }
2460         // DEBUG displayLevel();
2461         //checkForDungeonErrors();
2462 
2463         updateVision(true);
2464         rogue.aggroRange = currentAggroValue();
2465         if (rogue.displayAggroRangeMode) {
2466             displayLevel();
2467         }
2468 
2469         for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
2470             creature *monst = nextCreature(&it);
2471             if (canSeeMonster(monst) && !(monst->bookkeepingFlags & (MB_WAS_VISIBLE | MB_ALREADY_SEEN))) {
2472                 if (monst->creatureState != MONSTER_ALLY) {
2473                     rogue.disturbed = true;
2474                     if (rogue.cautiousMode || rogue.automationActive) {
2475                         oldRNG = rogue.RNG;
2476                         rogue.RNG = RNG_COSMETIC;
2477                         //assureCosmeticRNG;
2478                         monsterName(buf2, monst, false);
2479                         sprintf(buf, "you %s a%s %s",
2480                                 playerCanDirectlySee(monst->xLoc, monst->yLoc) ? "see" : "sense",
2481                                 (isVowelish(buf2) ? "n" : ""),
2482                                 buf2);
2483                         if (rogue.cautiousMode) {
2484                             strcat(buf, ".");
2485                             message(buf, REQUIRE_ACKNOWLEDGMENT);
2486                         } else {
2487                             combatMessage(buf, 0);
2488                         }
2489                         restoreRNG;
2490                     }
2491                 }
2492             }
2493 
2494             if (canSeeMonster(monst)) {
2495                 monst->bookkeepingFlags |= MB_WAS_VISIBLE;
2496                 if (cellHasTerrainFlag(monst->xLoc, monst->yLoc, T_OBSTRUCTS_PASSABILITY)
2497                     && cellHasTMFlag(monst->xLoc, monst->yLoc, TM_IS_SECRET)) {
2498 
2499                     discover(monst->xLoc, monst->yLoc);
2500                 }
2501                 if (canDirectlySeeMonster(monst)) {
2502                     if (rogue.weapon && rogue.weapon->flags & ITEM_RUNIC
2503                         && rogue.weapon->enchant2 == W_SLAYING
2504                         && !(rogue.weapon->flags & ITEM_RUNIC_HINTED)
2505                         && monsterIsInClass(monst, rogue.weapon->vorpalEnemy)) {
2506 
2507                         rogue.weapon->flags |= ITEM_RUNIC_HINTED;
2508                         itemName(rogue.weapon, buf2, false, false, NULL);
2509                         sprintf(buf, "the runes on your %s gleam balefully.", buf2);
2510                         messageWithColor(buf, &itemMessageColor, REQUIRE_ACKNOWLEDGMENT);
2511                     }
2512                     if (rogue.armor && rogue.armor->flags & ITEM_RUNIC
2513                         && rogue.armor->enchant2 == A_IMMUNITY
2514                         && !(rogue.armor->flags & ITEM_RUNIC_HINTED)
2515                         && monsterIsInClass(monst, rogue.armor->vorpalEnemy)) {
2516 
2517                         rogue.armor->flags |= ITEM_RUNIC_HINTED;
2518                         itemName(rogue.armor, buf2, false, false, NULL);
2519                         sprintf(buf, "the runes on your %s glow protectively.", buf2);
2520                         messageWithColor(buf, &itemMessageColor, REQUIRE_ACKNOWLEDGMENT);
2521                     }
2522                 }
2523             }
2524 
2525             if (!canSeeMonster(monst)
2526                 && (monst->bookkeepingFlags & MB_WAS_VISIBLE)
2527                 && !(monst->bookkeepingFlags & MB_CAPTIVE)) {
2528                 // For captives we never unset MB_WAS_VISIBLE because captives are not moving,
2529                 // so we don't want to get "You see a ..." every time they come back into view.
2530 
2531                 monst->bookkeepingFlags &= ~MB_WAS_VISIBLE;
2532             }
2533         }
2534 
2535         displayCombatText();
2536 
2537         if (player.status[STATUS_PARALYZED]) {
2538             if (!fastForward) {
2539                 fastForward = rogue.playbackFastForward || pauseBrogue(25);
2540             }
2541         }
2542 
2543         //checkNutrition(); // Now handled within decrementPlayerStatus().
2544         if (!rogue.playbackFastForward) {
2545             shuffleTerrainColors(100, false);
2546         }
2547 
2548         displayAnnotation();
2549 
2550         refreshSideBar(-1, -1, false);
2551 
2552         applyInstantTileEffectsToCreature(&player);
2553         if (rogue.gameHasEnded) { // caustic gas, lava, trapdoor, etc.
2554             return;
2555         }
2556 
2557         if (player.currentHP > player.info.maxHP) {
2558             player.currentHP = player.info.maxHP;
2559         }
2560 
2561         if (player.bookkeepingFlags & MB_IS_FALLING) {
2562             playerFalls();
2563             handleHealthAlerts();
2564             return;
2565         }
2566 
2567     } while (player.status[STATUS_PARALYZED]);
2568 
2569     rogue.justRested = false;
2570     rogue.justSearched = false;
2571     updateFlavorText();
2572 
2573     if (!rogue.updatedMapToShoreThisTurn) {
2574         updateMapToShore();
2575     }
2576 
2577     // "point of no return" check
2578     if ((player.status[STATUS_LEVITATING] && cellHasTerrainFlag(player.xLoc, player.yLoc, T_LAVA_INSTA_DEATH | T_IS_DEEP_WATER | T_AUTO_DESCENT))
2579         || (player.status[STATUS_IMMUNE_TO_FIRE] && cellHasTerrainFlag(player.xLoc, player.yLoc, T_LAVA_INSTA_DEATH))) {
2580         if (!rogue.receivedLevitationWarning) {
2581             turnsRequiredToShore = rogue.mapToShore[player.xLoc][player.yLoc] * player.movementSpeed / 100;
2582             if (cellHasTerrainFlag(player.xLoc, player.yLoc, T_LAVA_INSTA_DEATH)) {
2583                 turnsToShore = max(player.status[STATUS_LEVITATING], player.status[STATUS_IMMUNE_TO_FIRE]) * 100 / player.movementSpeed;
2584             } else {
2585                 turnsToShore = player.status[STATUS_LEVITATING] * 100 / player.movementSpeed;
2586             }
2587             if (turnsRequiredToShore == turnsToShore || turnsRequiredToShore + 1 == turnsToShore) {
2588                 message("better head back to solid ground!", REQUIRE_ACKNOWLEDGMENT);
2589                 rogue.receivedLevitationWarning = true;
2590             } else if (turnsRequiredToShore > turnsToShore
2591                        && turnsRequiredToShore < 10000) {
2592                 message("you're past the point of no return!", REQUIRE_ACKNOWLEDGMENT);
2593                 rogue.receivedLevitationWarning = true;
2594             }
2595         }
2596     } else {
2597         rogue.receivedLevitationWarning = false;
2598     }
2599 
2600     emptyGraveyard();
2601     rogue.playbackBetweenTurns = true;
2602     RNGCheck();
2603     handleHealthAlerts();
2604 
2605     if (rogue.flareCount > 0) {
2606         animateFlares(rogue.flares, rogue.flareCount);
2607         rogue.flareCount = 0;
2608     }
2609 }
2610 
resetScentTurnNumber()2611 void resetScentTurnNumber() { // don't want player.scentTurnNumber to roll over the short maxint!
2612     short i, j, d;
2613     rogue.scentTurnNumber -= 15000;
2614     for (d = 0; d < DEEPEST_LEVEL; d++) {
2615         if (levels[d].visited) {
2616             for (i=0; i<DCOLS; i++) {
2617                 for (j=0; j<DROWS; j++) {
2618                     if (levels[d].scentMap[i][j] > 15000) {
2619                         levels[d].scentMap[i][j] -= 15000;
2620                     } else {
2621                         levels[d].scentMap[i][j] = 0;
2622                     }
2623                 }
2624             }
2625         }
2626     }
2627 }
2628