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