1 /*
2 * Items.c
3 * Brogue
4 *
5 * Created by Brian Walker on 1/17/09.
6 * Copyright 2012. All rights reserved.
7 *
8 * This file is part of Brogue.
9 *
10 * This program is free software: you can redistribute it and/or modify
11 * it under the terms of the GNU Affero General Public License as
12 * published by the Free Software Foundation, either version 3 of the
13 * License, or (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU Affero General Public License for more details.
19 *
20 * You should have received a copy of the GNU Affero General Public License
21 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 */
23
24
25 #include "Rogue.h"
26 #include "IncludeGlobals.h"
27
initializeItem()28 item *initializeItem() {
29 short i;
30 item *theItem;
31
32 theItem = (item *) malloc(sizeof(item));
33 memset(theItem, '\0', sizeof(item) );
34
35 theItem->category = 0;
36 theItem->kind = 0;
37 theItem->flags = 0;
38 theItem->displayChar = '&';
39 theItem->foreColor = &itemColor;
40 theItem->inventoryColor = &white;
41 theItem->inventoryLetter = '\0';
42 theItem->armor = 0;
43 theItem->strengthRequired = 0;
44 theItem->enchant1 = 0;
45 theItem->enchant2 = 0;
46 theItem->timesEnchanted = 0;
47 theItem->vorpalEnemy = 0;
48 theItem->charges = 0;
49 theItem->quantity = 1;
50 theItem->quiverNumber = 0;
51 theItem->originDepth = 0;
52 theItem->inscription[0] = '\0';
53 theItem->lastUsed[0] = 0;
54 theItem->lastUsed[1] = 0;
55 theItem->lastUsed[2] = 0;
56 theItem->nextItem = NULL;
57
58 for (i=0; i < KEY_ID_MAXIMUM; i++) {
59 theItem->keyLoc[i].x = 0;
60 theItem->keyLoc[i].y = 0;
61 theItem->keyLoc[i].machine = 0;
62 theItem->keyLoc[i].disposableHere = false;
63 }
64 return theItem;
65 }
66
67 // Allocates space, generates a specified item (or random category/kind if -1)
68 // and returns a pointer to that item. The item is not given a location here
69 // and is not inserted into the item chain!
generateItem(unsigned short theCategory,short theKind)70 item *generateItem(unsigned short theCategory, short theKind) {
71 item *theItem = initializeItem();
72 makeItemInto(theItem, theCategory, theKind);
73 return theItem;
74 }
75
pickItemCategory(unsigned long theCategory)76 unsigned long pickItemCategory(unsigned long theCategory) {
77 short i, sum, randIndex;
78 short probabilities[13] = {50, 42, 52, 3, 3, 10, 8, 2, 3, 2, 0, 0, 0};
79 unsigned short correspondingCategories[13] = {GOLD, SCROLL, POTION, STAFF, WAND, WEAPON, ARMOR, FOOD, RING, CHARM, AMULET, GEM, KEY};
80
81 sum = 0;
82
83 for (i=0; i<13; i++) {
84 if (theCategory <= 0 || theCategory & correspondingCategories[i]) {
85 sum += probabilities[i];
86 }
87 }
88
89 if (sum == 0) {
90 return theCategory; // e.g. when you pass in AMULET or GEM, since they have no frequency
91 }
92
93 randIndex = rand_range(1, sum);
94
95 for (i=0; ; i++) {
96 if (theCategory <= 0 || theCategory & correspondingCategories[i]) {
97 if (randIndex <= probabilities[i]) {
98 return correspondingCategories[i];
99 }
100 randIndex -= probabilities[i];
101 }
102 }
103 }
104
105 // Sets an item to the given type and category (or chooses randomly if -1) with all other stats
makeItemInto(item * theItem,unsigned long itemCategory,short itemKind)106 item *makeItemInto(item *theItem, unsigned long itemCategory, short itemKind) {
107 itemTable *theEntry = NULL;
108
109 if (itemCategory <= 0) {
110 itemCategory = ALL_ITEMS;
111 }
112
113 itemCategory = pickItemCategory(itemCategory);
114
115 theItem->category = itemCategory;
116
117 switch (itemCategory) {
118
119 case FOOD:
120 if (itemKind < 0) {
121 itemKind = chooseKind(foodTable, NUMBER_FOOD_KINDS);
122 }
123 theEntry = &foodTable[itemKind];
124 theItem->displayChar = G_FOOD;
125 theItem->flags |= ITEM_IDENTIFIED;
126 break;
127
128 case WEAPON:
129 if (itemKind < 0) {
130 itemKind = chooseKind(weaponTable, NUMBER_WEAPON_KINDS);
131 }
132 theEntry = &weaponTable[itemKind];
133 theItem->damage = weaponTable[itemKind].range;
134 theItem->strengthRequired = weaponTable[itemKind].strengthRequired;
135 theItem->displayChar = G_WEAPON;
136
137 switch (itemKind) {
138 case DAGGER:
139 theItem->flags |= ITEM_SNEAK_ATTACK_BONUS;
140 break;
141 case MACE:
142 case HAMMER:
143 theItem->flags |= ITEM_ATTACKS_STAGGER;
144 break;
145 case WHIP:
146 theItem->flags |= ITEM_ATTACKS_EXTEND;
147 break;
148 case RAPIER:
149 theItem->flags |= (ITEM_ATTACKS_QUICKLY | ITEM_LUNGE_ATTACKS);
150 break;
151 case FLAIL:
152 theItem->flags |= ITEM_PASS_ATTACKS;
153 break;
154 case SPEAR:
155 case PIKE:
156 theItem->flags |= ITEM_ATTACKS_PENETRATE;
157 break;
158 case AXE:
159 case WAR_AXE:
160 theItem->flags |= ITEM_ATTACKS_ALL_ADJACENT;
161 break;
162 default:
163 break;
164 }
165
166 if (rand_percent(40)) {
167 theItem->enchant1 += rand_range(1, 3);
168 if (rand_percent(50)) {
169 // cursed
170 theItem->enchant1 *= -1;
171 theItem->flags |= ITEM_CURSED;
172 if (rand_percent(33)) { // give it a bad runic
173 theItem->enchant2 = rand_range(NUMBER_GOOD_WEAPON_ENCHANT_KINDS, NUMBER_WEAPON_RUNIC_KINDS - 1);
174 theItem->flags |= ITEM_RUNIC;
175 }
176 } else if (rand_range(3, 10)
177 * ((theItem->flags & ITEM_ATTACKS_STAGGER) ? 2 : 1)
178 / ((theItem->flags & ITEM_ATTACKS_QUICKLY) ? 2 : 1)
179 / ((theItem->flags & ITEM_ATTACKS_EXTEND) ? 2 : 1)
180 > theItem->damage.lowerBound) {
181 // give it a good runic; lower damage items are more likely to be runic
182 theItem->enchant2 = rand_range(0, NUMBER_GOOD_WEAPON_ENCHANT_KINDS - 1);
183 theItem->flags |= ITEM_RUNIC;
184 if (theItem->enchant2 == W_SLAYING) {
185 theItem->vorpalEnemy = chooseVorpalEnemy();
186 }
187 } else {
188 while (rand_percent(10)) {
189 theItem->enchant1++;
190 }
191 }
192 }
193 if (itemKind == DART || itemKind == INCENDIARY_DART || itemKind == JAVELIN) {
194 if (itemKind == INCENDIARY_DART) {
195 theItem->quantity = rand_range(3, 6);
196 } else {
197 theItem->quantity = rand_range(5, 18);
198 }
199 theItem->quiverNumber = rand_range(1, 60000);
200 theItem->flags &= ~(ITEM_CURSED | ITEM_RUNIC); // throwing weapons can't be cursed or runic
201 theItem->enchant1 = 0; // throwing weapons can't be magical
202 }
203 theItem->charges = WEAPON_KILLS_TO_AUTO_ID; // kill 20 enemies to auto-identify
204 break;
205
206 case ARMOR:
207 if (itemKind < 0) {
208 itemKind = chooseKind(armorTable, NUMBER_ARMOR_KINDS);
209 }
210 theEntry = &armorTable[itemKind];
211 theItem->armor = randClump(armorTable[itemKind].range);
212 theItem->strengthRequired = armorTable[itemKind].strengthRequired;
213 theItem->displayChar = G_ARMOR;
214 theItem->charges = ARMOR_DELAY_TO_AUTO_ID; // this many turns until it reveals its enchants and whether runic
215 if (rand_percent(40)) {
216 theItem->enchant1 += rand_range(1, 3);
217 if (rand_percent(50)) {
218 // cursed
219 theItem->enchant1 *= -1;
220 theItem->flags |= ITEM_CURSED;
221 if (rand_percent(33)) { // give it a bad runic
222 theItem->enchant2 = rand_range(NUMBER_GOOD_ARMOR_ENCHANT_KINDS, NUMBER_ARMOR_ENCHANT_KINDS - 1);
223 theItem->flags |= ITEM_RUNIC;
224 }
225 } else if (rand_range(0, 95) > theItem->armor) { // give it a good runic
226 theItem->enchant2 = rand_range(0, NUMBER_GOOD_ARMOR_ENCHANT_KINDS - 1);
227 theItem->flags |= ITEM_RUNIC;
228 if (theItem->enchant2 == A_IMMUNITY) {
229 theItem->vorpalEnemy = chooseVorpalEnemy();
230 }
231 } else {
232 while (rand_percent(10)) {
233 theItem->enchant1++;
234 }
235 }
236 }
237 break;
238 case SCROLL:
239 if (itemKind < 0) {
240 itemKind = chooseKind(scrollTable, NUMBER_SCROLL_KINDS);
241 }
242 theEntry = &scrollTable[itemKind];
243 theItem->displayChar = G_SCROLL;
244 theItem->flags |= ITEM_FLAMMABLE;
245 break;
246 case POTION:
247 if (itemKind < 0) {
248 itemKind = chooseKind(potionTable, NUMBER_POTION_KINDS);
249 }
250 theEntry = &potionTable[itemKind];
251 theItem->displayChar = G_POTION;
252 break;
253 case STAFF:
254 if (itemKind < 0) {
255 itemKind = chooseKind(staffTable, NUMBER_STAFF_KINDS);
256 }
257 theEntry = &staffTable[itemKind];
258 theItem->displayChar = G_STAFF;
259 theItem->charges = 2;
260 if (rand_percent(50)) {
261 theItem->charges++;
262 if (rand_percent(15)) {
263 theItem->charges++;
264 while (rand_percent(10)) {
265 theItem->charges++;
266 }
267 }
268 }
269 theItem->enchant1 = theItem->charges;
270 theItem->enchant2 = (itemKind == STAFF_BLINKING || itemKind == STAFF_OBSTRUCTION ? 1000 : 500); // start with no recharging mojo
271 break;
272 case WAND:
273 if (itemKind < 0) {
274 itemKind = chooseKind(wandTable, NUMBER_WAND_KINDS);
275 }
276 theEntry = &wandTable[itemKind];
277 theItem->displayChar = G_WAND;
278 theItem->charges = randClump(wandTable[itemKind].range);
279 break;
280 case RING:
281 if (itemKind < 0) {
282 itemKind = chooseKind(ringTable, NUMBER_RING_KINDS);
283 }
284 theEntry = &ringTable[itemKind];
285 theItem->displayChar = G_RING;
286 theItem->enchant1 = randClump(ringTable[itemKind].range);
287 theItem->charges = RING_DELAY_TO_AUTO_ID; // how many turns of being worn until it auto-identifies
288 if (rand_percent(16)) {
289 // cursed
290 theItem->enchant1 *= -1;
291 theItem->flags |= ITEM_CURSED;
292 } else {
293 while (rand_percent(10)) {
294 theItem->enchant1++;
295 }
296 }
297 break;
298 case CHARM:
299 if (itemKind < 0) {
300 itemKind = chooseKind(charmTable, NUMBER_CHARM_KINDS);
301 }
302 theItem->displayChar = G_CHARM;
303 theItem->charges = 0; // Charms are initially ready for use.
304 theItem->enchant1 = randClump(charmTable[itemKind].range);
305 while (rand_percent(7)) {
306 theItem->enchant1++;
307 }
308 theItem->flags |= ITEM_IDENTIFIED;
309 break;
310 case GOLD:
311 theEntry = NULL;
312 theItem->displayChar = G_GOLD;
313 theItem->quantity = rand_range(50 + rogue.depthLevel * 10, 100 + rogue.depthLevel * 15);
314 break;
315 case AMULET:
316 theEntry = NULL;
317 theItem->displayChar = G_AMULET;
318 itemKind = 0;
319 theItem->flags |= ITEM_IDENTIFIED;
320 break;
321 case GEM:
322 theEntry = NULL;
323 theItem->displayChar = G_GEM;
324 itemKind = 0;
325 theItem->flags |= ITEM_IDENTIFIED;
326 break;
327 case KEY:
328 theEntry = NULL;
329 theItem->displayChar = G_KEY;
330 theItem->flags |= ITEM_IDENTIFIED;
331 break;
332 default:
333 theEntry = NULL;
334 message("something has gone terribly wrong!", REQUIRE_ACKNOWLEDGMENT);
335 break;
336 }
337 if (theItem
338 && !(theItem->flags & ITEM_IDENTIFIED)
339 && (!(theItem->category & (POTION | SCROLL) ) || (theEntry && !theEntry->identified))) {
340
341 theItem->flags |= ITEM_CAN_BE_IDENTIFIED;
342 }
343 theItem->kind = itemKind;
344
345 return theItem;
346 }
347
chooseKind(itemTable * theTable,short numKinds)348 short chooseKind(itemTable *theTable, short numKinds) {
349 short i, totalFrequencies = 0, randomFrequency;
350 for (i=0; i<numKinds; i++) {
351 totalFrequencies += max(0, theTable[i].frequency);
352 }
353 randomFrequency = rand_range(1, totalFrequencies);
354 for (i=0; randomFrequency > theTable[i].frequency; i++) {
355 randomFrequency -= max(0, theTable[i].frequency);
356 }
357 return i;
358 }
359
360 // Places an item at (x,y) if provided or else a random location if they're 0. Inserts item into the floor list.
placeItem(item * theItem,short x,short y)361 item *placeItem(item *theItem, short x, short y) {
362 short loc[2];
363 enum dungeonLayers layer;
364 char theItemName[DCOLS], buf[DCOLS];
365 if (x <= 0 || y <= 0) {
366 randomMatchingLocation(&(loc[0]), &(loc[1]), FLOOR, NOTHING, -1);
367 theItem->xLoc = loc[0];
368 theItem->yLoc = loc[1];
369 } else {
370 theItem->xLoc = x;
371 theItem->yLoc = y;
372 }
373
374 removeItemFromChain(theItem, floorItems); // just in case; double-placing an item will result in game-crashing loops in the item list
375 addItemToChain(theItem, floorItems);
376 pmap[theItem->xLoc][theItem->yLoc].flags |= HAS_ITEM;
377 if ((theItem->flags & ITEM_MAGIC_DETECTED) && itemMagicPolarity(theItem)) {
378 pmap[theItem->xLoc][theItem->yLoc].flags |= ITEM_DETECTED;
379 }
380 if (cellHasTerrainFlag(x, y, T_IS_DF_TRAP)
381 && !cellHasTerrainFlag(x, y, T_MOVES_ITEMS)
382 && !(pmap[x][y].flags & PRESSURE_PLATE_DEPRESSED)) {
383
384 pmap[x][y].flags |= PRESSURE_PLATE_DEPRESSED;
385 if (playerCanSee(x, y)) {
386 if (cellHasTMFlag(x, y, TM_IS_SECRET)) {
387 discover(x, y);
388 refreshDungeonCell(x, y);
389 }
390 itemName(theItem, theItemName, false, false, NULL);
391 sprintf(buf, "a pressure plate clicks underneath the %s!", theItemName);
392 message(buf, REQUIRE_ACKNOWLEDGMENT);
393 }
394 for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
395 if (tileCatalog[pmap[x][y].layers[layer]].flags & T_IS_DF_TRAP) {
396 spawnDungeonFeature(x, y, &(dungeonFeatureCatalog[tileCatalog[pmap[x][y].layers[layer]].fireType]), true, false);
397 promoteTile(x, y, layer, false);
398 }
399 }
400 }
401 return theItem;
402 }
403
fillItemSpawnHeatMap(unsigned short heatMap[DCOLS][DROWS],unsigned short heatLevel,short x,short y)404 void fillItemSpawnHeatMap(unsigned short heatMap[DCOLS][DROWS], unsigned short heatLevel, short x, short y) {
405 enum directions dir;
406 short newX, newY;
407
408 if (pmap[x][y].layers[DUNGEON] == DOOR) {
409 heatLevel += 10;
410 } else if (pmap[x][y].layers[DUNGEON] == SECRET_DOOR) {
411 heatLevel += 3000;
412 }
413 if (heatMap[x][y] > heatLevel) {
414 heatMap[x][y] = heatLevel;
415 }
416 for (dir = 0; dir < 4; dir++) {
417 newX = x + nbDirs[dir][0];
418 newY = y + nbDirs[dir][1];
419 if (coordinatesAreInMap(newX, newY)
420 && !cellHasTerrainFlag(newX, newY, T_IS_DEEP_WATER | T_LAVA_INSTA_DEATH | T_AUTO_DESCENT)
421 && isPassableOrSecretDoor(newX, newY)
422 && heatLevel < heatMap[newX][newY]) {
423
424 fillItemSpawnHeatMap(heatMap, heatLevel, newX, newY);
425 }
426 }
427 }
428
coolHeatMapAt(unsigned short heatMap[DCOLS][DROWS],short x,short y,unsigned long * totalHeat)429 void coolHeatMapAt(unsigned short heatMap[DCOLS][DROWS], short x, short y, unsigned long *totalHeat) {
430 short k, l;
431 unsigned short currentHeat;
432
433 currentHeat = heatMap[x][y];
434 if (currentHeat == 0) {
435 return;
436 }
437 *totalHeat -= heatMap[x][y];
438 heatMap[x][y] = 0;
439
440 // lower the heat near the chosen location
441 for (k = -5; k <= 5; k++) {
442 for (l = -5; l <= 5; l++) {
443 if (coordinatesAreInMap(x+k, y+l) && heatMap[x+k][y+l] == currentHeat) {
444 heatMap[x+k][y+l] = max(1, heatMap[x+k][y+l]/10);
445 *totalHeat -= (currentHeat - heatMap[x+k][y+l]);
446 }
447 }
448 }
449 }
450
451 // Returns false if no place could be found.
452 // That should happen only if the total heat is zero.
getItemSpawnLoc(unsigned short heatMap[DCOLS][DROWS],short * x,short * y,unsigned long * totalHeat)453 boolean getItemSpawnLoc(unsigned short heatMap[DCOLS][DROWS], short *x, short *y, unsigned long *totalHeat) {
454 unsigned long randIndex;
455 unsigned short currentHeat;
456 short i, j;
457
458 if (*totalHeat <= 0) {
459 return false;
460 }
461
462 randIndex = rand_range(1, *totalHeat);
463
464 //printf("\nrandIndex: %i", randIndex);
465
466 for (i=0; i<DCOLS; i++) {
467 for (j=0; j<DROWS; j++) {
468 currentHeat = heatMap[i][j];
469 if (randIndex <= currentHeat) { // this is the spot!
470 *x = i;
471 *y = j;
472 return true;
473 }
474 randIndex -= currentHeat;
475 }
476 }
477 brogueAssert(0); // should never get here!
478 return false;
479 }
480
481 // Generates and places items for the level. Must pass the location of the up-stairway on the level.
populateItems(short upstairsX,short upstairsY)482 void populateItems(short upstairsX, short upstairsY) {
483 if (!ITEMS_ENABLED) {
484 return;
485 }
486 item *theItem;
487 unsigned short itemSpawnHeatMap[DCOLS][DROWS];
488 short i, j, numberOfItems, numberOfGoldPiles, goldBonusProbability, x = 0, y = 0;
489 unsigned long totalHeat;
490 short theCategory, theKind, randomDepthOffset = 0;
491
492 const int POW_GOLD[] = {
493 // b^3.05, with b from 0 to 25:
494 0, 1, 8, 28, 68, 135, 236, 378, 568, 813, 1122, 1500, 1956, 2497, 3131,
495 3864, 4705, 5660, 6738, 7946, 9292, 10783, 12427, 14232, 16204, 18353};
496 #define aggregateGoldLowerBound(d) (POW_GOLD[d] + 320 * (d))
497 #define aggregateGoldUpperBound(d) (POW_GOLD[d] + 420 * (d))
498 const fixpt POW_FOOD[] = {
499 // b^1.35 fixed point, with b from 1 to 50 (for future-proofing):
500 65536, 167059, 288797, 425854, 575558, 736180, 906488, 1085553, 1272645,
501 1467168, 1668630, 1876612, 2090756, 2310749, 2536314, 2767208, 3003211,
502 3244126, 3489773, 3739989, 3994624, 4253540, 4516609, 4783712, 5054741,
503 5329591, 5608167, 5890379, 6176141, 6465373, 6758000, 7053950, 7353155,
504 7655551, 7961076, 8269672, 8581283, 8895856, 9213341, 9533687, 9856849,
505 10182782, 10511443, 10842789, 11176783, 11513384, 11852556, 12194264,
506 12538472, 12885148};
507
508 #ifdef AUDIT_RNG
509 char RNGmessage[100];
510 #endif
511
512 if (rogue.depthLevel > AMULET_LEVEL) {
513 if (rogue.depthLevel - AMULET_LEVEL - 1 >= 8) {
514 numberOfItems = 1;
515 } else {
516 const short lumenstoneDistribution[8] = {3, 3, 3, 2, 2, 2, 2, 2};
517 numberOfItems = lumenstoneDistribution[rogue.depthLevel - AMULET_LEVEL - 1];
518 }
519 numberOfGoldPiles = 0;
520 } else {
521 rogue.lifePotionFrequency += 34;
522 rogue.strengthPotionFrequency += 17;
523 rogue.enchantScrollFrequency += 30;
524 numberOfItems = 3;
525 while (rand_percent(60)) {
526 numberOfItems++;
527 }
528 if (rogue.depthLevel <= 2) {
529 numberOfItems += 2; // 4 extra items to kickstart your career as a rogue
530 } else if (rogue.depthLevel <= 4) {
531 numberOfItems++; // and 2 more here
532 }
533
534 numberOfGoldPiles = min(5, rogue.depthLevel / 4);
535 for (goldBonusProbability = 60;
536 rand_percent(goldBonusProbability) && numberOfGoldPiles <= 10;
537 goldBonusProbability -= 15) {
538
539 numberOfGoldPiles++;
540 }
541 // Adjust the amount of gold if we're past depth 5 and we were below or above
542 // the production schedule as of the previous depth.
543 if (rogue.depthLevel > 5) {
544 if (rogue.goldGenerated < aggregateGoldLowerBound(rogue.depthLevel - 1)) {
545 numberOfGoldPiles += 2;
546 } else if (rogue.goldGenerated > aggregateGoldUpperBound(rogue.depthLevel - 1)) {
547 numberOfGoldPiles -= 2;
548 }
549 }
550 }
551
552 // Create an item spawn heat map to bias item generation behind secret doors (and, to a lesser
553 // extent, regular doors). This is in terms of the number of secret/regular doors that must be
554 // passed to reach the area when pathing to it from the upward staircase.
555 // This is why there are often several items in well hidden secret rooms. Otherwise,
556 // those rooms are usually empty, which is demoralizing after you take the trouble to find them.
557 for (i=0; i<DCOLS; i++) {
558 for (j=0; j<DROWS; j++) {
559 itemSpawnHeatMap[i][j] = 50000;
560 }
561 }
562 fillItemSpawnHeatMap(itemSpawnHeatMap, 5, upstairsX, upstairsY);
563 totalHeat = 0;
564
565 #ifdef AUDIT_RNG
566 sprintf(RNGmessage, "\n\nInitial heat map for level %i:\n", rogue.currentTurnNumber);
567 RNGLog(RNGmessage);
568 #endif
569
570 for (j=0; j<DROWS; j++) {
571 for (i=0; i<DCOLS; i++) {
572 if (cellHasTerrainFlag(i, j, T_OBSTRUCTS_ITEMS | T_PATHING_BLOCKER)
573 || (pmap[i][j].flags & (IS_CHOKEPOINT | IN_LOOP | IS_IN_MACHINE))
574 || passableArcCount(i, j) > 1) { // Not in walls, hallways, quest rooms, loops or chokepoints, please.
575
576 itemSpawnHeatMap[i][j] = 0;
577 } else if (itemSpawnHeatMap[i][j] == 50000) {
578 itemSpawnHeatMap[i][j] = 0;
579 pmap[i][j].layers[DUNGEON] = WALL; // due to a bug that created occasional isolated one-cell islands;
580 // not sure if it's still around, but this is a good-enough failsafe
581 }
582 #ifdef AUDIT_RNG
583 sprintf(RNGmessage, "%u%s%s\t%s",
584 itemSpawnHeatMap[i][j],
585 ((pmap[i][j].flags & IS_CHOKEPOINT) ? " (C)": ""), // chokepoint
586 ((pmap[i][j].flags & IN_LOOP) ? " (L)": ""), // loop
587 (i == DCOLS-1 ? "\n" : ""));
588 RNGLog(RNGmessage);
589 #endif
590 totalHeat += itemSpawnHeatMap[i][j];
591 }
592 }
593
594 if (D_INSPECT_LEVELGEN) {
595 short **map = allocGrid();
596 for (i=0; i<DCOLS; i++) {
597 for (j=0; j<DROWS; j++) {
598 map[i][j] = itemSpawnHeatMap[i][j] * -1;
599 }
600 }
601 dumpLevelToScreen();
602 displayGrid(map);
603 freeGrid(map);
604 temporaryMessage("Item spawn heat map:", REQUIRE_ACKNOWLEDGMENT);
605 }
606
607 if (rogue.depthLevel > 2) {
608 // Include a random factor in food and potion of life generation to make things slightly less predictable.
609 randomDepthOffset = rand_range(-1, 1);
610 randomDepthOffset += rand_range(-1, 1);
611 }
612
613 for (i=0; i<numberOfItems; i++) {
614 theCategory = ALL_ITEMS & ~GOLD; // gold is placed separately, below, so it's not a punishment
615 theKind = -1;
616
617 scrollTable[SCROLL_ENCHANTING].frequency = rogue.enchantScrollFrequency;
618 potionTable[POTION_STRENGTH].frequency = rogue.strengthPotionFrequency;
619 potionTable[POTION_LIFE].frequency = rogue.lifePotionFrequency;
620
621 // Adjust the desired item category if necessary.
622 if ((rogue.foodSpawned + foodTable[RATION].strengthRequired / 3) * 4 * FP_FACTOR
623 <= (POW_FOOD[rogue.depthLevel-1] + (randomDepthOffset * FP_FACTOR)) * foodTable[RATION].strengthRequired * 45/100) {
624 // Guarantee a certain nutrition minimum of the approximate equivalent of one ration every four levels,
625 // with more food on deeper levels since they generally take more turns to complete.
626 theCategory = FOOD;
627 if (rogue.depthLevel > AMULET_LEVEL) {
628 numberOfItems++; // Food isn't at the expense of lumenstones.
629 }
630 } else if (rogue.depthLevel > AMULET_LEVEL) {
631 theCategory = GEM;
632 } else if (rogue.lifePotionsSpawned * 4 + 3 < rogue.depthLevel + randomDepthOffset) {
633 theCategory = POTION;
634 theKind = POTION_LIFE;
635 }
636
637 // Generate the item.
638 theItem = generateItem(theCategory, theKind);
639 theItem->originDepth = rogue.depthLevel;
640
641 if (theItem->category & FOOD) {
642 rogue.foodSpawned += foodTable[theItem->kind].strengthRequired;
643 if (D_MESSAGE_ITEM_GENERATION) printf("\n(:) Depth %i: generated food", rogue.depthLevel);
644 }
645
646 // Choose a placement location.
647 if ((theItem->category & FOOD) || ((theItem->category & POTION) && theItem->kind == POTION_STRENGTH)) {
648 do {
649 randomMatchingLocation(&x, &y, FLOOR, NOTHING, -1); // Food and gain strength don't follow the heat map.
650 } while (passableArcCount(x, y) > 1); // Not in a hallway.
651 } else {
652 getItemSpawnLoc(itemSpawnHeatMap, &x, &y, &totalHeat);
653 }
654 brogueAssert(coordinatesAreInMap(x, y));
655 // Cool off the item spawning heat map at the chosen location:
656 coolHeatMapAt(itemSpawnHeatMap, x, y, &totalHeat);
657
658 // Regulate the frequency of enchantment scrolls and strength/life potions.
659 if ((theItem->category & SCROLL) && theItem->kind == SCROLL_ENCHANTING) {
660 rogue.enchantScrollFrequency -= 50;
661 if (D_MESSAGE_ITEM_GENERATION) printf("\n(?) Depth %i: generated an enchant scroll at %i frequency", rogue.depthLevel, rogue.enchantScrollFrequency);
662 } else if (theItem->category & POTION && theItem->kind == POTION_LIFE) {
663 if (D_MESSAGE_ITEM_GENERATION) printf("\n(!l) Depth %i: generated a life potion at %i frequency", rogue.depthLevel, rogue.lifePotionFrequency);
664 rogue.lifePotionFrequency -= 150;
665 rogue.lifePotionsSpawned++;
666 } else if (theItem->category & POTION && theItem->kind == POTION_STRENGTH) {
667 if (D_MESSAGE_ITEM_GENERATION) printf("\n(!s) Depth %i: generated a strength potion at %i frequency", rogue.depthLevel, rogue.strengthPotionFrequency);
668 rogue.strengthPotionFrequency -= 50;
669 }
670
671 // Place the item.
672 placeItem(theItem, x, y); // Random valid location already obtained according to heat map.
673 brogueAssert(!cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY));
674
675 if (D_INSPECT_LEVELGEN) {
676 short **map = allocGrid();
677 short i2, j2;
678 for (i2=0; i2<DCOLS; i2++) {
679 for (j2=0; j2<DROWS; j2++) {
680 map[i2][j2] = itemSpawnHeatMap[i2][j2] * -1;
681 }
682 }
683 dumpLevelToScreen();
684 displayGrid(map);
685 freeGrid(map);
686 plotCharWithColor(theItem->displayChar, mapToWindowX(x), mapToWindowY(y), &black, &purple);
687 temporaryMessage("Added an item.", REQUIRE_ACKNOWLEDGMENT);
688 }
689 }
690
691 // Now generate gold.
692 for (i=0; i<numberOfGoldPiles; i++) {
693 theItem = generateItem(GOLD, -1);
694 getItemSpawnLoc(itemSpawnHeatMap, &x, &y, &totalHeat);
695 coolHeatMapAt(itemSpawnHeatMap, x, y, &totalHeat);
696 placeItem(theItem, x, y);
697 rogue.goldGenerated += theItem->quantity;
698 }
699
700 if (D_INSPECT_LEVELGEN) {
701 dumpLevelToScreen();
702 temporaryMessage("Added gold.", REQUIRE_ACKNOWLEDGMENT);
703 }
704
705 scrollTable[SCROLL_ENCHANTING].frequency = 0; // No enchant scrolls or strength/life potions can spawn except via initial
706 potionTable[POTION_STRENGTH].frequency = 0; // item population or blueprints that create them specifically.
707 potionTable[POTION_LIFE].frequency = 0;
708
709 if (D_MESSAGE_ITEM_GENERATION) printf("\n---- Depth %i: %lu gold generated so far.", rogue.depthLevel, rogue.goldGenerated);
710 }
711
712 // Name of this function is a bit misleading -- basically returns true iff the item will stack without consuming an extra slot
713 // i.e. if it's a throwing weapon with a sibling already in your pack. False for potions and scrolls.
itemWillStackWithPack(item * theItem)714 boolean itemWillStackWithPack(item *theItem) {
715 item *tempItem;
716 if (theItem->category & GEM) {
717 for (tempItem = packItems->nextItem;
718 tempItem != NULL && !((tempItem->category & GEM) && theItem->originDepth == tempItem->originDepth);
719 tempItem = tempItem->nextItem);
720 return (tempItem ? true : false);
721 } else if (!(theItem->quiverNumber)) {
722 return false;
723 } else {
724 for (tempItem = packItems->nextItem;
725 tempItem != NULL && tempItem->quiverNumber != theItem->quiverNumber;
726 tempItem = tempItem->nextItem);
727 return (tempItem ? true : false);
728 }
729 }
730
removeItemFrom(short x,short y)731 void removeItemFrom(short x, short y) {
732 short layer;
733
734 pmap[x][y].flags &= ~HAS_ITEM;
735
736 if (cellHasTMFlag(x, y, TM_PROMOTES_ON_ITEM_PICKUP)) {
737 for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
738 if (tileCatalog[pmap[x][y].layers[layer]].mechFlags & TM_PROMOTES_ON_ITEM_PICKUP) {
739 promoteTile(x, y, layer, false);
740 }
741 }
742 }
743 }
744
745 // adds the item at (x,y) to the pack
pickUpItemAt(short x,short y)746 void pickUpItemAt(short x, short y) {
747 item *theItem;
748 creature *monst;
749 char buf[COLS * 3], buf2[COLS * 3];
750 short guardianX, guardianY;
751
752 rogue.disturbed = true;
753
754 // find the item
755 theItem = itemAtLoc(x, y);
756
757 if (!theItem) {
758 message("Error: Expected item; item not found.", REQUIRE_ACKNOWLEDGMENT);
759 return;
760 }
761
762 if ((theItem->flags & ITEM_KIND_AUTO_ID)
763 && tableForItemCategory(theItem->category, NULL)
764 && !(tableForItemCategory(theItem->category, NULL)[theItem->kind].identified)) {
765
766 identifyItemKind(theItem);
767 }
768
769 if ((theItem->category & WAND)
770 && wandTable[theItem->kind].identified
771 && wandTable[theItem->kind].range.lowerBound == wandTable[theItem->kind].range.upperBound) {
772
773 theItem->flags |= ITEM_IDENTIFIED;
774 }
775
776 if (numberOfItemsInPack() < MAX_PACK_ITEMS || (theItem->category & GOLD) || itemWillStackWithPack(theItem)) {
777 // remove from floor chain
778 pmap[x][y].flags &= ~ITEM_DETECTED;
779
780 if (!removeItemFromChain(theItem, floorItems)) {
781 brogueAssert(false);
782 }
783
784 if (theItem->category & GOLD) {
785 rogue.gold += theItem->quantity;
786 rogue.featRecord[FEAT_TONE] = false;
787 sprintf(buf, "you found %i pieces of gold.", theItem->quantity);
788 messageWithColor(buf, &itemMessageColor, 0);
789 deleteItem(theItem);
790 removeItemFrom(x, y); // triggers tiles with T_PROMOTES_ON_ITEM_PICKUP
791 return;
792 }
793
794 if ((theItem->category & AMULET) && numberOfMatchingPackItems(AMULET, 0, 0, false)) {
795 message("you already have the Amulet of Yendor.", 0);
796 deleteItem(theItem);
797 return;
798 }
799
800 theItem = addItemToPack(theItem);
801
802 itemName(theItem, buf2, true, true, NULL); // include suffix, article
803
804 sprintf(buf, "you now have %s (%c).", buf2, theItem->inventoryLetter);
805 messageWithColor(buf, &itemMessageColor, 0);
806
807 removeItemFrom(x, y); // triggers tiles with T_PROMOTES_ON_ITEM_PICKUP
808
809 if ((theItem->category & AMULET)
810 && !(rogue.yendorWarden)) {
811 // Identify the amulet guardian, or generate one if there isn't one.
812 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
813 creature *monst = nextCreature(&it);
814 if (monst->info.monsterID == MK_WARDEN_OF_YENDOR) {
815 rogue.yendorWarden = monst;
816 break;
817 }
818 }
819 if (!rogue.yendorWarden) {
820 getRandomMonsterSpawnLocation(&guardianX, &guardianY);
821 monst = generateMonster(MK_WARDEN_OF_YENDOR, false, false);
822 monst->xLoc = guardianX;
823 monst->yLoc = guardianY;
824 pmap[guardianX][guardianY].flags |= HAS_MONSTER;
825 rogue.yendorWarden = monst;
826 }
827 }
828 } else {
829 theItem->flags |= ITEM_PLAYER_AVOIDS; // explore shouldn't try to pick it up more than once.
830 itemName(theItem, buf2, false, true, NULL); // include article
831 sprintf(buf, "Your pack is too full to pick up %s.", buf2);
832 message(buf, 0);
833 }
834 }
835
conflateItemCharacteristics(item * newItem,item * oldItem)836 void conflateItemCharacteristics(item *newItem, item *oldItem) {
837
838 // let magic detection and other flags propagate to the new stack...
839 newItem->flags |= (oldItem->flags & (ITEM_MAGIC_DETECTED | ITEM_IDENTIFIED | ITEM_PROTECTED | ITEM_RUNIC
840 | ITEM_RUNIC_HINTED | ITEM_CAN_BE_IDENTIFIED | ITEM_MAX_CHARGES_KNOWN));
841
842 // keep the higher enchantment and lower strength requirement...
843 if (oldItem->enchant1 > newItem->enchant1) {
844 newItem->enchant1 = oldItem->enchant1;
845 }
846 if (oldItem->strengthRequired < newItem->strengthRequired) {
847 newItem->strengthRequired = oldItem->strengthRequired;
848 }
849 // Keep track of origin depth only if every item in the stack has the same origin depth.
850 if (oldItem->originDepth <= 0 || newItem->originDepth != oldItem->originDepth) {
851 newItem->originDepth = 0;
852 }
853 }
854
stackItems(item * newItem,item * oldItem)855 void stackItems(item *newItem, item *oldItem) {
856 //Increment the quantity of the old item...
857 newItem->quantity += oldItem->quantity;
858
859 // ...conflate attributes...
860 conflateItemCharacteristics(newItem, oldItem);
861
862 // ...and delete the new item.
863 deleteItem(oldItem);
864 }
865
inventoryLetterAvailable(char proposedLetter)866 boolean inventoryLetterAvailable(char proposedLetter) {
867 item *theItem;
868 if (proposedLetter >= 'a'
869 && proposedLetter <= 'z') {
870
871 for (theItem = packItems->nextItem; theItem != NULL; theItem = theItem->nextItem) {
872 if (theItem->inventoryLetter == proposedLetter) {
873 return false;
874 }
875 }
876 return true;
877 }
878 return false;
879 }
880
addItemToPack(item * theItem)881 item *addItemToPack(item *theItem) {
882 item *previousItem, *tempItem;
883 char itemLetter;
884
885 // Can the item stack with another in the inventory?
886 if (theItem->category & (FOOD|POTION|SCROLL|GEM)) {
887 for (tempItem = packItems->nextItem; tempItem != NULL; tempItem = tempItem->nextItem) {
888 if (theItem->category == tempItem->category
889 && theItem->kind == tempItem->kind
890 && (!(theItem->category & GEM) || theItem->originDepth == tempItem->originDepth)) {
891
892 // We found a match!
893 stackItems(tempItem, theItem);
894
895 // Pass back the incremented (old) item. No need to add it to the pack since it's already there.
896 return tempItem;
897 }
898 }
899 } else if (theItem->category & WEAPON && theItem->quiverNumber > 0) {
900 for (tempItem = packItems->nextItem; tempItem != NULL; tempItem = tempItem->nextItem) {
901 if (theItem->category == tempItem->category && theItem->kind == tempItem->kind
902 && theItem->quiverNumber == tempItem->quiverNumber) {
903 // We found a match!
904 stackItems(tempItem, theItem);
905
906 // Pass back the incremented (old) item. No need to add it to the pack since it's already there.
907 return tempItem;
908 }
909 }
910 }
911
912 // assign a reference letter to the item
913 if (!inventoryLetterAvailable(theItem->inventoryLetter)) {
914 itemLetter = nextAvailableInventoryCharacter();
915 if (itemLetter) {
916 theItem->inventoryLetter = itemLetter;
917 }
918 }
919
920 // insert at proper place in pack chain
921 for (previousItem = packItems;
922 previousItem->nextItem != NULL && previousItem->nextItem->category <= theItem->category;
923 previousItem = previousItem->nextItem);
924 theItem->nextItem = previousItem->nextItem;
925 previousItem->nextItem = theItem;
926
927 return theItem;
928 }
929
numberOfItemsInPack()930 short numberOfItemsInPack() {
931 short theCount = 0;
932 item *theItem;
933 for(theItem = packItems->nextItem; theItem != NULL; theItem = theItem->nextItem) {
934 theCount += (theItem->category & (WEAPON | GEM) ? 1 : theItem->quantity);
935 }
936 return theCount;
937 }
938
nextAvailableInventoryCharacter()939 char nextAvailableInventoryCharacter() {
940 boolean charTaken[26];
941 short i;
942 item *theItem;
943 char c;
944 for(i=0; i<26; i++) {
945 charTaken[i] = false;
946 }
947 for (theItem = packItems->nextItem; theItem != NULL; theItem = theItem->nextItem) {
948 c = theItem->inventoryLetter;
949 if (c >= 'a' && c <= 'z') {
950 charTaken[c - 'a'] = true;
951 }
952 }
953 for(i=0; i<26; i++) {
954 if (!charTaken[i]) {
955 return ('a' + i);
956 }
957 }
958 return 0;
959 }
960
checkForDisenchantment(item * theItem)961 void checkForDisenchantment(item *theItem) {
962 char buf[COLS], buf2[COLS];
963
964 if ((theItem->flags & ITEM_RUNIC)
965 && (((theItem->category & WEAPON) && theItem->enchant2 < NUMBER_GOOD_WEAPON_ENCHANT_KINDS) || ((theItem->category & ARMOR) && theItem->enchant2 < NUMBER_GOOD_ARMOR_ENCHANT_KINDS))
966 && theItem->enchant1 <= 0) {
967
968 theItem->enchant2 = 0;
969 theItem->flags &= ~(ITEM_RUNIC | ITEM_RUNIC_HINTED | ITEM_RUNIC_IDENTIFIED);
970
971 if (theItem->flags & ITEM_IDENTIFIED) {
972 identify(theItem);
973 itemName(theItem, buf2, false, false, NULL);
974 sprintf(buf, "the runes fade from your %s.", buf2);
975 messageWithColor(buf, &itemMessageColor, 0);
976 }
977 }
978 if (theItem->flags & ITEM_CURSED
979 && theItem->enchant1 >= 0) {
980
981 theItem->flags &= ~ITEM_CURSED;
982 }
983 }
984
itemIsSwappable(const item * theItem)985 boolean itemIsSwappable(const item *theItem) {
986 if ((theItem->category & CAN_BE_SWAPPED)
987 && theItem->quiverNumber == 0) {
988
989 return true;
990 } else {
991 return false;
992 }
993 }
994
swapItemToEnchantLevel(item * theItem,short newEnchant,boolean enchantmentKnown)995 void swapItemToEnchantLevel(item *theItem, short newEnchant, boolean enchantmentKnown) {
996 short x, y, charmPercent;
997 char buf1[COLS * 3], buf2[COLS * 3];
998
999 if ((theItem->category & STAFF) && newEnchant < 2
1000 || (theItem->category & CHARM) && newEnchant < 1
1001 || (theItem->category & WAND) && newEnchant < 0) {
1002
1003 itemName(theItem, buf1, false, true, NULL);
1004 sprintf(buf2, "%s shatter%s from the strain!",
1005 buf1,
1006 theItem->quantity == 1 ? "s" : "");
1007 x = theItem->xLoc;
1008 y = theItem->yLoc;
1009 removeItemFromChain(theItem, floorItems);
1010 pmap[x][y].flags &= ~(HAS_ITEM | ITEM_DETECTED);
1011 if (pmap[x][y].flags & (ANY_KIND_OF_VISIBLE | DISCOVERED | ITEM_DETECTED)) {
1012 refreshDungeonCell(x, y);
1013 }
1014 if (playerCanSee(x, y)) {
1015 messageWithColor(buf2, &itemMessageColor, 0);
1016 }
1017 } else {
1018 if ((theItem->category & STAFF)
1019 && theItem->charges > newEnchant) {
1020
1021 theItem->charges = newEnchant;
1022 }
1023 if (theItem->category & CHARM) {
1024 charmPercent = theItem->charges * 100 / charmRechargeDelay(theItem->kind, theItem->enchant1);
1025 theItem->charges = charmPercent * charmRechargeDelay(theItem->kind, newEnchant) / 100;
1026 }
1027 if (enchantmentKnown) {
1028 if (theItem->category & STAFF) {
1029 theItem->flags |= ITEM_MAX_CHARGES_KNOWN;
1030 }
1031 theItem->flags |= ITEM_IDENTIFIED;
1032 } else {
1033 theItem->flags &= ~(ITEM_MAX_CHARGES_KNOWN | ITEM_IDENTIFIED);
1034 theItem->flags |= ITEM_CAN_BE_IDENTIFIED;
1035 if (theItem->category & WEAPON) {
1036 theItem->charges = WEAPON_KILLS_TO_AUTO_ID; // kill this many enemies to auto-identify
1037 } else if (theItem->category & ARMOR) {
1038 theItem->charges = ARMOR_DELAY_TO_AUTO_ID; // this many turns until it reveals its enchants and whether runic
1039 } else if (theItem->category & RING) {
1040 theItem->charges = RING_DELAY_TO_AUTO_ID; // how many turns of being worn until it auto-identifies
1041 }
1042 }
1043 if (theItem->category & WAND) {
1044 theItem->charges = newEnchant;
1045 } else {
1046 theItem->enchant1 = newEnchant;
1047 }
1048 checkForDisenchantment(theItem);
1049 }
1050 }
1051
enchantLevelKnown(const item * theItem)1052 boolean enchantLevelKnown(const item *theItem) {
1053 if ((theItem->category & STAFF)
1054 && (theItem->flags & ITEM_MAX_CHARGES_KNOWN)) {
1055
1056 return true;
1057 } else {
1058 return (theItem->flags & ITEM_IDENTIFIED);
1059 }
1060 }
1061
effectiveEnchantLevel(const item * theItem)1062 short effectiveEnchantLevel(const item *theItem) {
1063 if (theItem->category & WAND) {
1064 return theItem->charges;
1065 } else {
1066 return theItem->enchant1;
1067 }
1068 }
1069
swapItemEnchants(const short machineNumber)1070 boolean swapItemEnchants(const short machineNumber) {
1071 item *lockedItem, *tempItem;
1072 short i, j, oldEnchant;
1073 boolean enchantmentKnown;
1074
1075 lockedItem = NULL;
1076 for (i = 0; i < DCOLS; i++) {
1077 for (j = 0; j < DROWS; j++) {
1078 tempItem = itemAtLoc(i, j);
1079 if (tempItem
1080 && pmap[i][j].machineNumber == machineNumber
1081 && cellHasTMFlag(i, j, TM_SWAP_ENCHANTS_ACTIVATION)
1082 && itemIsSwappable(tempItem)) {
1083
1084 if (lockedItem) {
1085 if (effectiveEnchantLevel(lockedItem) != effectiveEnchantLevel(tempItem)) {
1086 // Presto change-o!
1087 oldEnchant = effectiveEnchantLevel(lockedItem);
1088 enchantmentKnown = enchantLevelKnown(lockedItem);
1089 swapItemToEnchantLevel(lockedItem, effectiveEnchantLevel(tempItem), enchantLevelKnown(tempItem));
1090 swapItemToEnchantLevel(tempItem, oldEnchant, enchantmentKnown);
1091 return true;
1092 }
1093 } else {
1094 lockedItem = tempItem;
1095 }
1096 }
1097 }
1098 }
1099 return false;
1100 }
1101
updateFloorItems()1102 void updateFloorItems() {
1103 short x, y, loc[2];
1104 char buf[DCOLS*3], buf2[DCOLS*3];
1105 enum dungeonLayers layer;
1106 item *theItem, *nextItem;
1107
1108 for (theItem=floorItems->nextItem; theItem != NULL; theItem = nextItem) {
1109 nextItem = theItem->nextItem;
1110 x = theItem->xLoc;
1111 y = theItem->yLoc;
1112 if (rogue.absoluteTurnNumber < theItem->spawnTurnNumber) {
1113 // we are simulating an earlier turn than when the item fell into this level... let's not touch it yet
1114 continue;
1115 }
1116 if (cellHasTerrainFlag(x, y, T_AUTO_DESCENT)) {
1117 if (playerCanSeeOrSense(x, y)) {
1118 itemName(theItem, buf, false, false, NULL);
1119 sprintf(buf2, "The %s plunge%s out of sight!", buf, (theItem->quantity > 1 ? "" : "s"));
1120 messageWithColor(buf2, &itemMessageColor, 0);
1121 }
1122 if (playerCanSee(x, y)) {
1123 discover(x, y);
1124 }
1125 theItem->flags |= ITEM_PREPLACED;
1126
1127 // Remove from item chain.
1128 removeItemFromChain(theItem, floorItems);
1129
1130 pmap[x][y].flags &= ~(HAS_ITEM | ITEM_DETECTED);
1131
1132 if (theItem->category == POTION || rogue.depthLevel == DEEPEST_LEVEL) {
1133 // Potions don't survive the fall.
1134 deleteItem(theItem);
1135 } else {
1136 // Add to next level's chain.
1137 theItem->spawnTurnNumber = rogue.absoluteTurnNumber;
1138 theItem->nextItem = levels[rogue.depthLevel-1 + 1].items;
1139 levels[rogue.depthLevel-1 + 1].items = theItem;
1140 }
1141 refreshDungeonCell(x, y);
1142 continue;
1143 }
1144 if ((cellHasTerrainFlag(x, y, T_IS_FIRE) && (theItem->flags & ITEM_FLAMMABLE))
1145 || (cellHasTerrainFlag(x, y, T_LAVA_INSTA_DEATH) && !(theItem->category & AMULET))) {
1146
1147 burnItem(theItem);
1148 continue;
1149 }
1150 if (cellHasTerrainFlag(x, y, T_MOVES_ITEMS)) {
1151 getQualifyingLocNear(loc, x, y, true, 0, (T_OBSTRUCTS_ITEMS | T_OBSTRUCTS_PASSABILITY), (HAS_ITEM), false, false);
1152 removeItemFrom(x, y);
1153 pmap[loc[0]][loc[1]].flags |= HAS_ITEM;
1154 if (pmap[x][y].flags & ITEM_DETECTED) {
1155 pmap[x][y].flags &= ~ITEM_DETECTED;
1156 pmap[loc[0]][loc[1]].flags |= ITEM_DETECTED;
1157 }
1158 theItem->xLoc = loc[0];
1159 theItem->yLoc = loc[1];
1160 refreshDungeonCell(x, y);
1161 refreshDungeonCell(loc[0], loc[1]);
1162 continue;
1163 }
1164 if (cellHasTMFlag(x, y, TM_PROMOTES_ON_ITEM)) {
1165 for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
1166 if (tileCatalog[pmap[x][y].layers[layer]].mechFlags & TM_PROMOTES_ON_ITEM) {
1167 promoteTile(x, y, layer, false);
1168 }
1169 }
1170 continue;
1171 }
1172 if (pmap[x][y].machineNumber
1173 && pmap[x][y].machineNumber == pmap[player.xLoc][player.yLoc].machineNumber
1174 && (theItem->flags & ITEM_KIND_AUTO_ID)) {
1175
1176 identifyItemKind(theItem);
1177 }
1178 if (cellHasTMFlag(x, y, TM_SWAP_ENCHANTS_ACTIVATION)
1179 && pmap[x][y].machineNumber) {
1180
1181 while (nextItem != NULL
1182 && pmap[x][y].machineNumber == pmap[nextItem->xLoc][nextItem->yLoc].machineNumber
1183 && cellHasTMFlag(nextItem->xLoc, nextItem->yLoc, TM_SWAP_ENCHANTS_ACTIVATION)) {
1184
1185 // Skip future items that are also swappable, so that we don't inadvertently
1186 // destroy the next item and then try to update it.
1187 nextItem = nextItem->nextItem;
1188 }
1189
1190 if (!circuitBreakersPreventActivation(pmap[x][y].machineNumber)
1191 && swapItemEnchants(pmap[x][y].machineNumber)) {
1192
1193 activateMachine(pmap[x][y].machineNumber);
1194 }
1195 }
1196 }
1197 }
1198
inscribeItem(item * theItem)1199 boolean inscribeItem(item *theItem) {
1200 char itemText[30], buf[COLS * 3], nameOfItem[COLS * 3], oldInscription[COLS];
1201
1202 strcpy(oldInscription, theItem->inscription);
1203 theItem->inscription[0] = '\0';
1204 itemName(theItem, nameOfItem, true, true, NULL);
1205 strcpy(theItem->inscription, oldInscription);
1206
1207 sprintf(buf, "inscribe: %s \"", nameOfItem);
1208 if (getInputTextString(itemText, buf, min(29, DCOLS - strLenWithoutEscapes(buf) - 1), "", "\"", TEXT_INPUT_NORMAL, false)) {
1209 strcpy(theItem->inscription, itemText);
1210 confirmMessages();
1211 itemName(theItem, nameOfItem, true, true, NULL);
1212 sprintf(buf, "%s %s.", (theItem->quantity > 1 ? "they're" : "it's"), nameOfItem);
1213 messageWithColor(buf, &itemMessageColor, 0);
1214 return true;
1215 } else {
1216 confirmMessages();
1217 return false;
1218 }
1219 }
1220
itemCanBeCalled(item * theItem)1221 boolean itemCanBeCalled(item *theItem) {
1222 if (theItem->category & (WEAPON|ARMOR|SCROLL|RING|POTION|STAFF|WAND|CHARM)) {
1223 return true;
1224 } else if ((theItem->category & (POTION | SCROLL))
1225 && !tableForItemCategory(theItem->category, NULL)[theItem->kind].identified) {
1226 return true;
1227 } else {
1228 return false;
1229 }
1230 }
1231
call(item * theItem)1232 void call(item *theItem) {
1233 char itemText[30], buf[COLS * 3];
1234 short c;
1235 unsigned char command[100];
1236 item *tempItem;
1237
1238 c = 0;
1239 command[c++] = CALL_KEY;
1240 if (theItem == NULL) {
1241 // Need to gray out known potions and scrolls from inventory selection.
1242 // Hijack the "item can be identified" flag for this purpose,
1243 // and then reset it immediately afterward.
1244 for (tempItem = packItems->nextItem; tempItem != NULL; tempItem = tempItem->nextItem) {
1245 if ((tempItem->category & (POTION | SCROLL))
1246 && tableForItemCategory(tempItem->category, NULL)[tempItem->kind].identified) {
1247
1248 tempItem->flags &= ~ITEM_CAN_BE_IDENTIFIED;
1249 } else {
1250 tempItem->flags |= ITEM_CAN_BE_IDENTIFIED;
1251 }
1252 }
1253 theItem = promptForItemOfType((WEAPON|ARMOR|SCROLL|RING|POTION|STAFF|WAND|CHARM), ITEM_CAN_BE_IDENTIFIED, 0,
1254 KEYBOARD_LABELS ? "Call what? (a-z, shift for more info; or <esc> to cancel)" : "Call what?",
1255 true);
1256 updateIdentifiableItems(); // Reset the flags.
1257 }
1258 if (theItem == NULL) {
1259 return;
1260 }
1261
1262 command[c++] = theItem->inventoryLetter;
1263
1264 confirmMessages();
1265
1266 if ((theItem->flags & ITEM_IDENTIFIED) || theItem->category & (WEAPON|ARMOR|CHARM|FOOD|GOLD|AMULET|GEM)) {
1267 if (theItem->category & (WEAPON | ARMOR | CHARM | STAFF | WAND | RING)) {
1268 if (inscribeItem(theItem)) {
1269 command[c++] = '\0';
1270 strcat((char *) command, theItem->inscription);
1271 recordKeystrokeSequence(command);
1272 recordKeystroke(RETURN_KEY, false, false);
1273 }
1274 } else {
1275 message("you already know what that is.", 0);
1276 }
1277 return;
1278 }
1279
1280 if (theItem->category & (WEAPON | ARMOR | STAFF | WAND | RING)) {
1281 if (tableForItemCategory(theItem->category, NULL)[theItem->kind].identified) {
1282 if (inscribeItem(theItem)) {
1283 command[c++] = '\0';
1284 strcat((char *) command, theItem->inscription);
1285 recordKeystrokeSequence(command);
1286 recordKeystroke(RETURN_KEY, false, false);
1287 }
1288 return;
1289 } else if (confirm("Inscribe this particular item instead of all similar items?", true)) {
1290 command[c++] = 'y'; // y means yes, since the recording also needs to negotiate the above confirmation prompt.
1291 if (inscribeItem(theItem)) {
1292 command[c++] = '\0';
1293 strcat((char *) command, theItem->inscription);
1294 recordKeystrokeSequence(command);
1295 recordKeystroke(RETURN_KEY, false, false);
1296 }
1297 return;
1298 } else {
1299 command[c++] = 'n'; // n means no
1300 }
1301 }
1302
1303 if (tableForItemCategory(theItem->category, NULL)
1304 && !(tableForItemCategory(theItem->category, NULL)[theItem->kind].identified)) {
1305
1306 if (getInputTextString(itemText, "call them: \"", 29, "", "\"", TEXT_INPUT_NORMAL, false)) {
1307 command[c++] = '\0';
1308 strcat((char *) command, itemText);
1309 recordKeystrokeSequence(command);
1310 recordKeystroke(RETURN_KEY, false, false);
1311 if (itemText[0]) {
1312 strcpy(tableForItemCategory(theItem->category, NULL)[theItem->kind].callTitle, itemText);
1313 tableForItemCategory(theItem->category, NULL)[theItem->kind].called = true;
1314 } else {
1315 tableForItemCategory(theItem->category, NULL)[theItem->kind].callTitle[0] = '\0';
1316 tableForItemCategory(theItem->category, NULL)[theItem->kind].called = false;
1317 }
1318 confirmMessages();
1319 itemName(theItem, buf, false, true, NULL);
1320 messageWithColor(buf, &itemMessageColor, 0);
1321 }
1322 } else {
1323 message("you already know what that is.", 0);
1324 }
1325 }
1326
1327 // Generates the item name and returns it in the "root" string.
1328 // IncludeDetails governs things such as enchantment, charges, strength requirement, times used, etc.
1329 // IncludeArticle governs the article -- e.g. "some" food, "5" darts, "a" pink potion.
1330 // If baseColor is provided, then the suffix will be in gray, flavor portions of the item name (e.g. a "pink" potion,
1331 // a "sandalwood" staff, a "ruby" ring) will be in dark purple, and the Amulet of Yendor and lumenstones will be in yellow.
1332 // BaseColor itself will be the color that the name reverts to outside of these colored portions.
itemName(item * theItem,char * root,boolean includeDetails,boolean includeArticle,color * baseColor)1333 void itemName(item *theItem, char *root, boolean includeDetails, boolean includeArticle, color *baseColor) {
1334 char buf[DCOLS * 5], pluralization[10], article[10] = "", runicName[30],
1335 grayEscapeSequence[5], purpleEscapeSequence[5], yellowEscapeSequence[5], baseEscapeSequence[5];
1336 color tempColor;
1337
1338 strcpy(pluralization, (theItem->quantity > 1 ? "s" : ""));
1339
1340 grayEscapeSequence[0] = '\0';
1341 purpleEscapeSequence[0] = '\0';
1342 yellowEscapeSequence[0] = '\0';
1343 baseEscapeSequence[0] = '\0';
1344 if (baseColor) {
1345 tempColor = backgroundMessageColor;
1346 applyColorMultiplier(&tempColor, baseColor); // To gray out the purple if necessary.
1347 encodeMessageColor(purpleEscapeSequence, 0, &tempColor);
1348
1349 tempColor = gray;
1350 //applyColorMultiplier(&tempColor, baseColor);
1351 encodeMessageColor(grayEscapeSequence, 0, &tempColor);
1352
1353 tempColor = itemMessageColor;
1354 applyColorMultiplier(&tempColor, baseColor);
1355 encodeMessageColor(yellowEscapeSequence, 0, &tempColor);
1356
1357 encodeMessageColor(baseEscapeSequence, 0, baseColor);
1358 }
1359
1360 switch (theItem -> category) {
1361 case FOOD:
1362 if (theItem -> kind == FRUIT) {
1363 sprintf(root, "mango%s", pluralization);
1364 } else {
1365 if (theItem->quantity == 1) {
1366 sprintf(article, "some ");
1367 sprintf(root, "food");
1368 } else {
1369 sprintf(root, "ration%s of food", pluralization);
1370 }
1371 }
1372 break;
1373 case WEAPON:
1374 sprintf(root, "%s%s", weaponTable[theItem->kind].name, pluralization);
1375 if (includeDetails) {
1376 if ((theItem->flags & ITEM_IDENTIFIED) || rogue.playbackOmniscience) {
1377 sprintf(buf, "%s%i %s", (theItem->enchant1 < 0 ? "" : "+"), theItem->enchant1, root);
1378 strcpy(root, buf);
1379 }
1380
1381 if (theItem->flags & ITEM_RUNIC) {
1382 if ((theItem->flags & ITEM_RUNIC_IDENTIFIED) || rogue.playbackOmniscience) {
1383 itemRunicName(theItem, runicName);
1384 sprintf(buf, "%s of %s%s", root, runicName, grayEscapeSequence);
1385 strcpy(root, buf);
1386 } else if (theItem->flags & (ITEM_IDENTIFIED | ITEM_RUNIC_HINTED)) {
1387 if (grayEscapeSequence[0]) {
1388 strcat(root, grayEscapeSequence);
1389 }
1390 strcat(root, " (unknown runic)");
1391 }
1392 }
1393 sprintf(buf, "%s%s <%i>", root, grayEscapeSequence, theItem->strengthRequired);
1394 strcpy(root, buf);
1395 }
1396 break;
1397 case ARMOR:
1398 sprintf(root, "%s", armorTable[theItem->kind].name);
1399 if (includeDetails) {
1400
1401 if ((theItem->flags & ITEM_RUNIC)
1402 && ((theItem->flags & ITEM_RUNIC_IDENTIFIED)
1403 || rogue.playbackOmniscience)) {
1404 itemRunicName(theItem, runicName);
1405 sprintf(buf, "%s of %s%s", root, runicName, grayEscapeSequence);
1406 strcpy(root, buf);
1407 }
1408
1409 if ((theItem->flags & ITEM_IDENTIFIED) || rogue.playbackOmniscience) {
1410 if (theItem->enchant1 == 0) {
1411 sprintf(buf, "%s%s [%i]<%i>", root, grayEscapeSequence, theItem->armor/10, theItem->strengthRequired);
1412 } else {
1413 sprintf(buf, "%s%i %s%s [%i]<%i>",
1414 (theItem->enchant1 < 0 ? "" : "+"),
1415 theItem->enchant1,
1416 root,
1417 grayEscapeSequence,
1418 theItem->armor/10 + theItem->enchant1,
1419 theItem->strengthRequired);
1420 }
1421 strcpy(root, buf);
1422 } else {
1423 sprintf(buf, "%s%s <%i>", root, grayEscapeSequence, theItem->strengthRequired);
1424 strcpy(root, buf);
1425 }
1426
1427 if ((theItem->flags & ITEM_RUNIC)
1428 && (theItem->flags & (ITEM_IDENTIFIED | ITEM_RUNIC_HINTED))
1429 && !(theItem->flags & ITEM_RUNIC_IDENTIFIED)
1430 && !rogue.playbackOmniscience) {
1431 strcat(root, " (unknown runic)");
1432 }
1433 }
1434 break;
1435 case SCROLL:
1436 if (scrollTable[theItem->kind].identified || rogue.playbackOmniscience) {
1437 sprintf(root, "scroll%s of %s", pluralization, scrollTable[theItem->kind].name);
1438 } else if (scrollTable[theItem->kind].called) {
1439 sprintf(root, "scroll%s called %s%s%s",
1440 pluralization,
1441 purpleEscapeSequence,
1442 scrollTable[theItem->kind].callTitle,
1443 baseEscapeSequence);
1444 } else {
1445 sprintf(root, "scroll%s entitled %s\"%s\"%s",
1446 pluralization,
1447 purpleEscapeSequence,
1448 scrollTable[theItem->kind].flavor,
1449 baseEscapeSequence);
1450 }
1451 break;
1452 case POTION:
1453 if (potionTable[theItem->kind].identified || rogue.playbackOmniscience) {
1454 sprintf(root, "potion%s of %s", pluralization, potionTable[theItem->kind].name);
1455 } else if (potionTable[theItem->kind].called) {
1456 sprintf(root, "potion%s called %s%s%s",
1457 pluralization,
1458 purpleEscapeSequence,
1459 potionTable[theItem->kind].callTitle,
1460 baseEscapeSequence);
1461 } else {
1462 sprintf(root, "%s%s%s potion%s",
1463 purpleEscapeSequence,
1464 potionTable[theItem->kind].flavor,
1465 baseEscapeSequence,
1466 pluralization);
1467 }
1468 break;
1469 case WAND:
1470 if (wandTable[theItem->kind].identified || rogue.playbackOmniscience) {
1471 sprintf(root, "wand%s of %s",
1472 pluralization,
1473 wandTable[theItem->kind].name);
1474 } else if (wandTable[theItem->kind].called) {
1475 sprintf(root, "wand%s called %s%s%s",
1476 pluralization,
1477 purpleEscapeSequence,
1478 wandTable[theItem->kind].callTitle,
1479 baseEscapeSequence);
1480 } else {
1481 sprintf(root, "%s%s%s wand%s",
1482 purpleEscapeSequence,
1483 wandTable[theItem->kind].flavor,
1484 baseEscapeSequence,
1485 pluralization);
1486 }
1487 if (includeDetails) {
1488 if (theItem->flags & (ITEM_IDENTIFIED | ITEM_MAX_CHARGES_KNOWN) || rogue.playbackOmniscience) {
1489 sprintf(buf, "%s%s [%i]",
1490 root,
1491 grayEscapeSequence,
1492 theItem->charges);
1493 strcpy(root, buf);
1494 } else if (theItem->enchant2 > 2) {
1495 sprintf(buf, "%s%s (used %i times)",
1496 root,
1497 grayEscapeSequence,
1498 theItem->enchant2);
1499 strcpy(root, buf);
1500 } else if (theItem->enchant2) {
1501 sprintf(buf, "%s%s (used %s)",
1502 root,
1503 grayEscapeSequence,
1504 (theItem->enchant2 == 2 ? "twice" : "once"));
1505 strcpy(root, buf);
1506 }
1507 }
1508 break;
1509 case STAFF:
1510 if (staffTable[theItem->kind].identified || rogue.playbackOmniscience) {
1511 sprintf(root, "staff%s of %s", pluralization, staffTable[theItem->kind].name);
1512 } else if (staffTable[theItem->kind].called) {
1513 sprintf(root, "staff%s called %s%s%s",
1514 pluralization,
1515 purpleEscapeSequence,
1516 staffTable[theItem->kind].callTitle,
1517 baseEscapeSequence);
1518 } else {
1519 sprintf(root, "%s%s%s staff%s",
1520 purpleEscapeSequence,
1521 staffTable[theItem->kind].flavor,
1522 baseEscapeSequence,
1523 pluralization);
1524 }
1525 if (includeDetails) {
1526 if ((theItem->flags & ITEM_IDENTIFIED) || rogue.playbackOmniscience) {
1527 sprintf(buf, "%s%s [%i/%i]", root, grayEscapeSequence, theItem->charges, theItem->enchant1);
1528 strcpy(root, buf);
1529 } else if (theItem->flags & ITEM_MAX_CHARGES_KNOWN) {
1530 sprintf(buf, "%s%s [?/%i]", root, grayEscapeSequence, theItem->enchant1);
1531 strcpy(root, buf);
1532 }
1533 }
1534 break;
1535 case RING:
1536 if (ringTable[theItem->kind].identified || rogue.playbackOmniscience) {
1537 sprintf(root, "ring%s of %s", pluralization, ringTable[theItem->kind].name);
1538 } else if (ringTable[theItem->kind].called) {
1539 sprintf(root, "ring%s called %s%s%s",
1540 pluralization,
1541 purpleEscapeSequence,
1542 ringTable[theItem->kind].callTitle,
1543 baseEscapeSequence);
1544 } else {
1545 sprintf(root, "%s%s%s ring%s",
1546 purpleEscapeSequence,
1547 ringTable[theItem->kind].flavor,
1548 baseEscapeSequence,
1549 pluralization);
1550 }
1551 if (includeDetails && ((theItem->flags & ITEM_IDENTIFIED) || rogue.playbackOmniscience)) {
1552 sprintf(buf, "%s%i %s", (theItem->enchant1 < 0 ? "" : "+"), theItem->enchant1, root);
1553 strcpy(root, buf);
1554 }
1555 break;
1556 case CHARM:
1557 sprintf(root, "%s charm%s", charmTable[theItem->kind].name, pluralization);
1558
1559 if (includeDetails) {
1560 sprintf(buf, "%s%i %s", (theItem->enchant1 < 0 ? "" : "+"), theItem->enchant1, root);
1561 strcpy(root, buf);
1562
1563 if (theItem->charges) {
1564 sprintf(buf, "%s %s(%i%%)",
1565 root,
1566 grayEscapeSequence,
1567 (charmRechargeDelay(theItem->kind, theItem->enchant1) - theItem->charges) * 100 / charmRechargeDelay(theItem->kind, theItem->enchant1));
1568 strcpy(root, buf);
1569 } else {
1570 strcat(root, grayEscapeSequence);
1571 strcat(root, " (ready)");
1572 }
1573 }
1574 break;
1575 case GOLD:
1576 sprintf(root, "gold piece%s", pluralization);
1577 break;
1578 case AMULET:
1579 sprintf(root, "%sAmulet%s of Yendor%s", yellowEscapeSequence, pluralization, baseEscapeSequence);
1580 break;
1581 case GEM:
1582 sprintf(root, "%slumenstone%s%s from depth %i", yellowEscapeSequence, pluralization, baseEscapeSequence, theItem->originDepth);
1583 break;
1584 case KEY:
1585 if (includeDetails && theItem->originDepth > 0 && theItem->originDepth != rogue.depthLevel) {
1586 sprintf(root, "%s%s%s from depth %i",
1587 keyTable[theItem->kind].name,
1588 pluralization,
1589 grayEscapeSequence,
1590 theItem->originDepth);
1591 } else {
1592 sprintf(root,
1593 keyTable[theItem->kind].name,
1594 "%s%s",
1595 pluralization);
1596 }
1597 break;
1598 default:
1599 sprintf(root, "unknown item%s", pluralization);
1600 break;
1601 }
1602
1603 if (includeArticle) {
1604 // prepend number if quantity is over 1
1605 if (theItem->quantity > 1) {
1606 sprintf(article, "%i ", theItem->quantity);
1607 } else if (theItem->category & AMULET) {
1608 sprintf(article, "the ");
1609 } else if (!(theItem->category & ARMOR) && !(theItem->category & FOOD && theItem->kind == RATION)) {
1610 // otherwise prepend a/an if the item is not armor and not a ration of food;
1611 // armor gets no article, and "some food" was taken care of above.
1612 sprintf(article, "a%s ", (isVowelish(root) ? "n" : ""));
1613 }
1614 }
1615 // strcat(buf, suffixID);
1616 if (includeArticle) {
1617 sprintf(buf, "%s%s", article, root);
1618 strcpy(root, buf);
1619 }
1620
1621 if (includeDetails && theItem->inscription[0]) {
1622 sprintf(buf, "%s \"%s\"", root, theItem->inscription);
1623 strcpy(root, buf);
1624 }
1625 return;
1626 }
1627
itemKindName(item * theItem,char * kindName)1628 void itemKindName(item *theItem, char *kindName) {
1629
1630 // use lookup table for randomly generated items with more than one kind per category
1631 if (theItem->category & (ARMOR | CHARM | FOOD | POTION | RING | SCROLL | STAFF | WAND | WEAPON)) {
1632 strcpy(kindName, tableForItemCategory(theItem->category, NULL)[theItem->kind].name);
1633 } else {
1634 switch (theItem->category) {
1635 case KEY:
1636 strcpy(kindName, keyTable[theItem->kind].name); //keys are not randomly generated but have a lookup table
1637 break;
1638 // these items have only one kind per category and no lookup table
1639 case GOLD:
1640 strcpy(kindName, "gold pieces");
1641 break;
1642 case AMULET:
1643 strcpy(kindName, "amulet of yendor");
1644 break;
1645 case GEM:
1646 strcpy(kindName, "lumenstone");
1647 break;
1648 default:
1649 strcpy(kindName, "unknown");
1650 break;
1651 }
1652 }
1653 }
1654
itemRunicName(item * theItem,char * runicName)1655 void itemRunicName(item *theItem, char *runicName) {
1656 char vorpalEnemyMonsterClass[15] ="";
1657
1658 if (theItem->flags & ITEM_RUNIC) {
1659 if ((theItem->category == ARMOR && theItem->enchant2 == A_IMMUNITY)
1660 || (theItem->category == WEAPON && theItem->enchant2 == W_SLAYING)) {
1661 sprintf(vorpalEnemyMonsterClass, "%s ", monsterClassCatalog[theItem->vorpalEnemy].name);
1662 }
1663 if (theItem->category == WEAPON) {
1664 sprintf(runicName, "%s%s", vorpalEnemyMonsterClass, weaponRunicNames[theItem->enchant2]);
1665 } else if (theItem->category == ARMOR) {
1666 sprintf(runicName, "%s%s", vorpalEnemyMonsterClass, armorRunicNames[theItem->enchant2]);
1667 }
1668 }
1669 }
1670
1671 // kindCount is optional
tableForItemCategory(enum itemCategory theCat,short * kindCount)1672 itemTable *tableForItemCategory(enum itemCategory theCat, short *kindCount) {
1673 itemTable *returnedTable;
1674 short returnedCount;
1675 switch (theCat) {
1676 case FOOD:
1677 returnedTable = foodTable;
1678 returnedCount = NUMBER_FOOD_KINDS;
1679 break;
1680 case WEAPON:
1681 returnedTable = weaponTable;
1682 returnedCount = NUMBER_WEAPON_KINDS;
1683 break;
1684 case ARMOR:
1685 returnedTable = armorTable;
1686 returnedCount = NUMBER_ARMOR_KINDS;
1687 break;
1688 case POTION:
1689 returnedTable = potionTable;
1690 returnedCount = NUMBER_POTION_KINDS;
1691 break;
1692 case SCROLL:
1693 returnedTable = scrollTable;
1694 returnedCount = NUMBER_SCROLL_KINDS;
1695 break;
1696 case RING:
1697 returnedTable = ringTable;
1698 returnedCount = NUMBER_RING_KINDS;
1699 break;
1700 case WAND:
1701 returnedTable = wandTable;
1702 returnedCount = NUMBER_WAND_KINDS;
1703 break;
1704 case STAFF:
1705 returnedTable = staffTable;
1706 returnedCount = NUMBER_STAFF_KINDS;
1707 break;
1708 case CHARM:
1709 returnedTable = charmTable;
1710 returnedCount = NUMBER_CHARM_KINDS;
1711 break;
1712 default:
1713 returnedTable = NULL;
1714 returnedCount = 0;
1715 break;
1716 }
1717 if (kindCount) {
1718 *kindCount = returnedCount;
1719 }
1720 return returnedTable;
1721 }
1722
isVowelish(char * theChar)1723 boolean isVowelish(char *theChar) {
1724 short i;
1725
1726 while (*theChar == COLOR_ESCAPE) {
1727 theChar += 4;
1728 }
1729 char str[30];
1730 strncpy(str, theChar, 30);
1731 str[29] = '\0';
1732 for (i = 0; i < 29; i++) {
1733 upperCase(&(str[i]));
1734 }
1735 if (stringsMatch(str, "UNI") // Words that start with "uni" aren't treated like vowels; e.g., "a" unicorn.
1736 || stringsMatch(str, "EU")) { // Words that start with "eu" aren't treated like vowels; e.g., "a" eucalpytus staff.
1737
1738 return false;
1739 } else {
1740 return (str[0] == 'A'
1741 || str[0] == 'E'
1742 || str[0] == 'I'
1743 || str[0] == 'O'
1744 || str[0] == 'U');
1745 }
1746 }
1747
enchantIncrement(item * theItem)1748 fixpt enchantIncrement(item *theItem) {
1749 if (theItem->category & (WEAPON | ARMOR)) {
1750 if (theItem->strengthRequired == 0) {
1751 return FP_FACTOR;
1752 } else if (rogue.strength - player.weaknessAmount < theItem->strengthRequired) {
1753 return FP_FACTOR * 35 / 10;
1754 } else {
1755 return FP_FACTOR * 125 / 100;
1756 }
1757 } else {
1758 return FP_FACTOR;
1759 }
1760 }
1761
itemIsCarried(item * theItem)1762 boolean itemIsCarried(item *theItem) {
1763 item *tempItem;
1764
1765 for (tempItem = packItems->nextItem; tempItem != NULL; tempItem = tempItem->nextItem) {
1766 if (tempItem == theItem) {
1767 return true;
1768 }
1769 }
1770 return false;
1771 }
1772
effectiveRingEnchant(item * theItem)1773 short effectiveRingEnchant(item *theItem) {
1774 if (theItem->category != RING) {
1775 return 0;
1776 }
1777 if (!(theItem->flags & ITEM_IDENTIFIED)
1778 && theItem->enchant1 > 0) {
1779
1780 return theItem->timesEnchanted + 1; // Unidentified positive rings act as +1 until identified.
1781 }
1782 return theItem->enchant1;
1783 }
1784
apparentRingBonus(const enum ringKind kind)1785 short apparentRingBonus(const enum ringKind kind) {
1786 item *rings[2] = {rogue.ringLeft, rogue.ringRight}, *ring;
1787 short retval = 0;
1788 short i;
1789
1790 if (ringTable[kind].identified) {
1791 for (i = 0; i < 2; i++) {
1792 ring = rings[i];
1793 if (ring && ring->kind == kind) {
1794 retval += effectiveRingEnchant(ring);
1795 }
1796 }
1797 }
1798 return retval;
1799 }
1800
itemDetails(char * buf,item * theItem)1801 void itemDetails(char *buf, item *theItem) {
1802 char buf2[1000], buf3[1000], theName[500], goodColorEscape[20], badColorEscape[20], whiteColorEscape[20];
1803 boolean singular, carried;
1804 fixpt enchant;
1805 fixpt currentDamage, newDamage;
1806 short nextLevelState = 0, new, current, accuracyChange, damageChange;
1807 unsigned long turnsSinceLatestUse;
1808 const char weaponRunicEffectDescriptions[NUMBER_WEAPON_RUNIC_KINDS][DCOLS] = {
1809 "time will stop while you take an extra turn",
1810 "the enemy will die instantly",
1811 "the enemy will be paralyzed",
1812 "[multiplicity]", // never used
1813 "the enemy will be slowed",
1814 "the enemy will be confused",
1815 "the enemy will be flung",
1816 "[slaying]", // never used
1817 "the enemy will be healed",
1818 "the enemy will be cloned"
1819 };
1820
1821 goodColorEscape[0] = badColorEscape[0] = whiteColorEscape[0] = '\0';
1822 encodeMessageColor(goodColorEscape, 0, &goodMessageColor);
1823 encodeMessageColor(badColorEscape, 0, &badMessageColor);
1824 encodeMessageColor(whiteColorEscape, 0, &white);
1825
1826 singular = (theItem->quantity == 1 ? true : false);
1827 carried = itemIsCarried(theItem);
1828
1829 // Name
1830 itemName(theItem, theName, true, true, NULL);
1831 buf[0] = '\0';
1832 encodeMessageColor(buf, 0, &itemMessageColor);
1833 upperCase(theName);
1834 strcat(buf, theName);
1835 if (carried) {
1836 sprintf(buf2, " (%c)", theItem->inventoryLetter);
1837 strcat(buf, buf2);
1838 }
1839 encodeMessageColor(buf, strlen(buf), &white);
1840 strcat(buf, "\n\n");
1841
1842 enchant = netEnchant(theItem);
1843
1844 itemName(theItem, theName, false, false, NULL);
1845
1846 // introductory text
1847 if (tableForItemCategory(theItem->category, NULL)
1848 && (tableForItemCategory(theItem->category, NULL)[theItem->kind].identified || rogue.playbackOmniscience)) {
1849
1850 strcat(buf, tableForItemCategory(theItem->category, NULL)[theItem->kind].description);
1851
1852 if (theItem->category == POTION && theItem->kind == POTION_LIFE) {
1853 sprintf(buf2, "\n\nIt will increase your maximum health by %s%i%%%s.",
1854 goodColorEscape,
1855 (player.info.maxHP + 10) * 100 / player.info.maxHP - 100,
1856 whiteColorEscape);
1857 strcat(buf, buf2);
1858 }
1859 } else {
1860 switch (theItem->category) {
1861 case POTION:
1862 sprintf(buf2, "%s flask%s contain%s a swirling %s liquid. Who knows what %s will do when drunk or thrown?",
1863 (singular ? "This" : "These"),
1864 (singular ? "" : "s"),
1865 (singular ? "s" : ""),
1866 tableForItemCategory(theItem->category, NULL)[theItem->kind].flavor,
1867 (singular ? "it" : "they"));
1868 break;
1869 case SCROLL:
1870 sprintf(buf2, "%s parchment%s %s covered with indecipherable writing, and bear%s a title of \"%s.\" Who knows what %s will do when read aloud?",
1871 (singular ? "This" : "These"),
1872 (singular ? "" : "s"),
1873 (singular ? "is" : "are"),
1874 (singular ? "s" : ""),
1875 tableForItemCategory(theItem->category, NULL)[theItem->kind].flavor,
1876 (singular ? "it" : "they"));
1877 break;
1878 case STAFF:
1879 sprintf(buf2, "This gnarled %s staff is warm to the touch. Who knows what it will do when used?",
1880 tableForItemCategory(theItem->category, NULL)[theItem->kind].flavor);
1881 break;
1882 case WAND:
1883 sprintf(buf2, "This thin %s wand is warm to the touch. Who knows what it will do when used?",
1884 tableForItemCategory(theItem->category, NULL)[theItem->kind].flavor);
1885 break;
1886 case RING:
1887 sprintf(buf2, "This metal band is adorned with a%s %s gem that glitters in the darkness. Who knows what effect it has when worn? ",
1888 isVowelish(tableForItemCategory(theItem->category, NULL)[theItem->kind].flavor) ? "n" : "",
1889 tableForItemCategory(theItem->category, NULL)[theItem->kind].flavor);
1890 break;
1891 case CHARM: // Should never be displayed.
1892 strcat(buf2, "What a perplexing charm!");
1893 break;
1894 case AMULET:
1895 strcpy(buf2, "Legends are told about this mysterious golden amulet, and legions of adventurers have perished in its pursuit. Unfathomable riches await anyone with the skill and ambition to carry it into the light of day.");
1896 break;
1897 case GEM:
1898 sprintf(buf2, "Faint golden lights swirl and fluoresce beneath the stone%s surface. Lumenstones are said to contain mysterious properties of untold power, but for you, they mean one thing: riches.",
1899 (singular ? "'s" : "s'"));
1900 break;
1901 case KEY:
1902 strcpy(buf2, keyTable[theItem->kind].description);
1903 break;
1904 case GOLD:
1905 sprintf(buf2, "A pile of %i shining gold coins.", theItem->quantity);
1906 break;
1907 default:
1908 break;
1909 }
1910 strcat(buf, buf2);
1911 }
1912
1913 if (carried && theItem->originDepth > 0) {
1914 sprintf(buf2, " (You found %s on depth %i.) ",
1915 singular ? "it" : "them",
1916 theItem->originDepth);
1917 strcat(buf, buf2);
1918 }
1919
1920 // detailed description
1921 switch (theItem->category) {
1922
1923 case FOOD:
1924 sprintf(buf2, "\n\nYou are %shungry enough to fully enjoy a %s.",
1925 ((STOMACH_SIZE - player.status[STATUS_NUTRITION]) >= foodTable[theItem->kind].strengthRequired ? "" : "not yet "),
1926 foodTable[theItem->kind].name);
1927 strcat(buf, buf2);
1928 break;
1929
1930 case WEAPON:
1931 case ARMOR:
1932 // enchanted? strength modifier?
1933 if ((theItem->flags & ITEM_IDENTIFIED) || rogue.playbackOmniscience) {
1934 if (theItem->enchant1) {
1935 if (theItem->enchant1 > 0) {
1936 sprintf(buf2, "\n\nThe %s bear%s an intrinsic enchantment of %s+%i%s",
1937 theName,
1938 (singular ? "s" : ""),
1939 goodColorEscape,
1940 theItem->enchant1,
1941 whiteColorEscape);
1942 } else {
1943 sprintf(buf2, "\n\nThe %s bear%s an intrinsic penalty of %s%i%s",
1944 theName,
1945 (singular ? "s" : ""),
1946 badColorEscape,
1947 theItem->enchant1,
1948 whiteColorEscape);
1949 }
1950 } else {
1951 sprintf(buf2, "\n\nThe %s bear%s no intrinsic enchantment",
1952 theName,
1953 (singular ? "s" : ""));
1954 }
1955 strcat(buf, buf2);
1956 if (strengthModifier(theItem)) {
1957 sprintf(buf2, ", %s %s %s %s%s%+.2f%s because of your %s strength. ",
1958 (theItem->enchant1 ? "and" : "but"),
1959 (singular ? "carries" : "carry"),
1960 (theItem->enchant1 && (theItem->enchant1 > 0) == (strengthModifier(theItem) > 0) ? "an additional" : "a"),
1961 (strengthModifier(theItem) > 0 ? "bonus of " : "penalty of "),
1962 (strengthModifier(theItem) > 0 ? goodColorEscape : badColorEscape),
1963 strengthModifier(theItem) / (double) FP_FACTOR,
1964 whiteColorEscape,
1965 (strengthModifier(theItem) > 0 ? "excess" : "inadequate"));
1966 strcat(buf, buf2);
1967 } else {
1968 strcat(buf, ". ");
1969 }
1970 } else {
1971 if ((theItem->enchant1 > 0) && (theItem->flags & ITEM_MAGIC_DETECTED)) {
1972 sprintf(buf2, "\n\nYou can feel an %saura of benevolent magic%s radiating from the %s. ",
1973 goodColorEscape,
1974 whiteColorEscape,
1975 theName);
1976 strcat(buf, buf2);
1977 }
1978 if (strengthModifier(theItem)) {
1979 sprintf(buf2, "\n\nThe %s %s%s a %s%s%+.2f%s because of your %s strength. ",
1980 theName,
1981 ((theItem->enchant1 > 0) && (theItem->flags & ITEM_MAGIC_DETECTED) ? "also " : ""),
1982 (singular ? "carries" : "carry"),
1983 (strengthModifier(theItem) > 0 ? "bonus of " : "penalty of "),
1984 (strengthModifier(theItem) > 0 ? goodColorEscape : badColorEscape),
1985 strengthModifier(theItem) / (double) FP_FACTOR,
1986 whiteColorEscape,
1987 (strengthModifier(theItem) > 0 ? "excess" : "inadequate"));
1988 strcat(buf, buf2);
1989 }
1990
1991 if (theItem->category & WEAPON) {
1992 sprintf(buf2, "It will reveal its secrets if you defeat %i%s %s with it. ",
1993 theItem->charges,
1994 (theItem->charges == WEAPON_KILLS_TO_AUTO_ID ? "" : " more"),
1995 (theItem->charges == 1 ? "enemy" : "enemies"));
1996 } else {
1997 sprintf(buf2, "It will reveal its secrets if worn for %i%s turn%s. ",
1998 theItem->charges,
1999 (theItem->charges == ARMOR_DELAY_TO_AUTO_ID ? "" : " more"),
2000 (theItem->charges == 1 ? "" : "s"));
2001 }
2002 strcat(buf, buf2);
2003 }
2004
2005 // Display the known percentage by which the armor/weapon will increase/decrease accuracy/damage/defense if not already equipped.
2006 if (!(theItem->flags & ITEM_EQUIPPED)) {
2007 if (theItem->category & WEAPON) {
2008 current = player.info.accuracy;
2009 if (rogue.weapon) {
2010 currentDamage = (rogue.weapon->damage.lowerBound + rogue.weapon->damage.upperBound) * FP_FACTOR / 2;
2011 if ((rogue.weapon->flags & ITEM_IDENTIFIED) || rogue.playbackOmniscience) {
2012 current = current * accuracyFraction(netEnchant(rogue.weapon)) / FP_FACTOR;
2013 currentDamage = currentDamage * damageFraction(netEnchant(rogue.weapon)) / FP_FACTOR;
2014 } else {
2015 current = current * accuracyFraction(strengthModifier(rogue.weapon)) / FP_FACTOR;
2016 currentDamage = currentDamage * damageFraction(strengthModifier(rogue.weapon)) / FP_FACTOR;
2017 }
2018 } else {
2019 currentDamage = (player.info.damage.lowerBound + player.info.damage.upperBound) * FP_FACTOR / 2;
2020 }
2021
2022 new = player.info.accuracy;
2023 newDamage = (theItem->damage.lowerBound + theItem->damage.upperBound) * FP_FACTOR / 2;
2024 if ((theItem->flags & ITEM_IDENTIFIED) || rogue.playbackOmniscience) {
2025 new = new * accuracyFraction(netEnchant(theItem)) / FP_FACTOR;
2026 newDamage = newDamage * damageFraction(netEnchant(theItem)) / FP_FACTOR;
2027 } else {
2028 new = new * accuracyFraction(strengthModifier(theItem)) / FP_FACTOR;
2029 newDamage = newDamage * damageFraction(strengthModifier(theItem)) / FP_FACTOR;
2030 }
2031 accuracyChange = (new * 100 / current) - 100;
2032 damageChange = (newDamage * 100 / currentDamage) - 100;
2033 sprintf(buf2, "Wielding the %s%s will %s your current accuracy by %s%i%%%s, and will %s your current damage by %s%i%%%s. ",
2034 theName,
2035 ((theItem->flags & ITEM_IDENTIFIED) || rogue.playbackOmniscience) ? "" : ", assuming it has no hidden properties,",
2036 (((short) accuracyChange) < 0) ? "decrease" : "increase",
2037 (((short) accuracyChange) < 0) ? badColorEscape : (accuracyChange > 0 ? goodColorEscape : ""),
2038 abs((short) accuracyChange),
2039 whiteColorEscape,
2040 (((short) damageChange) < 0) ? "decrease" : "increase",
2041 (((short) damageChange) < 0) ? badColorEscape : (damageChange > 0 ? goodColorEscape : ""),
2042 abs((short) damageChange),
2043 whiteColorEscape);
2044 } else {
2045 new = theItem->armor;
2046 if ((theItem->flags & ITEM_IDENTIFIED) || rogue.playbackOmniscience) {
2047 new += 10 * netEnchant(theItem) / FP_FACTOR;
2048 } else {
2049 new += 10 * strengthModifier(theItem) / FP_FACTOR;
2050 }
2051 new = max(0, new);
2052 new /= 10;
2053 sprintf(buf2, "Wearing the %s%s will result in an armor rating of %s%i%s. ",
2054 theName,
2055 ((theItem->flags & ITEM_IDENTIFIED) || rogue.playbackOmniscience) ? "" : ", assuming it has no hidden properties,",
2056 (new > displayedArmorValue() ? goodColorEscape : (new < displayedArmorValue() ? badColorEscape : whiteColorEscape)),
2057 new, whiteColorEscape);
2058 }
2059 strcat(buf, buf2);
2060 }
2061
2062 // protected?
2063 if (theItem->flags & ITEM_PROTECTED) {
2064 sprintf(buf2, "%sThe %s cannot be corroded by acid.%s ",
2065 goodColorEscape,
2066 theName,
2067 whiteColorEscape);
2068 strcat(buf, buf2);
2069 }
2070
2071 // heavy armor?
2072 current = armorAggroAdjustment(rogue.armor);
2073 if ((theItem->category & ARMOR)
2074 && !(theItem->flags & ITEM_EQUIPPED)
2075 && (current != armorAggroAdjustment(theItem))) {
2076
2077 new = armorAggroAdjustment(theItem);
2078 if (rogue.armor) {
2079 new -= armorAggroAdjustment(rogue.armor);
2080 }
2081 sprintf(buf2, "Equipping the %s will %s%s your stealth range by %i%s. ",
2082 theName,
2083 new > 0 ? badColorEscape : goodColorEscape,
2084 new > 0 ? "increase" : "decrease",
2085 abs(new),
2086 whiteColorEscape);
2087 strcat(buf, buf2);
2088 }
2089
2090 if (theItem->category & WEAPON) {
2091
2092 // runic?
2093 if (theItem->flags & ITEM_RUNIC) {
2094 if ((theItem->flags & ITEM_RUNIC_IDENTIFIED) || rogue.playbackOmniscience) {
2095 sprintf(buf2, "\n\nGlowing runes of %s adorn the %s. ",
2096 weaponRunicNames[theItem->enchant2],
2097 theName);
2098 strcat(buf, buf2);
2099 if (theItem->enchant2 == W_SLAYING) {
2100 describeMonsterClass(buf3, theItem->vorpalEnemy, false);
2101 sprintf(buf2, "It will never fail to slay a%s %s in a single stroke. ",
2102 (isVowelish(buf3) ? "n" : ""),
2103 buf3);
2104 strcat(buf, buf2);
2105 } else if (theItem->enchant2 == W_MULTIPLICITY) {
2106 if ((theItem->flags & ITEM_IDENTIFIED) || rogue.playbackOmniscience) {
2107 sprintf(buf2, "%i%% of the time that it hits an enemy, %i spectral %s%s will spring into being with accuracy and attack power equal to your own, and will dissipate %i turns later. (If the %s is enchanted, %i image%s will appear %i%% of the time, and will last %i turns.)",
2108 runicWeaponChance(theItem, false, 0),
2109 weaponImageCount(enchant),
2110 theName,
2111 (weaponImageCount(enchant) > 1 ? "s" : ""),
2112 weaponImageDuration(enchant),
2113 theName,
2114 weaponImageCount(enchant + enchantIncrement(theItem)),
2115 (weaponImageCount(enchant + enchantIncrement(theItem)) > 1 ? "s" : ""),
2116 runicWeaponChance(theItem, true, enchant + enchantIncrement(theItem)),
2117 weaponImageDuration(enchant + enchantIncrement(theItem)));
2118 } else {
2119 sprintf(buf2, "Sometimes, when it hits an enemy, spectral %ss will spring into being with accuracy and attack power equal to your own, and will dissipate shortly thereafter.",
2120 theName);
2121 }
2122 strcat(buf, buf2);
2123 } else {
2124 if ((theItem->flags & ITEM_IDENTIFIED) || rogue.playbackOmniscience) {
2125 if (runicWeaponChance(theItem, false, 0) < 2
2126 && rogue.strength - player.weaknessAmount < theItem->strengthRequired) {
2127
2128 strcpy(buf2, "Its runic effect will almost never activate because of your inadequate strength, but sometimes, when");
2129 } else {
2130 sprintf(buf2, "%i%% of the time that",
2131 runicWeaponChance(theItem, false, 0));
2132 }
2133 strcat(buf, buf2);
2134 } else {
2135 strcat(buf, "Sometimes, when");
2136 }
2137 sprintf(buf2, " it hits an enemy, %s",
2138 weaponRunicEffectDescriptions[theItem->enchant2]);
2139 strcat(buf, buf2);
2140
2141 if ((theItem->flags & ITEM_IDENTIFIED) || rogue.playbackOmniscience) {
2142 switch (theItem->enchant2) {
2143 case W_SPEED:
2144 strcat(buf, ". ");
2145 break;
2146 case W_PARALYSIS:
2147 sprintf(buf2, " for %i turns. ",
2148 (int) (weaponParalysisDuration(enchant)));
2149 strcat(buf, buf2);
2150 nextLevelState = (int) (weaponParalysisDuration(enchant + enchantIncrement(theItem)));
2151 break;
2152 case W_SLOWING:
2153 sprintf(buf2, " for %i turns. ",
2154 weaponSlowDuration(enchant));
2155 strcat(buf, buf2);
2156 nextLevelState = weaponSlowDuration(enchant + enchantIncrement(theItem));
2157 break;
2158 case W_CONFUSION:
2159 sprintf(buf2, " for %i turns. ",
2160 weaponConfusionDuration(enchant));
2161 strcat(buf, buf2);
2162 nextLevelState = weaponConfusionDuration(enchant + enchantIncrement(theItem));
2163 break;
2164 case W_FORCE:
2165 sprintf(buf2, " up to %i spaces backward. If the enemy hits an obstruction, it (and any monster it hits) will take damage in proportion to the distance it flew. ",
2166 weaponForceDistance(enchant));
2167 strcat(buf, buf2);
2168 nextLevelState = weaponForceDistance(enchant + enchantIncrement(theItem));
2169 break;
2170 case W_MERCY:
2171 strcpy(buf2, " by 50% of its maximum health. ");
2172 strcat(buf, buf2);
2173 break;
2174 default:
2175 strcpy(buf2, ". ");
2176 strcat(buf, buf2);
2177 break;
2178 }
2179
2180 if (((theItem->flags & ITEM_IDENTIFIED) || rogue.playbackOmniscience)
2181 && runicWeaponChance(theItem, false, 0) < runicWeaponChance(theItem, true, enchant + enchantIncrement(theItem))){
2182 sprintf(buf2, "(If the %s is enchanted, the chance will increase to %i%%",
2183 theName,
2184 runicWeaponChance(theItem, true, enchant + enchantIncrement(theItem)));
2185 strcat(buf, buf2);
2186 if (nextLevelState) {
2187 if (theItem->enchant2 == W_FORCE) {
2188 sprintf(buf2, " and the distance will increase to %i.)",
2189 nextLevelState);
2190 } else {
2191 sprintf(buf2, " and the duration will increase to %i turns.)",
2192 nextLevelState);
2193 }
2194 } else {
2195 strcpy(buf2, ".)");
2196 }
2197 strcat(buf, buf2);
2198 }
2199 } else {
2200 strcat(buf, ". ");
2201 }
2202 }
2203
2204 } else if (theItem->flags & ITEM_IDENTIFIED) {
2205 sprintf(buf2, "\n\nGlowing runes of an indecipherable language run down the length of the %s. ",
2206 theName);
2207 strcat(buf, buf2);
2208 }
2209 }
2210
2211 // equipped? cursed?
2212 if (theItem->flags & ITEM_EQUIPPED) {
2213 sprintf(buf2, "\n\nYou hold the %s at the ready%s. ",
2214 theName,
2215 ((theItem->flags & ITEM_CURSED) ? ", and because it is cursed, you are powerless to let go" : ""));
2216 strcat(buf, buf2);
2217 } else if (((theItem->flags & (ITEM_IDENTIFIED | ITEM_MAGIC_DETECTED)) || rogue.playbackOmniscience)
2218 && (theItem->flags & ITEM_CURSED)) {
2219 sprintf(buf2, "\n\n%sYou can feel a malevolent magic lurking within the %s.%s ",
2220 badColorEscape,
2221 theName,
2222 whiteColorEscape);
2223 strcat(buf, buf2);
2224 }
2225
2226 } else if (theItem->category & ARMOR) {
2227
2228 // runic?
2229 if (theItem->flags & ITEM_RUNIC) {
2230 if ((theItem->flags & ITEM_RUNIC_IDENTIFIED) || rogue.playbackOmniscience) {
2231 sprintf(buf2, "\n\nGlowing runes of %s adorn the %s. ",
2232 armorRunicNames[theItem->enchant2],
2233 theName);
2234 strcat(buf, buf2);
2235
2236 // A_MULTIPLICITY, A_MUTUALITY, A_ABSORPTION, A_REPRISAL, A_IMMUNITY, A_REFLECTION, A_BURDEN, A_VULNERABILITY, A_IMMOLATION
2237 switch (theItem->enchant2) {
2238 case A_MULTIPLICITY:
2239 sprintf(buf2, "When worn, 33%% of the time that an enemy's attack connects, %i allied spectral duplicate%s of your attacker will appear for 3 turns. ",
2240 armorImageCount(enchant),
2241 (armorImageCount(enchant) == 1 ? "" : "s"));
2242 if (armorImageCount(enchant + enchantIncrement(theItem)) > armorImageCount(enchant)) {
2243 sprintf(buf3, "(If the %s is enchanted, the number of duplicates will increase to %i.) ",
2244 theName,
2245 (armorImageCount(enchant + enchantIncrement(theItem))));
2246 strcat(buf2, buf3);
2247 }
2248 break;
2249 case A_MUTUALITY:
2250 strcpy(buf2, "When worn, the damage that you incur from physical attacks will be split evenly among yourself and all other adjacent enemies. ");
2251 break;
2252 case A_ABSORPTION:
2253 if (theItem->flags & ITEM_IDENTIFIED) {
2254 sprintf(buf2, "It will reduce the damage of inbound attacks by a random amount between 0 and %i, which is %i%% of your current maximum health. (If the %s is enchanted, this maximum amount will %s %i.) ",
2255 (int) armorAbsorptionMax(enchant),
2256 (int) (100 * armorAbsorptionMax(enchant) / player.info.maxHP),
2257 theName,
2258 (armorAbsorptionMax(enchant) == armorAbsorptionMax(enchant + enchantIncrement(theItem)) ? "remain at" : "increase to"),
2259 (int) armorAbsorptionMax(enchant + enchantIncrement(theItem)));
2260 } else {
2261 strcpy(buf2, "It will reduce the damage of inbound attacks by a random amount determined by its enchantment level. ");
2262 }
2263 break;
2264 case A_REPRISAL:
2265 if (theItem->flags & ITEM_IDENTIFIED) {
2266 sprintf(buf2, "Any enemy that attacks you will itself be wounded by %i%% of the damage that it inflicts. (If the %s is enchanted, this percentage will increase to %i%%.) ",
2267 armorReprisalPercent(enchant),
2268 theName,
2269 armorReprisalPercent(enchant + enchantIncrement(theItem)));
2270 } else {
2271 strcpy(buf2, "Any enemy that attacks you will itself be wounded by a percentage (determined by enchantment level) of the damage that it inflicts. ");
2272 }
2273 break;
2274 case A_IMMUNITY:
2275 describeMonsterClass(buf3, theItem->vorpalEnemy, false);
2276 sprintf(buf2, "It offers complete protection from any attacking %s. ",
2277 buf3);
2278 break;
2279 case A_REFLECTION:
2280 if (theItem->flags & ITEM_IDENTIFIED) {
2281 if (theItem->enchant1 > 0) {
2282 short reflectChance = reflectionChance(enchant);
2283 short reflectChance2 = reflectionChance(enchant + enchantIncrement(theItem));
2284 sprintf(buf2, "When worn, you will deflect %i%% of incoming spells -- including directly back at their source %i%% of the time. (If the armor is enchanted, these will increase to %i%% and %i%%.) ",
2285 reflectChance,
2286 reflectChance * reflectChance / 100,
2287 reflectChance2,
2288 reflectChance2 * reflectChance2 / 100);
2289 } else if (theItem->enchant1 < 0) {
2290 short reflectChance = reflectionChance(enchant);
2291 short reflectChance2 = reflectionChance(enchant + enchantIncrement(theItem));
2292 sprintf(buf2, "When worn, %i%% of your own spells will deflect from their target -- including directly back at you %i%% of the time. (If the armor is enchanted, these will decrease to %i%% and %i%%.) ",
2293 reflectChance,
2294 reflectChance * reflectChance / 100,
2295 reflectChance2,
2296 reflectChance2 * reflectChance2 / 100);
2297 }
2298 } else {
2299 strcpy(buf2, "When worn, you will deflect some percentage of incoming spells, determined by enchantment level. ");
2300 }
2301 break;
2302 case A_RESPIRATION:
2303 strcpy(buf2, "When worn, it will maintain a pocket of fresh air around you, rendering you immune to the effects of steam and all toxic gases. ");
2304 break;
2305 case A_DAMPENING:
2306 strcpy(buf2, "When worn, it will safely absorb the concussive impact of any explosions (though you may still be burned). ");
2307 break;
2308 case A_BURDEN:
2309 strcpy(buf2, "10% of the time it absorbs a blow, its strength requirement will permanently increase. ");
2310 break;
2311 case A_VULNERABILITY:
2312 strcpy(buf2, "While it is worn, inbound attacks will inflict twice as much damage. ");
2313 break;
2314 case A_IMMOLATION:
2315 strcpy(buf2, "10% of the time it absorbs a blow, it will explode in flames. ");
2316 break;
2317 default:
2318 break;
2319 }
2320 strcat(buf, buf2);
2321 } else if (theItem->flags & ITEM_IDENTIFIED) {
2322 sprintf(buf2, "\n\nGlowing runes of an indecipherable language spiral around the %s. ",
2323 theName);
2324 strcat(buf, buf2);
2325 }
2326 }
2327
2328 // equipped? cursed?
2329 if (theItem->flags & ITEM_EQUIPPED) {
2330 sprintf(buf2, "\n\nYou are wearing the %s%s. ",
2331 theName,
2332 ((theItem->flags & ITEM_CURSED) ? ", and because it is cursed, you are powerless to remove it" : ""));
2333 strcat(buf, buf2);
2334 } else if (((theItem->flags & (ITEM_IDENTIFIED | ITEM_MAGIC_DETECTED)) || rogue.playbackOmniscience)
2335 && (theItem->flags & ITEM_CURSED)) {
2336 sprintf(buf2, "\n\n%sYou can feel a malevolent magic lurking within the %s.%s ",
2337 badColorEscape,
2338 theName,
2339 whiteColorEscape);
2340 strcat(buf, buf2);
2341 }
2342
2343 }
2344 break;
2345
2346 case STAFF:
2347
2348 // charges
2349 new = apparentRingBonus(RING_WISDOM);
2350 if ((theItem->flags & ITEM_IDENTIFIED) || rogue.playbackOmniscience) {
2351 sprintf(buf2, "\n\nThe %s has %i charges remaining out of a maximum of %i charges, and%s recovers a charge in approximately %lli turns. ",
2352 theName,
2353 theItem->charges,
2354 theItem->enchant1,
2355 new == 0 ? "" : ", with your current rings,",
2356 FP_DIV(staffChargeDuration(theItem), 10 * ringWisdomMultiplier(new * FP_FACTOR)));
2357 strcat(buf, buf2);
2358 } else if (theItem->flags & ITEM_MAX_CHARGES_KNOWN) {
2359 sprintf(buf2, "\n\nThe %s has a maximum of %i charges, and%s recovers a charge in approximately %lli turns. ",
2360 theName,
2361 theItem->enchant1,
2362 new == 0 ? "" : ", with your current rings,",
2363 FP_DIV(staffChargeDuration(theItem), 10 * ringWisdomMultiplier(new * FP_FACTOR)));
2364 strcat(buf, buf2);
2365 }
2366
2367 if (theItem->lastUsed[0] > 0 && theItem->lastUsed[1] > 0 && theItem->lastUsed[2] > 0) {
2368 sprintf(buf2, "You last used it %li, %li and %li turns ago. ",
2369 rogue.absoluteTurnNumber - theItem->lastUsed[0],
2370 rogue.absoluteTurnNumber - theItem->lastUsed[1],
2371 rogue.absoluteTurnNumber - theItem->lastUsed[2]);
2372 strcat(buf, buf2);
2373 } else if (theItem->lastUsed[0] > 0 && theItem->lastUsed[1] > 0) {
2374 sprintf(buf2, "You last used it %li and %li turns ago. ",
2375 rogue.absoluteTurnNumber - theItem->lastUsed[0],
2376 rogue.absoluteTurnNumber - theItem->lastUsed[1]);
2377 strcat(buf, buf2);
2378 } else if (theItem->lastUsed[0] > 0) {
2379 turnsSinceLatestUse = rogue.absoluteTurnNumber - theItem->lastUsed[0];
2380 sprintf(buf2, "You last used it %li turn%s ago. ",
2381 turnsSinceLatestUse,
2382 turnsSinceLatestUse == 1 ? "" : "s");
2383 strcat(buf, buf2);
2384 }
2385
2386 // effect description
2387 if (((theItem->flags & (ITEM_IDENTIFIED | ITEM_MAX_CHARGES_KNOWN)) && staffTable[theItem->kind].identified)
2388 || rogue.playbackOmniscience) {
2389 switch (theItem->kind) {
2390 case STAFF_LIGHTNING:
2391 sprintf(buf2, "This staff deals damage to every creature in its line of fire; nothing is immune. (If the staff is enchanted, its average damage will increase by %i%%.)",
2392 (int) (100 * (staffDamageLow(enchant + FP_FACTOR) + staffDamageHigh(enchant + FP_FACTOR)) / (staffDamageLow(enchant) + staffDamageHigh(enchant)) - 100));
2393 break;
2394 case STAFF_FIRE:
2395 sprintf(buf2, "This staff deals damage to any creature that it hits, unless the creature is immune to fire. (If the staff is enchanted, its average damage will increase by %i%%.) It also sets creatures and flammable terrain on fire.",
2396 (int) (100 * (staffDamageLow(enchant + FP_FACTOR) + staffDamageHigh(enchant + FP_FACTOR)) / (staffDamageLow(enchant) + staffDamageHigh(enchant)) - 100));
2397 break;
2398 case STAFF_POISON:
2399 sprintf(buf2, "The bolt from this staff will poison any creature that it hits for %i turns. (If the staff is enchanted, this will increase to %i turns.)",
2400 staffPoison(enchant),
2401 staffPoison(enchant + FP_FACTOR));
2402 break;
2403 case STAFF_TUNNELING:
2404 sprintf(buf2, "The bolt from this staff will dissolve %i layers of obstruction. (If the staff is enchanted, this will increase to %i layers.)",
2405 theItem->enchant1,
2406 theItem->enchant1 + 1);
2407 break;
2408 case STAFF_BLINKING:
2409 sprintf(buf2, "This staff enables you to teleport up to %i spaces. (If the staff is enchanted, this will increase to %i spaces.)",
2410 staffBlinkDistance(enchant),
2411 staffBlinkDistance(enchant + FP_FACTOR));
2412 break;
2413 case STAFF_ENTRANCEMENT:
2414 sprintf(buf2, "This staff will compel its target to mirror your movements for %i turns. (If the staff is enchanted, this will increase to %i turns.)",
2415 staffEntrancementDuration(enchant),
2416 staffEntrancementDuration(enchant + FP_FACTOR));
2417 break;
2418 case STAFF_HEALING:
2419 if (enchant / FP_FACTOR < 10) {
2420 sprintf(buf2, "This staff will heal its target by %i%% of its maximum health. (If the staff is enchanted, this will increase to %i%%.)",
2421 theItem->enchant1 * 10,
2422 (theItem->enchant1 + 1) * 10);
2423 } else {
2424 strcpy(buf2, "This staff will completely heal its target.");
2425 }
2426 break;
2427 case STAFF_HASTE:
2428 sprintf(buf2, "This staff will cause its target to move twice as fast for %i turns. (If the staff is enchanted, this will increase to %i turns.)",
2429 staffHasteDuration(enchant),
2430 staffHasteDuration(enchant + FP_FACTOR));
2431 break;
2432 case STAFF_OBSTRUCTION:
2433 strcpy(buf2, "");
2434 break;
2435 case STAFF_DISCORD:
2436 sprintf(buf2, "This staff will cause discord for %i turns. (If the staff is enchanted, this will increase to %i turns.)",
2437 staffDiscordDuration(enchant),
2438 staffDiscordDuration(enchant + FP_FACTOR));
2439 break;
2440 case STAFF_CONJURATION:
2441 sprintf(buf2, "%i phantom blades will be called into service. (If the staff is enchanted, this will increase to %i blades.)",
2442 staffBladeCount(enchant),
2443 staffBladeCount(enchant + FP_FACTOR));
2444 break;
2445 case STAFF_PROTECTION:
2446 sprintf(buf2, "This staff will shield a creature for up to 20 turns against up to %i damage. (If the staff is enchanted, this will increase to %i damage.)",
2447 staffProtection(enchant) / 10,
2448 staffProtection(enchant + FP_FACTOR) / 10);
2449 break;
2450 default:
2451 strcpy(buf2, "No one knows what this staff does.");
2452 break;
2453 }
2454 if (buf2[0]) {
2455 strcat(buf, "\n\n");
2456 strcat(buf, buf2);
2457 }
2458 }
2459 break;
2460
2461 case WAND:
2462 strcat(buf, "\n\n");
2463 if ((theItem->flags & (ITEM_IDENTIFIED | ITEM_MAX_CHARGES_KNOWN)) || rogue.playbackOmniscience) {
2464 if (theItem->charges) {
2465 sprintf(buf2, "%i charge%s remain%s. Enchanting this wand will add %i charge%s.",
2466 theItem->charges,
2467 (theItem->charges == 1 ? "" : "s"),
2468 (theItem->charges == 1 ? "s" : ""),
2469 wandTable[theItem->kind].range.lowerBound,
2470 (wandTable[theItem->kind].range.lowerBound == 1 ? "" : "s"));
2471 } else {
2472 sprintf(buf2, "No charges remain. Enchanting this wand will add %i charge%s.",
2473 wandTable[theItem->kind].range.lowerBound,
2474 (wandTable[theItem->kind].range.lowerBound == 1 ? "" : "s"));
2475 }
2476 } else {
2477 if (theItem->enchant2) {
2478 sprintf(buf2, "You have used this wand %i time%s, but do not know how many charges, if any, remain.",
2479 theItem->enchant2,
2480 (theItem->enchant2 == 1 ? "" : "s"));
2481 } else {
2482 strcpy(buf2, "You have not yet used this wand.");
2483 }
2484
2485 if (wandTable[theItem->kind].identified) {
2486 strcat(buf, buf2);
2487 sprintf(buf2, " Wands of this type can be found with %i to %i charges. Enchanting this wand will add %i charge%s.",
2488 wandTable[theItem->kind].range.lowerBound,
2489 wandTable[theItem->kind].range.upperBound,
2490 wandTable[theItem->kind].range.lowerBound,
2491 (wandTable[theItem->kind].range.lowerBound == 1 ? "" : "s"));
2492 }
2493 }
2494 strcat(buf, buf2);
2495 break;
2496
2497 case RING:
2498 if (((theItem->flags & ITEM_IDENTIFIED) && ringTable[theItem->kind].identified) || rogue.playbackOmniscience) {
2499 if (theItem->enchant1) {
2500 switch (theItem->kind) {
2501 case RING_CLAIRVOYANCE:
2502 if (theItem->enchant1 > 0) {
2503 sprintf(buf2, "\n\nThis ring provides magical sight with a radius of %i. (If the ring is enchanted, this will increase to %i.)",
2504 theItem->enchant1 + 1,
2505 theItem->enchant1 + 2);
2506 } else {
2507 sprintf(buf2, "\n\nThis ring magically blinds you to a radius of %i. (If the ring is enchanted, this will decrease to %i.)",
2508 (theItem->enchant1 * -1) + 1,
2509 (theItem->enchant1 * -1));
2510 }
2511 strcat(buf, buf2);
2512 break;
2513 case RING_REGENERATION:
2514 sprintf(buf2, "\n\nWith this ring equipped, you will regenerate all of your health in %li turns (instead of %li). (If the ring is enchanted, this will decrease to %li turns.)",
2515 (long) (turnsForFullRegenInThousandths(enchant) / 1000),
2516 (long) TURNS_FOR_FULL_REGEN,
2517 (long) (turnsForFullRegenInThousandths(enchant + FP_FACTOR) / 1000));
2518 strcat(buf, buf2);
2519 break;
2520 case RING_TRANSFERENCE:
2521 sprintf(buf2, "\n\nDealing direct damage to a creature (whether in melee or otherwise) will %s you by %i%% of the damage dealt. (If the ring is enchanted, this will %s to %i%%.)",
2522 (theItem->enchant1 >= 0 ? "heal" : "harm"),
2523 abs(theItem->enchant1) * 5,
2524 (theItem->enchant1 >= 0 ? "increase" : "decrease"),
2525 abs(theItem->enchant1 + 1) * 5);
2526 strcat(buf, buf2);
2527 break;
2528 case RING_WISDOM:
2529 sprintf(buf2, "\n\nWhen worn, your staffs will recharge at %i%% of their normal rate. (If the ring is enchanted, the rate will increase to %i%% of the normal rate.)",
2530 (int) (100 * ringWisdomMultiplier(enchant) / FP_FACTOR),
2531 (int) (100 * ringWisdomMultiplier(enchant + FP_FACTOR) / FP_FACTOR));
2532 strcat(buf, buf2);
2533 break;
2534 case RING_REAPING:
2535 sprintf(buf2, "\n\nEach blow that you land with a weapon will %s your staffs and charms by 0-%i turns per point of damage dealt. (If the ring is enchanted, this will %s to 0-%i turns per point of damage.)",
2536 (theItem->enchant1 >= 0 ? "recharge" : "drain"),
2537 abs(theItem->enchant1),
2538 (theItem->enchant1 >= 0 ? "increase" : "decrease"),
2539 abs(theItem->enchant1 + 1));
2540 strcat(buf, buf2);
2541 break;
2542 default:
2543 break;
2544 }
2545 }
2546 } else {
2547 sprintf(buf2, "\n\nIt will reveal its secrets if worn for %i%s turn%s",
2548 theItem->charges,
2549 (theItem->charges == RING_DELAY_TO_AUTO_ID ? "" : " more"),
2550 (theItem->charges == 1 ? "" : "s"));
2551 strcat(buf, buf2);
2552
2553 if (!(theItem->flags & ITEM_IDENTIFIED) && (theItem->charges < RING_DELAY_TO_AUTO_ID || (theItem->flags & ITEM_MAGIC_DETECTED))) {
2554 sprintf(buf2, ", and until then it will function, at best, as a +%i ring.", theItem->timesEnchanted + 1);
2555 strcat(buf, buf2);
2556 } else {
2557 strcat(buf, ".");
2558 }
2559 }
2560
2561 // equipped? cursed?
2562 if (theItem->flags & ITEM_EQUIPPED) {
2563 sprintf(buf2, "\n\nThe %s is on your finger%s. ",
2564 theName,
2565 ((theItem->flags & ITEM_CURSED) ? ", and because it is cursed, you are powerless to remove it" : ""));
2566 strcat(buf, buf2);
2567 } else if (((theItem->flags & (ITEM_IDENTIFIED | ITEM_MAGIC_DETECTED)) || rogue.playbackOmniscience)
2568 && (theItem->flags & ITEM_CURSED)) {
2569 sprintf(buf2, "\n\n%sYou can feel a malevolent magic lurking within the %s.%s ",
2570 badColorEscape,
2571 theName,
2572 whiteColorEscape);
2573 strcat(buf, buf2);
2574 }
2575 break;
2576 case CHARM:
2577 switch (theItem->kind) {
2578 case CHARM_HEALTH:
2579 sprintf(buf2, "\n\nWhen used, the charm will heal %i%% of your health and recharge in %i turns. (If the charm is enchanted, it will heal %i%% of your health and recharge in %i turns.)",
2580 charmHealing(enchant),
2581 charmRechargeDelay(theItem->kind, theItem->enchant1),
2582 charmHealing(enchant + FP_FACTOR),
2583 charmRechargeDelay(theItem->kind, theItem->enchant1 + 1));
2584 break;
2585 case CHARM_PROTECTION:
2586 sprintf(buf2, "\n\nWhen used, the charm will shield you for up to 20 turns for up to %i%% of your total health and recharge in %i turns. (If the charm is enchanted, it will shield up to %i%% of your total health and recharge in %i turns.)",
2587 100 * charmProtection(enchant) / 10 / player.info.maxHP,
2588 charmRechargeDelay(theItem->kind, theItem->enchant1),
2589 100 * charmProtection(enchant + FP_FACTOR) / 10 / player.info.maxHP,
2590 charmRechargeDelay(theItem->kind, theItem->enchant1 + 1));
2591 break;
2592 case CHARM_HASTE:
2593 sprintf(buf2, "\n\nWhen used, the charm will haste you for %i turns and recharge in %i turns. (If the charm is enchanted, the haste will last %i turns and it will recharge in %i turns.)",
2594 charmEffectDuration(theItem->kind, theItem->enchant1),
2595 charmRechargeDelay(theItem->kind, theItem->enchant1),
2596 charmEffectDuration(theItem->kind, theItem->enchant1 + 1),
2597 charmRechargeDelay(theItem->kind, theItem->enchant1 + 1));
2598 break;
2599 case CHARM_FIRE_IMMUNITY:
2600 sprintf(buf2, "\n\nWhen used, the charm will grant you immunity to fire for %i turns and recharge in %i turns. (If the charm is enchanted, the immunity will last %i turns and it will recharge in %i turns.)",
2601 charmEffectDuration(theItem->kind, theItem->enchant1),
2602 charmRechargeDelay(theItem->kind, theItem->enchant1),
2603 charmEffectDuration(theItem->kind, theItem->enchant1 + 1),
2604 charmRechargeDelay(theItem->kind, theItem->enchant1 + 1));
2605 break;
2606 case CHARM_INVISIBILITY:
2607 sprintf(buf2, "\n\nWhen used, the charm will turn you invisible for %i turns and recharge in %i turns. While invisible, monsters more than two spaces away cannot track you. (If the charm is enchanted, the invisibility will last %i turns and it will recharge in %i turns.)",
2608 charmEffectDuration(theItem->kind, theItem->enchant1),
2609 charmRechargeDelay(theItem->kind, theItem->enchant1),
2610 charmEffectDuration(theItem->kind, theItem->enchant1 + 1),
2611 charmRechargeDelay(theItem->kind, theItem->enchant1 + 1));
2612 break;
2613 case CHARM_TELEPATHY:
2614 sprintf(buf2, "\n\nWhen used, the charm will grant you telepathy for %i turns and recharge in %i turns. (If the charm is enchanted, the telepathy will last %i turns and it will recharge in %i turns.)",
2615 charmEffectDuration(theItem->kind, theItem->enchant1),
2616 charmRechargeDelay(theItem->kind, theItem->enchant1),
2617 charmEffectDuration(theItem->kind, theItem->enchant1 + 1),
2618 charmRechargeDelay(theItem->kind, theItem->enchant1 + 1));
2619 break;
2620 case CHARM_LEVITATION:
2621 sprintf(buf2, "\n\nWhen used, the charm will lift you off the ground for %i turns and recharge in %i turns. (If the charm is enchanted, the levitation will last %i turns and it will recharge in %i turns.)",
2622 charmEffectDuration(theItem->kind, theItem->enchant1),
2623 charmRechargeDelay(theItem->kind, theItem->enchant1),
2624 charmEffectDuration(theItem->kind, theItem->enchant1 + 1),
2625 charmRechargeDelay(theItem->kind, theItem->enchant1 + 1));
2626 break;
2627 case CHARM_SHATTERING:
2628 sprintf(buf2, "\n\nWhen used, the charm will dissolve the nearby walls up to %i spaces away, and recharge in %i turns. (If the charm is enchanted, it will reach up to %i spaces and recharge in %i turns.)",
2629 charmShattering(enchant),
2630 charmRechargeDelay(theItem->kind, theItem->enchant1),
2631 charmShattering(enchant + FP_FACTOR),
2632 charmRechargeDelay(theItem->kind, theItem->enchant1 + 1));
2633 break;
2634 case CHARM_GUARDIAN:
2635 sprintf(buf2, "\n\nWhen used, a guardian will materialize for %i turns, and the charm will recharge in %i turns. (If the charm is enchanted, the guardian will last for %i turns and the charm will recharge in %i turns.)",
2636 charmGuardianLifespan(enchant),
2637 charmRechargeDelay(theItem->kind, theItem->enchant1),
2638 charmGuardianLifespan(enchant + FP_FACTOR),
2639 charmRechargeDelay(theItem->kind, theItem->enchant1 + 1));
2640 break;
2641 case CHARM_TELEPORTATION:
2642 sprintf(buf2, "\n\nWhen used, the charm will teleport you elsewhere in the dungeon and recharge in %i turns. (If the charm is enchanted, it will recharge in %i turns.)",
2643 charmRechargeDelay(theItem->kind, theItem->enchant1),
2644 charmRechargeDelay(theItem->kind, theItem->enchant1 + 1));
2645 break;
2646 case CHARM_RECHARGING:
2647 sprintf(buf2, "\n\nWhen used, the charm will recharge your staffs (though not your wands or charms), after which it will recharge in %i turns. (If the charm is enchanted, it will recharge in %i turns.)",
2648 charmRechargeDelay(theItem->kind, theItem->enchant1),
2649 charmRechargeDelay(theItem->kind, theItem->enchant1 + 1));
2650 break;
2651 case CHARM_NEGATION:
2652 sprintf(buf2, "\n\nWhen used, the charm will negate all magical effects on the creatures in your field of view and the items on the ground up to %i spaces away, and recharge in %i turns. (If the charm is enchanted, it will reach up to %i spaces and recharge in %i turns.)",
2653 charmNegationRadius(enchant),
2654 charmRechargeDelay(theItem->kind, theItem->enchant1),
2655 charmNegationRadius(enchant + FP_FACTOR),
2656 charmRechargeDelay(theItem->kind, theItem->enchant1 + 1));
2657 break;
2658 default:
2659 break;
2660 }
2661 strcat(buf, buf2);
2662 break;
2663 default:
2664 break;
2665 }
2666 }
2667
displayMagicCharForItem(item * theItem)2668 boolean displayMagicCharForItem(item *theItem) {
2669 if (!(theItem->flags & ITEM_MAGIC_DETECTED)
2670 || (theItem->category & PRENAMED_CATEGORY)) {
2671 return false;
2672 } else {
2673 return true;
2674 }
2675 }
2676
displayInventory(unsigned short categoryMask,unsigned long requiredFlags,unsigned long forbiddenFlags,boolean waitForAcknowledge,boolean includeButtons)2677 char displayInventory(unsigned short categoryMask,
2678 unsigned long requiredFlags,
2679 unsigned long forbiddenFlags,
2680 boolean waitForAcknowledge,
2681 boolean includeButtons) {
2682 item *theItem;
2683 short i, j, m, maxLength = 0, itemNumber, itemCount, equippedItemCount;
2684 short extraLineCount = 0;
2685 item *itemList[DROWS];
2686 char buf[COLS*3];
2687 char theKey;
2688 rogueEvent theEvent;
2689 boolean magicDetected, repeatDisplay;
2690 short highlightItemLine, itemSpaceRemaining;
2691 cellDisplayBuffer dbuf[COLS][ROWS];
2692 cellDisplayBuffer rbuf[COLS][ROWS];
2693 brogueButton buttons[50] = {{{0}}};
2694 short actionKey = -1;
2695 color darkItemColor;
2696
2697 char whiteColorEscapeSequence[20],
2698 grayColorEscapeSequence[20],
2699 yellowColorEscapeSequence[20],
2700 darkYellowColorEscapeSequence[20],
2701 goodColorEscapeSequence[20],
2702 badColorEscapeSequence[20];
2703 char *magicEscapePtr;
2704
2705 assureCosmeticRNG;
2706
2707 clearCursorPath();
2708 clearDisplayBuffer(dbuf);
2709
2710 whiteColorEscapeSequence[0] = '\0';
2711 encodeMessageColor(whiteColorEscapeSequence, 0, &white);
2712 grayColorEscapeSequence[0] = '\0';
2713 encodeMessageColor(grayColorEscapeSequence, 0, &gray);
2714 yellowColorEscapeSequence[0] = '\0';
2715 encodeMessageColor(yellowColorEscapeSequence, 0, &itemColor);
2716 darkItemColor = itemColor;
2717 applyColorAverage(&darkItemColor, &black, 50);
2718 darkYellowColorEscapeSequence[0] = '\0';
2719 encodeMessageColor(darkYellowColorEscapeSequence, 0, &darkItemColor);
2720 goodColorEscapeSequence[0] = '\0';
2721 encodeMessageColor(goodColorEscapeSequence, 0, &goodMessageColor);
2722 badColorEscapeSequence[0] = '\0';
2723 encodeMessageColor(badColorEscapeSequence, 0, &badMessageColor);
2724
2725 if (packItems->nextItem == NULL) {
2726 confirmMessages();
2727 message("Your pack is empty!", 0);
2728 restoreRNG;
2729 return 0;
2730 }
2731
2732 magicDetected = false;
2733 for (theItem = packItems->nextItem; theItem != NULL; theItem = theItem->nextItem) {
2734 if (displayMagicCharForItem(theItem) && (theItem->flags & ITEM_MAGIC_DETECTED)) {
2735 magicDetected = true;
2736 }
2737 }
2738
2739 // List the items in the order we want to display them, with equipped items at the top.
2740 itemNumber = 0;
2741 equippedItemCount = 0;
2742 // First, the equipped weapon if any.
2743 if (rogue.weapon) {
2744 itemList[itemNumber] = rogue.weapon;
2745 itemNumber++;
2746 equippedItemCount++;
2747 }
2748 // Now, the equipped armor if any.
2749 if (rogue.armor) {
2750 itemList[itemNumber] = rogue.armor;
2751 itemNumber++;
2752 equippedItemCount++;
2753 }
2754 // Now, the equipped rings, if any.
2755 if (rogue.ringLeft) {
2756 itemList[itemNumber] = rogue.ringLeft;
2757 itemNumber++;
2758 equippedItemCount++;
2759 }
2760 if (rogue.ringRight) {
2761 itemList[itemNumber] = rogue.ringRight;
2762 itemNumber++;
2763 equippedItemCount++;
2764 }
2765 // Now all of the non-equipped items.
2766 for (theItem = packItems->nextItem; theItem != NULL; theItem = theItem->nextItem) {
2767 if (!(theItem->flags & ITEM_EQUIPPED)) {
2768 itemList[itemNumber] = theItem;
2769 itemNumber++;
2770 }
2771 }
2772
2773 // Initialize the buttons:
2774 for (i=0; i < max(MAX_PACK_ITEMS, ROWS); i++) {
2775 buttons[i].y = mapToWindowY(i + (equippedItemCount && i >= equippedItemCount ? 1 : 0));
2776 buttons[i].buttonColor = black;
2777 buttons[i].opacity = INTERFACE_OPACITY;
2778 buttons[i].flags |= B_DRAW;
2779 }
2780 // Now prepare the buttons.
2781 const char closeParen = KEYBOARD_LABELS ? ')' : ' ';
2782 for (i=0; i<itemNumber; i++) {
2783 theItem = itemList[i];
2784 // Set button parameters for the item:
2785 buttons[i].flags |= (B_DRAW | B_GRADIENT | B_ENABLED);
2786 if (!waitForAcknowledge) {
2787 buttons[i].flags |= B_KEYPRESS_HIGHLIGHT;
2788 }
2789 buttons[i].hotkey[0] = theItem->inventoryLetter;
2790 buttons[i].hotkey[1] = theItem->inventoryLetter + 'A' - 'a';
2791
2792 if ((theItem->category & categoryMask) &&
2793 !(~(theItem->flags) & requiredFlags) &&
2794 !(theItem->flags & forbiddenFlags)) {
2795
2796 buttons[i].flags |= (B_HOVER_ENABLED);
2797 }
2798
2799 // Set the text for the button:
2800 itemName(theItem, buf, true, true, (buttons[i].flags & B_HOVER_ENABLED) ? &white : &gray);
2801 upperCase(buf);
2802
2803 if ((theItem->flags & ITEM_MAGIC_DETECTED)
2804 && !(theItem->category & AMULET)) { // Won't include food, keys, lumenstones or amulet.
2805
2806 int polarity = itemMagicPolarity(theItem);
2807 if (polarity == 0) {
2808 buttons[i].symbol[0] = '-';
2809 magicEscapePtr = yellowColorEscapeSequence;
2810 } else if (polarity == 1) {
2811 buttons[i].symbol[0] = G_GOOD_MAGIC;
2812 magicEscapePtr = goodColorEscapeSequence;
2813 } else {
2814 buttons[i].symbol[0] = G_BAD_MAGIC;
2815 magicEscapePtr = badColorEscapeSequence;
2816 }
2817
2818 // The first '*' is the magic detection symbol, e.g. '-' for non-magical.
2819 // The second '*' is the item character, e.g. ':' for food.
2820 sprintf(buttons[i].text, " %c%c %s* %s* %s%s%s%s",
2821 KEYBOARD_LABELS ? theItem->inventoryLetter : ' ',
2822 (theItem->flags & ITEM_PROTECTED ? '}' : closeParen),
2823 magicEscapePtr,
2824 (buttons[i].flags & B_HOVER_ENABLED) ? yellowColorEscapeSequence : darkYellowColorEscapeSequence,
2825 (buttons[i].flags & B_HOVER_ENABLED) ? whiteColorEscapeSequence : grayColorEscapeSequence,
2826 buf,
2827 grayColorEscapeSequence,
2828 (theItem->flags & ITEM_EQUIPPED ? ((theItem->category & WEAPON) ? " (in hand) " : " (worn) ") : ""));
2829 buttons[i].symbol[1] = theItem->displayChar;
2830 } else {
2831 sprintf(buttons[i].text, " %c%c %s%s* %s%s%s%s", // The '*' is the item character, e.g. ':' for food.
2832 KEYBOARD_LABELS ? theItem->inventoryLetter : ' ',
2833 (theItem->flags & ITEM_PROTECTED ? '}' : closeParen),
2834 (magicDetected ? " " : ""), // For proper spacing when this item is not detected but another is.
2835 (buttons[i].flags & B_HOVER_ENABLED) ? yellowColorEscapeSequence : darkYellowColorEscapeSequence,
2836 (buttons[i].flags & B_HOVER_ENABLED) ? whiteColorEscapeSequence : grayColorEscapeSequence,
2837 buf,
2838 grayColorEscapeSequence,
2839 (theItem->flags & ITEM_EQUIPPED ? ((theItem->category & WEAPON) ? " (in hand) " : " (worn) ") : ""));
2840 buttons[i].symbol[0] = theItem->displayChar;
2841 }
2842
2843 // Keep track of the maximum width needed:
2844 maxLength = max(maxLength, strLenWithoutEscapes(buttons[i].text));
2845
2846 // itemList[itemNumber] = theItem;
2847 //
2848 // itemNumber++;
2849 }
2850 //printf("\nMaxlength: %i", maxLength);
2851 itemCount = itemNumber;
2852 if (!itemNumber) {
2853 confirmMessages();
2854 message("Nothing of that type!", 0);
2855 restoreRNG;
2856 return 0;
2857 }
2858 if (waitForAcknowledge) {
2859 // Add the two extra lines as disabled buttons.
2860 itemSpaceRemaining = MAX_PACK_ITEMS - numberOfItemsInPack();
2861 if (itemSpaceRemaining) {
2862 sprintf(buttons[itemNumber + extraLineCount].text, "%s%s You have room for %i more item%s.",
2863 grayColorEscapeSequence,
2864 (magicDetected ? " " : ""),
2865 itemSpaceRemaining,
2866 (itemSpaceRemaining == 1 ? "" : "s"));
2867 } else {
2868 sprintf(buttons[itemNumber + extraLineCount].text, "%s%s Your pack is full.",
2869 grayColorEscapeSequence,
2870 (magicDetected ? " " : ""));
2871 }
2872 maxLength = max(maxLength, (strLenWithoutEscapes(buttons[itemNumber + extraLineCount].text)));
2873 extraLineCount++;
2874
2875 sprintf(buttons[itemNumber + extraLineCount].text,
2876 KEYBOARD_LABELS ? "%s%s -- press (a-z) for more info -- " : "%s%s -- touch an item for more info -- ",
2877 grayColorEscapeSequence,
2878 (magicDetected ? " " : ""));
2879 maxLength = max(maxLength, (strLenWithoutEscapes(buttons[itemNumber + extraLineCount].text)));
2880 extraLineCount++;
2881 }
2882 if (equippedItemCount) {
2883 // Add a separator button to fill in the blank line between equipped and unequipped items.
2884 sprintf(buttons[itemNumber + extraLineCount].text, " %s%s---",
2885 (magicDetected ? " " : ""),
2886 grayColorEscapeSequence);
2887 buttons[itemNumber + extraLineCount].y = mapToWindowY(equippedItemCount);
2888 extraLineCount++;
2889 }
2890
2891 for (i=0; i < itemNumber + extraLineCount; i++) {
2892
2893 // Position the button.
2894 buttons[i].x = COLS - maxLength;
2895
2896 // Pad the button label with space, so the button reaches to the right edge of the screen.
2897 m = strlen(buttons[i].text);
2898 for (j=buttons[i].x + strLenWithoutEscapes(buttons[i].text); j < COLS; j++) {
2899 buttons[i].text[m] = ' ';
2900 m++;
2901 }
2902 buttons[i].text[m] = '\0';
2903
2904 // Display the button. This would be redundant with the button loop,
2905 // except that we want the display to stick around until we get rid of it.
2906 drawButton(&(buttons[i]), BUTTON_NORMAL, dbuf);
2907 }
2908
2909 // Add invisible previous and next buttons, so up and down arrows can select items.
2910 // Previous
2911 buttons[itemNumber + extraLineCount + 0].flags = B_ENABLED; // clear everything else
2912 buttons[itemNumber + extraLineCount + 0].hotkey[0] = NUMPAD_8;
2913 buttons[itemNumber + extraLineCount + 0].hotkey[1] = UP_ARROW;
2914 // Next
2915 buttons[itemNumber + extraLineCount + 1].flags = B_ENABLED; // clear everything else
2916 buttons[itemNumber + extraLineCount + 1].hotkey[0] = NUMPAD_2;
2917 buttons[itemNumber + extraLineCount + 1].hotkey[1] = DOWN_ARROW;
2918
2919 overlayDisplayBuffer(dbuf, rbuf);
2920
2921 do {
2922 repeatDisplay = false;
2923
2924 // Do the button loop.
2925 highlightItemLine = -1;
2926 overlayDisplayBuffer(rbuf, NULL); // Remove the inventory display while the buttons are active,
2927 // since they look the same and we don't want their opacities to stack.
2928
2929 highlightItemLine = buttonInputLoop(buttons,
2930 itemCount + extraLineCount + 2, // the 2 is for up/down hotkeys
2931 COLS - maxLength,
2932 mapToWindowY(0),
2933 maxLength,
2934 itemNumber + extraLineCount,
2935 &theEvent);
2936 if (highlightItemLine == itemNumber + extraLineCount + 0) {
2937 // Up key
2938 highlightItemLine = itemNumber - 1;
2939 theEvent.shiftKey = true;
2940 } else if (highlightItemLine == itemNumber + extraLineCount + 1) {
2941 // Down key
2942 highlightItemLine = 0;
2943 theEvent.shiftKey = true;
2944 }
2945
2946 if (highlightItemLine >= 0) {
2947 theKey = itemList[highlightItemLine]->inventoryLetter;
2948 theItem = itemList[highlightItemLine];
2949 } else {
2950 theKey = ESCAPE_KEY;
2951 }
2952
2953 // Was an item selected?
2954 if (highlightItemLine > -1 && (waitForAcknowledge || theEvent.shiftKey || theEvent.controlKey)) {
2955
2956 do {
2957 // Yes. Highlight the selected item. Do this by changing the button color and re-displaying it.
2958
2959 overlayDisplayBuffer(dbuf, NULL);
2960
2961 //buttons[highlightItemLine].buttonColor = interfaceBoxColor;
2962 drawButton(&(buttons[highlightItemLine]), BUTTON_PRESSED, NULL);
2963 //buttons[highlightItemLine].buttonColor = black;
2964
2965 if (theEvent.shiftKey || theEvent.controlKey || waitForAcknowledge) {
2966 // Display an information window about the item.
2967 actionKey = printCarriedItemDetails(theItem, max(2, mapToWindowX(DCOLS - maxLength - 42)), mapToWindowY(2), 40, includeButtons, NULL);
2968
2969 overlayDisplayBuffer(rbuf, NULL); // remove the item info window
2970
2971 if (actionKey == -1) {
2972 repeatDisplay = true;
2973 overlayDisplayBuffer(dbuf, NULL); // redisplay the inventory
2974 } else {
2975 restoreRNG;
2976 repeatDisplay = false;
2977 overlayDisplayBuffer(rbuf, NULL); // restore the original screen
2978 }
2979
2980 switch (actionKey) {
2981 case APPLY_KEY:
2982 apply(theItem, true);
2983 break;
2984 case EQUIP_KEY:
2985 equip(theItem);
2986 break;
2987 case UNEQUIP_KEY:
2988 unequip(theItem);
2989 break;
2990 case DROP_KEY:
2991 drop(theItem);
2992 break;
2993 case THROW_KEY:
2994 throwCommand(theItem, false);
2995 break;
2996 case RELABEL_KEY:
2997 relabel(theItem);
2998 break;
2999 case CALL_KEY:
3000 call(theItem);
3001 break;
3002 case UP_KEY:
3003 highlightItemLine = highlightItemLine - 1;
3004 if (highlightItemLine < 0) {
3005 highlightItemLine = itemNumber - 1;
3006 }
3007 break;
3008 case DOWN_KEY:
3009 highlightItemLine = highlightItemLine + 1;
3010 if (highlightItemLine >= itemNumber) {
3011 highlightItemLine = 0;
3012 }
3013 break;
3014 default:
3015 break;
3016 }
3017
3018 if (actionKey == UP_KEY || actionKey == DOWN_KEY) {
3019 theKey = itemList[highlightItemLine]->inventoryLetter;
3020 theItem = itemList[highlightItemLine];
3021 } else if (actionKey > -1) {
3022 // Player took an action directly from the item screen; we're done here.
3023 restoreRNG;
3024 return 0;
3025 }
3026 }
3027 } while (actionKey == UP_KEY || actionKey == DOWN_KEY);
3028 }
3029 } while (repeatDisplay); // so you can get info on multiple items sequentially
3030
3031 overlayDisplayBuffer(rbuf, NULL); // restore the original screen
3032
3033 restoreRNG;
3034 return theKey;
3035 }
3036
numberOfMatchingPackItems(unsigned short categoryMask,unsigned long requiredFlags,unsigned long forbiddenFlags,boolean displayErrors)3037 short numberOfMatchingPackItems(unsigned short categoryMask,
3038 unsigned long requiredFlags, unsigned long forbiddenFlags,
3039 boolean displayErrors) {
3040 item *theItem;
3041 short matchingItemCount = 0;
3042
3043 if (packItems->nextItem == NULL) {
3044 if (displayErrors) {
3045 confirmMessages();
3046 message("Your pack is empty!", 0);
3047 }
3048 return 0;
3049 }
3050
3051 for (theItem = packItems->nextItem; theItem != NULL; theItem = theItem->nextItem) {
3052
3053 if (theItem->category & categoryMask &&
3054 !(~(theItem->flags) & requiredFlags) &&
3055 !(theItem->flags & forbiddenFlags)) {
3056
3057 matchingItemCount++;
3058 }
3059 }
3060
3061 if (matchingItemCount == 0) {
3062 if (displayErrors) {
3063 confirmMessages();
3064 message("You have nothing suitable.", 0);
3065 }
3066 return 0;
3067 }
3068
3069 return matchingItemCount;
3070 }
3071
updateEncumbrance()3072 void updateEncumbrance() {
3073 short moveSpeed, attackSpeed;
3074
3075 moveSpeed = player.info.movementSpeed;
3076 attackSpeed = player.info.attackSpeed;
3077
3078 if (player.status[STATUS_HASTED]) {
3079 moveSpeed /= 2;
3080 attackSpeed /= 2;
3081 } else if (player.status[STATUS_SLOWED]) {
3082 moveSpeed *= 2;
3083 attackSpeed *= 2;
3084 }
3085
3086 player.movementSpeed = moveSpeed;
3087 player.attackSpeed = attackSpeed;
3088
3089 recalculateEquipmentBonuses();
3090 }
3091
displayedArmorValue()3092 short displayedArmorValue() {
3093 if (!rogue.armor || (rogue.armor->flags & ITEM_IDENTIFIED)) {
3094 return player.info.defense / 10;
3095 } else {
3096 return ((armorTable[rogue.armor->kind].range.upperBound + armorTable[rogue.armor->kind].range.lowerBound) * FP_FACTOR / 2 / 10
3097 + strengthModifier(rogue.armor)) / FP_FACTOR;
3098 }
3099 }
3100
strengthCheck(item * theItem,boolean noisy)3101 void strengthCheck(item *theItem, boolean noisy) {
3102 char buf1[COLS], buf2[COLS*2];
3103 short strengthDeficiency;
3104
3105 updateEncumbrance();
3106 if (noisy && theItem) {
3107 if (theItem->category & WEAPON && theItem->strengthRequired > rogue.strength - player.weaknessAmount) {
3108 strengthDeficiency = theItem->strengthRequired - max(0, rogue.strength - player.weaknessAmount);
3109 strcpy(buf1, "");
3110 itemName(theItem, buf1, false, false, NULL);
3111 sprintf(buf2, "You can barely lift the %s; %i more strength would be ideal.", buf1, strengthDeficiency);
3112 message(buf2, 0);
3113 }
3114
3115 if (theItem->category & ARMOR && theItem->strengthRequired > rogue.strength - player.weaknessAmount) {
3116 strengthDeficiency = theItem->strengthRequired - max(0, rogue.strength - player.weaknessAmount);
3117 strcpy(buf1, "");
3118 itemName(theItem, buf1, false, false, NULL);
3119 sprintf(buf2, "You stagger under the weight of the %s; %i more strength would be ideal.",
3120 buf1, strengthDeficiency);
3121 message(buf2, 0);
3122 }
3123 }
3124 }
3125
3126 // Will prompt for an item if none is given.
3127 // Equips the item and records input if successful.
3128 // Player's failure to select an item will result in failure.
3129 // Failure does not record input.
equip(item * theItem)3130 void equip(item *theItem) {
3131 unsigned char command[10];
3132 short c = 0;
3133 item *theItem2;
3134
3135 command[c++] = EQUIP_KEY;
3136 if (!theItem) {
3137 theItem = promptForItemOfType((WEAPON|ARMOR|RING), 0, ITEM_EQUIPPED,
3138 KEYBOARD_LABELS ? "Equip what? (a-z, shift for more info; or <esc> to cancel)" : "Equip what?", true);
3139 }
3140 if (theItem == NULL) {
3141 return;
3142 }
3143
3144 theItem2 = NULL;
3145 command[c++] = theItem->inventoryLetter;
3146
3147 if (theItem->category & (WEAPON|ARMOR|RING)) {
3148
3149 if (theItem->category & RING) {
3150 if (theItem->flags & ITEM_EQUIPPED) {
3151 confirmMessages();
3152 message("you are already wearing that ring.", 0);
3153 return;
3154 } else if (rogue.ringLeft && rogue.ringRight) {
3155 confirmMessages();
3156 theItem2 = promptForItemOfType((RING), ITEM_EQUIPPED, 0,
3157 "You are already wearing two rings; remove which first?", true);
3158 if (!theItem2 || theItem2->category != RING || !(theItem2->flags & ITEM_EQUIPPED)) {
3159 if (theItem2) { // No message if canceled or did an inventory action instead.
3160 message("Invalid entry.", 0);
3161 }
3162 return;
3163 } else {
3164 command[c++] = theItem2->inventoryLetter;
3165 }
3166 }
3167 }
3168
3169 if (theItem->flags & ITEM_EQUIPPED) {
3170 confirmMessages();
3171 message("already equipped.", 0);
3172 return;
3173 }
3174
3175 if (theItem->category & (WEAPON | ARMOR)) {
3176 // Swapped out rings are handled above
3177 theItem2 = theItem->category & WEAPON ? rogue.weapon : rogue.armor;
3178 }
3179
3180 if (!equipItem(theItem, false, theItem2)) {
3181 return; // equip failed because current item is cursed
3182 }
3183
3184 command[c] = '\0';
3185 recordKeystrokeSequence(command);
3186
3187 // Something is only swapped in if something else swapped out
3188 rogue.swappedOut = theItem2;
3189 rogue.swappedIn = rogue.swappedOut ? theItem : NULL;
3190
3191 playerTurnEnded();
3192 } else {
3193 confirmMessages();
3194 message("You can't equip that.", 0);
3195 }
3196 }
3197
3198 // Returns whether the given item is a key that can unlock the given location.
3199 // An item qualifies if:
3200 // (1) it's a key (has ITEM_IS_KEY flag),
3201 // (2) its originDepth matches the depth, and
3202 // (3) either its key (x, y) location matches (x, y), or its machine number matches the machine number at (x, y).
keyMatchesLocation(item * theItem,short x,short y)3203 boolean keyMatchesLocation(item *theItem, short x, short y) {
3204 short i;
3205
3206 if ((theItem->flags & ITEM_IS_KEY)
3207 && theItem->originDepth == rogue.depthLevel) {
3208
3209 for (i=0; i < KEY_ID_MAXIMUM && (theItem->keyLoc[i].x || theItem->keyLoc[i].machine); i++) {
3210 if (theItem->keyLoc[i].x == x && theItem->keyLoc[i].y == y) {
3211 return true;
3212 } else if (theItem->keyLoc[i].machine == pmap[x][y].machineNumber) {
3213 return true;
3214 }
3215 }
3216 }
3217 return false;
3218 }
3219
keyInPackFor(short x,short y)3220 item *keyInPackFor(short x, short y) {
3221 item *theItem;
3222
3223 for (theItem = packItems->nextItem; theItem != NULL; theItem = theItem->nextItem) {
3224 if (keyMatchesLocation(theItem, x, y)) {
3225 return theItem;
3226 }
3227 }
3228 return NULL;
3229 }
3230
keyOnTileAt(short x,short y)3231 item *keyOnTileAt(short x, short y) {
3232 item *theItem;
3233 creature *monst;
3234
3235 if ((pmap[x][y].flags & HAS_PLAYER)
3236 && player.xLoc == x
3237 && player.yLoc == y
3238 && keyInPackFor(x, y)) {
3239
3240 return keyInPackFor(x, y);
3241 }
3242 if (pmap[x][y].flags & HAS_ITEM) {
3243 theItem = itemAtLoc(x, y);
3244 if (keyMatchesLocation(theItem, x, y)) {
3245 return theItem;
3246 }
3247 }
3248 if (pmap[x][y].flags & HAS_MONSTER) {
3249 monst = monsterAtLoc(x, y);
3250 if (monst->carriedItem) {
3251 theItem = monst->carriedItem;
3252 if (keyMatchesLocation(theItem, x, y)) {
3253 return theItem;
3254 }
3255 }
3256 }
3257 return NULL;
3258 }
3259
3260 // Aggroes out to the given distance.
aggravateMonsters(short distance,short x,short y,const color * flashColor)3261 void aggravateMonsters(short distance, short x, short y, const color *flashColor) {
3262 short i, j, **grid;
3263
3264 rogue.wpCoordinates[0][0] = x;
3265 rogue.wpCoordinates[0][1] = y;
3266 refreshWaypoint(0);
3267
3268 grid = allocGrid();
3269 fillGrid(grid, 0);
3270 calculateDistances(grid, x, y, T_PATHING_BLOCKER, NULL, true, false);
3271
3272 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
3273 creature *monst = nextCreature(&it);
3274 if (grid[monst->xLoc][monst->yLoc] <= distance) {
3275 if (monst->creatureState == MONSTER_SLEEPING) {
3276 wakeUp(monst);
3277 }
3278 if (monst->creatureState != MONSTER_ALLY && monst->leader != &player) {
3279 alertMonster(monst);
3280 monst->info.flags &= ~MONST_MAINTAINS_DISTANCE;
3281 monst->info.abilityFlags &= ~MA_AVOID_CORRIDORS;
3282 }
3283 }
3284 }
3285 for (i=0; i<DCOLS; i++) {
3286 for (j=0; j<DROWS; j++) {
3287 if (grid[i][j] >= 0 && grid[i][j] <= distance) {
3288 scentMap[i][j] = 0;
3289 addScentToCell(i, j, 2 * grid[i][j]);
3290 }
3291 }
3292 }
3293
3294 if (player.xLoc == x && player.yLoc == y) {
3295 player.status[STATUS_AGGRAVATING] = player.maxStatus[STATUS_AGGRAVATING] = distance;
3296 rogue.aggroRange = currentAggroValue();
3297 }
3298
3299 if (grid[player.xLoc][player.yLoc] >= 0 && grid[player.xLoc][player.yLoc] <= distance) {
3300 discover(x, y);
3301 discoverCell(x, y);
3302 colorFlash(flashColor, 0, (DISCOVERED | MAGIC_MAPPED), 10, distance, x, y);
3303 if (!playerCanSee(x, y)) {
3304 message("You hear a piercing shriek; something must have triggered a nearby alarm.", 0);
3305 }
3306 }
3307
3308 freeGrid(grid);
3309 }
3310
3311 // Generates a list of coordinates extending in a straight line
3312 // from originLoc (not included in the output), through targetLoc,
3313 // all the way to the edge of the map.
3314 // Any straight line passing through the target cell generates a valid
3315 // path; the function tries several lines and picks the one that works
3316 // best for the specified bolt type.
3317 // The list is terminated by a marker (-1, -1).
3318 // Returns the number of entries in the list (not counting the terminal marker).
getLineCoordinates(short listOfCoordinates[][2],const short originLoc[2],const short targetLoc[2],const bolt * theBolt)3319 short getLineCoordinates(short listOfCoordinates[][2], const short originLoc[2], const short targetLoc[2], const bolt *theBolt) {
3320 fixpt point[2], step[2];
3321 short listLength;
3322 int score, bestScore = 0, offset, bestOffset = 0;
3323
3324 // Set of candidate waypoints strategically placed within a diamond shape.
3325 // For why they must be within a diamond, google "diamond-exit rule".
3326 const int numOffsets = 21;
3327 const fixpt offsets[][2] = {
3328 {50, 50}, // center of the square first (coordinates are in %)
3329 {40, 40}, {60, 40}, {60, 60}, {40, 60},
3330 {50, 30}, {70, 50}, {50, 70}, {30, 50},
3331 {50, 20}, {80, 50}, {50, 80}, {20, 50},
3332 {50, 10}, {90, 50}, {50, 90}, {10, 50},
3333 {50, 1}, {99, 50}, {50, 99}, { 1, 50} };
3334
3335 if (originLoc[0] == targetLoc[0] && originLoc[1] == targetLoc[1]) {
3336 listOfCoordinates[0][0] = listOfCoordinates[0][1] = -1;
3337 return 0;
3338 }
3339
3340 // try all offsets; the last iteration will use the best offset found
3341 for (offset = 0; offset < numOffsets + 1; offset++) {
3342
3343 listLength = 0;
3344
3345 for (int i = 0; i <= 1; i++) {
3346 // always shoot from the center of the origin cell
3347 point[i] = originLoc[i] * FP_FACTOR + FP_FACTOR/2;
3348 // vector to target
3349 step[i] = targetLoc[i] * FP_FACTOR + offsets[offset < numOffsets ? offset : bestOffset][i] * FP_FACTOR / 100 - point[i];
3350 }
3351
3352 // normalize the step, to move exactly one row or column at a time
3353 fixpt m = max(llabs(step[0]), llabs(step[1]));
3354 step[0] = step[0] * FP_FACTOR / m;
3355 step[1] = step[1] * FP_FACTOR / m;
3356
3357 // move until we exit the map
3358 while (true) {
3359 for (int i = 0; i <= 1; i++) {
3360 point[i] += step[i];
3361 listOfCoordinates[listLength][i] = (point[i] < 0 ? -1 : point[i] / FP_FACTOR);
3362 }
3363 if (!coordinatesAreInMap(listOfCoordinates[listLength][0], listOfCoordinates[listLength][1])) break;
3364 listLength++;
3365 };
3366
3367 // last iteration does not need evaluation, we are returning it anyway
3368 if (offset == numOffsets) break;
3369
3370 // No bolt means we don't want any tuning. Returning first path (using center of target)
3371 if (theBolt == NULL) break;
3372
3373 // evaluate this path; we will return the path with the highest score
3374 score = 0;
3375
3376 for (int i = 0; i < listLength; i++) {
3377 short x = listOfCoordinates[i][0], y = listOfCoordinates[i][1];
3378
3379 boolean isImpassable = cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY);
3380 boolean isOpaque = cellHasTerrainFlag(x, y, T_OBSTRUCTS_VISION);
3381 boolean targetsEnemies = theBolt->flags & BF_TARGET_ENEMIES;
3382 boolean targetsAllies = theBolt->flags & BF_TARGET_ALLIES;
3383 boolean burningThrough = (theBolt->flags & BF_FIERY) && cellHasTerrainFlag(x, y, T_IS_FLAMMABLE);
3384 boolean isCastByPlayer = (originLoc[0] == player.xLoc && originLoc[1] == player.yLoc);
3385
3386 creature *caster = monsterAtLoc(originLoc[0], originLoc[1]);
3387 creature *monst = monsterAtLoc(x, y);
3388 boolean isMonster = monst
3389 && !(monst->bookkeepingFlags & MB_SUBMERGED)
3390 && !monsterIsHidden(monst, caster);
3391 boolean isEnemyOfCaster = (monst && caster && monstersAreEnemies(monst, caster));
3392 boolean isAllyOfCaster = (monst && caster && monstersAreTeammates(monst, caster));
3393
3394 // small bonus for making it this far
3395 score += 2;
3396
3397 // target reached?
3398 if (x == targetLoc[0] && y == targetLoc[1]) {
3399
3400 if ((!targetsEnemies && !targetsAllies) ||
3401 (targetsEnemies && isMonster && isEnemyOfCaster) ||
3402 (targetsAllies && isMonster && isAllyOfCaster)) {
3403
3404 // big bonus for hitting the target
3405 score += 5000;
3406 }
3407
3408 break; // we don't care about anything beyond the target--if the player did, they would have selected a farther target
3409 }
3410
3411 // if the caster is the player, undiscovered cells don't count (lest we reveal something about them)
3412 if (isCastByPlayer && !(pmap[x][y].flags & (DISCOVERED | MAGIC_MAPPED))) continue;
3413
3414 // nothing can get through impregnable obstacles
3415 if (isImpassable && pmap[x][y].flags & IMPREGNABLE) {
3416 break;
3417 }
3418
3419 // tunneling goes through everything
3420 if (theBolt->boltEffect == BE_TUNNELING) {
3421 score += (isImpassable ? 50 : isOpaque ? 10 : 0);
3422 continue;
3423 }
3424
3425 // hitting a creature with a bolt meant for enemies
3426 if (isMonster && targetsEnemies) {
3427 score += isEnemyOfCaster ? 50 : -200;
3428 }
3429
3430 // hitting a creature with a bolt meant for allies
3431 if (isMonster && targetsAllies) {
3432 score += isAllyOfCaster ? 50 : -200;
3433 }
3434
3435 // small penalty for setting terrain on fire (to prefer not to)
3436 if (burningThrough) {
3437 score -= 1;
3438 }
3439
3440 // check for obstruction
3441 if (isMonster && (theBolt->flags & BF_PASSES_THRU_CREATURES)) continue;
3442 if (isMonster || isImpassable || (isOpaque && !burningThrough)) break;
3443 }
3444
3445 if (score > bestScore) {
3446 bestScore = score;
3447 bestOffset = offset;
3448 }
3449 }
3450
3451 // demarcate the end of the list
3452 listOfCoordinates[listLength][0] = listOfCoordinates[listLength][1] = -1;
3453
3454 return listLength;
3455 }
3456
3457 // If a hypothetical bolt were launched from originLoc toward targetLoc,
3458 // with a given max distance and a toggle as to whether it halts at its impact location
3459 // or one space prior, where would it stop?
3460 // Takes into account the caster's knowledge; i.e. won't be blocked by monsters
3461 // that the caster is not aware of.
getImpactLoc(short returnLoc[2],const short originLoc[2],const short targetLoc[2],const short maxDistance,const boolean returnLastEmptySpace,const bolt * theBolt)3462 void getImpactLoc(short returnLoc[2], const short originLoc[2], const short targetLoc[2],
3463 const short maxDistance, const boolean returnLastEmptySpace, const bolt *theBolt) {
3464 short coords[DCOLS + 1][2];
3465 short i, n;
3466 creature *monst;
3467
3468 n = getLineCoordinates(coords, originLoc, targetLoc, theBolt);
3469 n = min(n, maxDistance);
3470 for (i=0; i<n; i++) {
3471 monst = monsterAtLoc(coords[i][0], coords[i][1]);
3472 if (monst
3473 && !monsterIsHidden(monst, monsterAtLoc(originLoc[0], originLoc[1]))
3474 && !(monst->bookkeepingFlags & MB_SUBMERGED)) {
3475 // Imaginary bolt hit the player or a monster.
3476 break;
3477 }
3478 if (cellHasTerrainFlag(coords[i][0], coords[i][1], (T_OBSTRUCTS_VISION | T_OBSTRUCTS_PASSABILITY))) {
3479 break;
3480 }
3481 }
3482 if (i == maxDistance) {
3483 returnLoc[0] = coords[i-1][0];
3484 returnLoc[1] = coords[i-1][1];
3485 } else if (returnLastEmptySpace) {
3486 if (i == 0) {
3487 returnLoc[0] = originLoc[0];
3488 returnLoc[1] = originLoc[1];
3489 } else {
3490 returnLoc[0] = coords[i-1][0];
3491 returnLoc[1] = coords[i-1][1];
3492 }
3493 } else {
3494 returnLoc[0] = coords[i][0];
3495 returnLoc[1] = coords[i][1];
3496 }
3497 brogueAssert(coordinatesAreInMap(returnLoc[0], returnLoc[1]));
3498 }
3499
3500 // Returns true if the two coordinates are unobstructed and diagonally adjacent,
3501 // but their two common neighbors are obstructed and at least one blocks diagonal movement.
impermissibleKinkBetween(short x1,short y1,short x2,short y2)3502 boolean impermissibleKinkBetween(short x1, short y1, short x2, short y2) {
3503 brogueAssert(coordinatesAreInMap(x1, y1));
3504 brogueAssert(coordinatesAreInMap(x2, y2));
3505 if (cellHasTerrainFlag(x1, y1, T_OBSTRUCTS_PASSABILITY)
3506 || cellHasTerrainFlag(x2, y2, T_OBSTRUCTS_PASSABILITY)) {
3507 // One of the two locations is obstructed.
3508 return false;
3509 }
3510 if (abs(x1 - x2) != 1
3511 || abs(y1 - y2) != 1) {
3512 // Not diagonally adjacent.
3513 return false;
3514 }
3515 if (!cellHasTerrainFlag(x2, y1, T_OBSTRUCTS_PASSABILITY)
3516 || !cellHasTerrainFlag(x1, y2, T_OBSTRUCTS_PASSABILITY)) {
3517 // At least one of the common neighbors isn't obstructed.
3518 return false;
3519 }
3520 if (!cellHasTerrainFlag(x2, y1, T_OBSTRUCTS_DIAGONAL_MOVEMENT)
3521 && !cellHasTerrainFlag(x1, y2, T_OBSTRUCTS_DIAGONAL_MOVEMENT)) {
3522 // Neither of the common neighbors obstructs diagonal movement.
3523 return false;
3524 }
3525 return true;
3526 }
3527
tunnelize(short x,short y)3528 boolean tunnelize(short x, short y) {
3529 enum dungeonLayers layer;
3530 boolean didSomething = false;
3531 creature *monst;
3532 short x2, y2;
3533 enum directions dir;
3534
3535 if (pmap[x][y].flags & IMPREGNABLE) {
3536 return false;
3537 }
3538 freeCaptivesEmbeddedAt(x, y);
3539 if (x == 0 || x == DCOLS - 1 || y == 0 || y == DROWS - 1) {
3540 pmap[x][y].layers[DUNGEON] = CRYSTAL_WALL; // don't dissolve the boundary walls
3541 didSomething = true;
3542 } else {
3543 for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
3544 if (tileCatalog[pmap[x][y].layers[layer]].flags & (T_OBSTRUCTS_PASSABILITY | T_OBSTRUCTS_VISION)) {
3545 pmap[x][y].layers[layer] = (layer == DUNGEON ? FLOOR : NOTHING);
3546 didSomething = true;
3547 }
3548 }
3549 }
3550 if (didSomething) {
3551 spawnDungeonFeature(x, y, &dungeonFeatureCatalog[DF_TUNNELIZE], true, false);
3552 if (pmap[x][y].flags & HAS_MONSTER) {
3553 // Kill turrets and sentinels if you tunnelize them.
3554 monst = monsterAtLoc(x, y);
3555 if (monst->info.flags & MONST_ATTACKABLE_THRU_WALLS) {
3556 inflictLethalDamage(NULL, monst);
3557 }
3558 }
3559 }
3560 if (!cellHasTerrainFlag(x, y, T_OBSTRUCTS_DIAGONAL_MOVEMENT)
3561 && didSomething) {
3562 // Tunnel out any diagonal kinks between walls.
3563 for (dir = 0; dir < DIRECTION_COUNT; dir++) {
3564 x2 = x + nbDirs[dir][0];
3565 y2 = y + nbDirs[dir][1];
3566 if (coordinatesAreInMap(x2, y2)
3567 && impermissibleKinkBetween(x, y, x2, y2)) {
3568
3569 if ((pmap[x][y2].flags & IMPREGNABLE)
3570 || (!(pmap[x2][y].flags & IMPREGNABLE) && rand_percent(50))) {
3571
3572 tunnelize(x2, y);
3573 } else {
3574 tunnelize(x, y2);
3575 }
3576 }
3577 }
3578 }
3579 return didSomething;
3580 }
3581 /* Negates the given creature. Returns true if there was an effect for the purpose of identifying a wand of negation.
3582 * If the creature was stripped of any traits or abilities, the wasNegated property is set, which is used for display in
3583 * sidebar and the creature's description.
3584 */
negate(creature * monst)3585 boolean negate(creature *monst) {
3586 short i, j;
3587 enum boltType backupBolts[20];
3588 char buf[DCOLS * 3], monstName[DCOLS];
3589 boolean negated = false;
3590
3591 monsterName(monstName, monst, true);
3592
3593 if (monst->info.abilityFlags & ~MA_NON_NEGATABLE_ABILITIES) {
3594 monst->info.abilityFlags &= MA_NON_NEGATABLE_ABILITIES; // negated monsters lose all special abilities
3595 negated = true;
3596 monst->wasNegated = true;
3597 }
3598
3599 if (monst->bookkeepingFlags & MB_SEIZING){
3600 monst->bookkeepingFlags &= ~MB_SEIZING;
3601 negated = true;
3602 }
3603
3604 if (monst->info.flags & MONST_DIES_IF_NEGATED) {
3605 if (monst->status[STATUS_LEVITATING]) {
3606 sprintf(buf, "%s dissipates into thin air", monstName);
3607 } else if (monst->info.flags & MONST_INANIMATE) {
3608 sprintf(buf, "%s shatters into tiny pieces", monstName);
3609 } else {
3610 sprintf(buf, "%s falls to the ground, lifeless", monstName);
3611 }
3612 killCreature(monst, false);
3613 combatMessage(buf, messageColorFromVictim(monst));
3614 negated = true;
3615 } else if (!(monst->info.flags & MONST_INVULNERABLE)) {
3616 // works on inanimates
3617 if (monst->status[STATUS_IMMUNE_TO_FIRE]) {
3618 monst->status[STATUS_IMMUNE_TO_FIRE] = 0;
3619 negated = true;
3620 }
3621 if (monst->status[STATUS_SLOWED]) {
3622 monst->status[STATUS_SLOWED] = 0;
3623 negated = true;
3624 }
3625 if (monst->status[STATUS_HASTED]) {
3626 monst->status[STATUS_HASTED] = 0;
3627 negated = true;
3628 }
3629 if (monst->status[STATUS_CONFUSED]) {
3630 monst->status[STATUS_CONFUSED] = 0;
3631 negated = true;
3632 }
3633 if (monst->status[STATUS_ENTRANCED]) {
3634 monst->status[STATUS_ENTRANCED] = 0;
3635 negated = true;
3636 }
3637 if (monst->status[STATUS_DISCORDANT]) {
3638 monst->status[STATUS_DISCORDANT] = 0;
3639 negated = true;
3640 }
3641 if (monst->status[STATUS_SHIELDED]) {
3642 monst->status[STATUS_SHIELDED] = 0;
3643 negated = true;
3644 }
3645 if (monst->status[STATUS_INVISIBLE]) {
3646 monst->status[STATUS_INVISIBLE] = 0;
3647 negated = true;
3648 }
3649 if (monst == &player) {
3650 if (monst->status[STATUS_TELEPATHIC] > 1 ) {
3651 monst->status[STATUS_TELEPATHIC] = 1;
3652 negated = true;
3653 }
3654 if (monst->status[STATUS_MAGICAL_FEAR] > 1 ) {
3655 monst->status[STATUS_MAGICAL_FEAR] = 1;
3656 negated = true;
3657 }
3658 if (monst->status[STATUS_LEVITATING] > 1 ) {
3659 monst->status[STATUS_LEVITATING] = 1;
3660 negated = true;
3661 }
3662 if (monst->status[STATUS_DARKNESS]) {
3663 monst->status[STATUS_DARKNESS] = 0;
3664 updateMinersLightRadius();
3665 updateVision(true);
3666 negated = true;
3667 }
3668 } else {
3669 if (monst->status[STATUS_TELEPATHIC] > 0 ) {
3670 monst->status[STATUS_TELEPATHIC] = 0;
3671 negated = true;
3672 }
3673 if (monst->status[STATUS_MAGICAL_FEAR] > 0 ) {
3674 monst->status[STATUS_MAGICAL_FEAR] = 0;
3675 negated = true;
3676 }
3677 if (monst->status[STATUS_LEVITATING] > 0 ) {
3678 monst->status[STATUS_LEVITATING] = 0;
3679 negated = true;
3680 }
3681 }
3682 if (monst->info.flags & MONST_IMMUNE_TO_FIRE) {
3683 monst->info.flags &= ~MONST_IMMUNE_TO_FIRE;
3684 monst->wasNegated = true;
3685 negated = true;
3686 }
3687 if (monst->movementSpeed != monst->info.movementSpeed) {
3688 monst->movementSpeed = monst->info.movementSpeed;
3689 negated = true;
3690 }
3691 if (monst->attackSpeed != monst->info.attackSpeed) {
3692 monst->attackSpeed = monst->info.attackSpeed;
3693 negated = true;
3694 }
3695
3696 if (monst != &player && monst->mutationIndex > -1 && mutationCatalog[monst->mutationIndex].canBeNegated) {
3697
3698 monst->mutationIndex = -1;
3699 negated = true;
3700 monst->wasNegated = true;
3701 }
3702 if (monst != &player && (monst->info.flags & NEGATABLE_TRAITS)) {
3703 if ((monst->info.flags & MONST_FIERY) && monst->status[STATUS_BURNING]) {
3704 extinguishFireOnCreature(monst);
3705 }
3706 monst->info.flags &= ~NEGATABLE_TRAITS;
3707 negated = true;
3708 monst->wasNegated = true;
3709 refreshDungeonCell(monst->xLoc, monst->yLoc);
3710 refreshSideBar(-1, -1, false);
3711 }
3712 for (i = 0; i < 20; i++) {
3713 backupBolts[i] = monst->info.bolts[i];
3714 monst->info.bolts[i] = BOLT_NONE;
3715 if (monst->info.bolts[i] && !(boltCatalog[monst->info.bolts[i]].flags & BF_NOT_NEGATABLE)) {
3716 negated = true;
3717 }
3718 }
3719 for (i = 0, j = 0; i < 20 && backupBolts[i]; i++) {
3720 if (boltCatalog[backupBolts[i]].flags & BF_NOT_NEGATABLE) {
3721 monst->info.bolts[j] = backupBolts[i];
3722 j++;
3723 }
3724 }
3725 monst->newPowerCount = monst->totalPowerCount; // Allies can re-learn lost ability slots.
3726 applyInstantTileEffectsToCreature(monst); // in case it should immediately die or fall into a chasm
3727 }
3728
3729 if (negated && monst != &player && !(monst->info.flags & MONST_DIES_IF_NEGATED)) {
3730 sprintf(buf, "%s is stripped of $HISHER special traits", monstName);
3731 resolvePronounEscapes(buf, monst);
3732 combatMessage(buf, messageColorFromVictim(monst));
3733 }
3734
3735 return negated;
3736 }
3737
3738 // Adds one to the creature's weakness, sets the weakness status duration to maxDuration.
weaken(creature * monst,short maxDuration)3739 void weaken(creature *monst, short maxDuration) {
3740 if (monst->weaknessAmount < 10) {
3741 monst->weaknessAmount++;
3742 }
3743 monst->status[STATUS_WEAKENED] = max(monst->status[STATUS_WEAKENED], maxDuration);
3744 monst->maxStatus[STATUS_WEAKENED] = max(monst->maxStatus[STATUS_WEAKENED], maxDuration);
3745 if (monst == &player) {
3746 messageWithColor("your muscles weaken as an enervating toxin fills your veins.", &badMessageColor, 0);
3747 strengthCheck(rogue.weapon, true);
3748 strengthCheck(rogue.armor, true);
3749 }
3750 }
3751
3752 // True if the creature polymorphed; false if not.
polymorph(creature * monst)3753 boolean polymorph(creature *monst) {
3754 short previousDamageTaken, healthFraction, newMonsterIndex;
3755
3756 if (monst == &player || (monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
3757 return false; // Sorry, this is not Nethack.
3758 }
3759
3760 if (monst->creatureState == MONSTER_FLEEING
3761 && (monst->info.flags & (MONST_MAINTAINS_DISTANCE | MONST_FLEES_NEAR_DEATH)) || (monst->info.abilityFlags & MA_HIT_STEAL_FLEE)) {
3762
3763 monst->creatureState = MONSTER_TRACKING_SCENT;
3764 monst->creatureMode = MODE_NORMAL;
3765 }
3766
3767 unAlly(monst); // Sorry, no cheap dragon allies.
3768 monst->mutationIndex = -1; // Polymorph cures mutation -- basic science.
3769
3770 // After polymorphing, don't "drop" any creature on death (e.g. phylactery, phoenix egg)
3771 if (monst->carriedMonster) {
3772 freeCreature(monst->carriedMonster);
3773 monst->carriedMonster = NULL;
3774 }
3775
3776 healthFraction = monst->currentHP * 1000 / monst->info.maxHP;
3777 previousDamageTaken = monst->info.maxHP - monst->currentHP;
3778
3779 do {
3780 newMonsterIndex = rand_range(1, NUMBER_MONSTER_KINDS - 1);
3781 } while (monsterCatalog[newMonsterIndex].flags & (MONST_INANIMATE | MONST_NO_POLYMORPH) // Can't turn something into an inanimate object or lich/phoenix/warden.
3782 || newMonsterIndex == monst->info.monsterID); // Can't stay the same monster.
3783 monst->info = monsterCatalog[newMonsterIndex]; // Presto change-o!
3784
3785 monst->info.turnsBetweenRegen *= 1000;
3786 monst->currentHP = max(1, max(healthFraction * monst->info.maxHP / 1000, monst->info.maxHP - previousDamageTaken));
3787
3788 monst->movementSpeed = monst->info.movementSpeed;
3789 monst->attackSpeed = monst->info.attackSpeed;
3790 if (monst->status[STATUS_HASTED]) {
3791 monst->movementSpeed /= 2;
3792 monst->attackSpeed /= 2;
3793 }
3794 if (monst->status[STATUS_SLOWED]) {
3795 monst->movementSpeed *= 2;
3796 monst->attackSpeed *= 2;
3797 }
3798
3799 clearStatus(monst);
3800
3801 if (monst->info.flags & MONST_FIERY) {
3802 monst->status[STATUS_BURNING] = monst->maxStatus[STATUS_BURNING] = 1000; // won't decrease
3803 }
3804 if (monst->info.flags & MONST_FLIES) {
3805 monst->status[STATUS_LEVITATING] = monst->maxStatus[STATUS_LEVITATING] = 1000; // won't decrease
3806 }
3807 if (monst->info.flags & MONST_IMMUNE_TO_FIRE) {
3808 monst->status[STATUS_IMMUNE_TO_FIRE] = monst->maxStatus[STATUS_IMMUNE_TO_FIRE] = 1000; // won't decrease
3809 }
3810 if (monst->info.flags & MONST_INVISIBLE) {
3811 monst->status[STATUS_INVISIBLE] = monst->maxStatus[STATUS_INVISIBLE] = 1000; // won't decrease
3812 }
3813 monst->status[STATUS_NUTRITION] = monst->maxStatus[STATUS_NUTRITION] = 1000;
3814
3815 if (monst->bookkeepingFlags & MB_CAPTIVE) {
3816 demoteMonsterFromLeadership(monst);
3817 monst->creatureState = MONSTER_TRACKING_SCENT;
3818 monst->bookkeepingFlags &= ~MB_CAPTIVE;
3819 }
3820 monst->bookkeepingFlags &= ~(MB_SEIZING | MB_SEIZED);
3821
3822 monst->ticksUntilTurn = max(monst->ticksUntilTurn, 101);
3823
3824 refreshDungeonCell(monst->xLoc, monst->yLoc);
3825 if (boltCatalog[BOLT_POLYMORPH].backColor) {
3826 flashMonster(monst, boltCatalog[BOLT_POLYMORPH].backColor, 100);
3827 }
3828 return true;
3829 }
3830
slow(creature * monst,short turns)3831 void slow(creature *monst, short turns) {
3832 if (!(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
3833 monst->status[STATUS_SLOWED] = monst->maxStatus[STATUS_SLOWED] = turns;
3834 monst->status[STATUS_HASTED] = 0;
3835 if (monst == &player) {
3836 updateEncumbrance();
3837 message("you feel yourself slow down.", 0);
3838 } else {
3839 monst->movementSpeed = monst->info.movementSpeed * 2;
3840 monst->attackSpeed = monst->info.attackSpeed * 2;
3841 }
3842 }
3843 }
3844
haste(creature * monst,short turns)3845 void haste(creature *monst, short turns) {
3846 if (monst && !(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
3847 monst->status[STATUS_SLOWED] = 0;
3848 monst->status[STATUS_HASTED] = monst->maxStatus[STATUS_HASTED] = turns;
3849 if (monst == &player) {
3850 updateEncumbrance();
3851 message("you feel yourself speed up.", 0);
3852 } else {
3853 monst->movementSpeed = monst->info.movementSpeed / 2;
3854 monst->attackSpeed = monst->info.attackSpeed / 2;
3855 }
3856 }
3857 }
3858
heal(creature * monst,short percent,boolean panacea)3859 void heal(creature *monst, short percent, boolean panacea) {
3860 char buf[COLS], monstName[COLS];
3861 monst->currentHP = min(monst->info.maxHP, monst->currentHP + percent * monst->info.maxHP / 100);
3862 if (panacea) {
3863 if (monst->status[STATUS_HALLUCINATING] > 1) {
3864 monst->status[STATUS_HALLUCINATING] = 1;
3865 }
3866 if (monst->status[STATUS_CONFUSED] > 1) {
3867 monst->status[STATUS_CONFUSED] = 1;
3868 }
3869 if (monst->status[STATUS_NAUSEOUS] > 1) {
3870 monst->status[STATUS_NAUSEOUS] = 1;
3871 }
3872 if (monst->status[STATUS_SLOWED] > 1) {
3873 monst->status[STATUS_SLOWED] = 1;
3874 }
3875 if (monst->status[STATUS_WEAKENED] > 1) {
3876 monst->weaknessAmount = 0;
3877 monst->status[STATUS_WEAKENED] = 0;
3878 updateEncumbrance();
3879 }
3880 if (monst->status[STATUS_POISONED]) {
3881 monst->poisonAmount = 0;
3882 monst->status[STATUS_POISONED] = 0;
3883 }
3884 if (monst->status[STATUS_DARKNESS] > 0) {
3885 monst->status[STATUS_DARKNESS] = 0;
3886 if (monst == &player) {
3887 updateMinersLightRadius();
3888 updateVision(true);
3889 }
3890 }
3891 }
3892 if (canDirectlySeeMonster(monst)
3893 && monst != &player
3894 && !panacea) {
3895
3896 monsterName(monstName, monst, true);
3897 sprintf(buf, "%s looks healthier", monstName);
3898 combatMessage(buf, NULL);
3899 }
3900 }
3901
makePlayerTelepathic(short duration)3902 void makePlayerTelepathic(short duration) {
3903 player.status[STATUS_TELEPATHIC] = player.maxStatus[STATUS_TELEPATHIC] = duration;
3904 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
3905 creature *monst = nextCreature(&it);
3906 refreshDungeonCell(monst->xLoc, monst->yLoc);
3907 }
3908 if (!hasNextCreature(iterateCreatures(monsters))) {
3909 message("you can somehow tell that you are alone on this depth at the moment.", 0);
3910 } else {
3911 message("you can somehow feel the presence of other creatures' minds!", 0);
3912 }
3913 }
3914
rechargeItems(unsigned long categories)3915 void rechargeItems(unsigned long categories) {
3916 item *tempItem;
3917 short x, y, z, i, categoryCount;
3918 char buf[DCOLS * 3];
3919
3920 x = y = z = 0; // x counts staffs, y counts wands, z counts charms
3921 for (tempItem = packItems->nextItem; tempItem != NULL; tempItem = tempItem->nextItem) {
3922 if (tempItem->category & categories & STAFF) {
3923 x++;
3924 tempItem->charges = tempItem->enchant1;
3925 tempItem->enchant2 = (tempItem->kind == STAFF_BLINKING || tempItem->kind == STAFF_OBSTRUCTION ? 10000 : 5000) / tempItem->enchant1;
3926 }
3927 if (tempItem->category & categories & WAND) {
3928 y++;
3929 tempItem->charges++;
3930 }
3931 if (tempItem->category & categories & CHARM) {
3932 z++;
3933 tempItem->charges = 0;
3934 }
3935 }
3936
3937 categoryCount = (x ? 1 : 0) + (y ? 1 : 0) + (z ? 1 : 0);
3938
3939 if (categoryCount) {
3940 i = 0;
3941 strcpy(buf, "a surge of energy courses through your pack, recharging your ");
3942 if (x) {
3943 i++;
3944 strcat(buf, x == 1 ? "staff" : "staffs");
3945 if (i == categoryCount - 1) {
3946 strcat(buf, " and ");
3947 } else if (i <= categoryCount - 2) {
3948 strcat(buf, ", ");
3949 }
3950 }
3951 if (y) {
3952 i++;
3953 strcat(buf, y == 1 ? "wand" : "wands");
3954 if (i == categoryCount - 1) {
3955 strcat(buf, " and ");
3956 } else if (i <= categoryCount - 2) {
3957 strcat(buf, ", ");
3958 }
3959 }
3960 if (z) {
3961 strcat(buf, z == 1 ? "charm" : "charms");
3962 }
3963 strcat(buf, ".");
3964 message(buf, 0);
3965 } else {
3966 message("a surge of energy courses through your pack, but nothing happens.", 0);
3967 }
3968 }
3969
3970 //void causeFear(const char *emitterName) {
3971 // creature *monst;
3972 // short numberOfMonsters = 0;
3973 // char buf[DCOLS*3], mName[DCOLS];
3974 //
3975 // for (monst = monsters->nextCreature; monst != NULL; monst = monst->nextCreature) {
3976 // if (pmap[monst->xLoc][monst->yLoc].flags & IN_FIELD_OF_VIEW
3977 // && monst->creatureState != MONSTER_FLEEING
3978 // && !(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
3979 //
3980 // monst->status[STATUS_MAGICAL_FEAR] = monst->maxStatus[STATUS_MAGICAL_FEAR] = rand_range(150, 225);
3981 // monst->creatureState = MONSTER_FLEEING;
3982 // if (canSeeMonster(monst)) {
3983 // numberOfMonsters++;
3984 // monsterName(mName, monst, true);
3985 // }
3986 // }
3987 // }
3988 // if (numberOfMonsters > 1) {
3989 // sprintf(buf, "%s emits a brilliant flash of red light, and the monsters flee!", emitterName);
3990 // } else if (numberOfMonsters == 1) {
3991 // sprintf(buf, "%s emits a brilliant flash of red light, and %s flees!", emitterName, mName);
3992 // } else {
3993 // sprintf(buf, "%s emits a brilliant flash of red light!", emitterName);
3994 // }
3995 // message(buf, 0);
3996 // colorFlash(&redFlashColor, 0, IN_FIELD_OF_VIEW, 15, DCOLS, player.xLoc, player.yLoc);
3997 //}
3998
negationBlast(const char * emitterName,const short distance)3999 void negationBlast(const char *emitterName, const short distance) {
4000 item *theItem;
4001 char buf[DCOLS];
4002
4003 sprintf(buf, "%s emits a numbing torrent of anti-magic!", emitterName);
4004 messageWithColor(buf, &itemMessageColor, 0);
4005 colorFlash(&pink, 0, IN_FIELD_OF_VIEW, 3 + distance / 5, distance, player.xLoc, player.yLoc);
4006 negate(&player);
4007 flashMonster(&player, &pink, 100);
4008 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
4009 creature *monst = nextCreature(&it);
4010 if ((pmap[monst->xLoc][monst->yLoc].flags & IN_FIELD_OF_VIEW)
4011 && (player.xLoc - monst->xLoc) * (player.xLoc - monst->xLoc) + (player.yLoc - monst->yLoc) * (player.yLoc - monst->yLoc) <= distance * distance) {
4012
4013 if (canSeeMonster(monst)) {
4014 flashMonster(monst, &pink, 100);
4015 }
4016 negate(monst); // This can be fatal.
4017 }
4018 }
4019 for (theItem = floorItems; theItem != NULL; theItem = theItem->nextItem) {
4020 if ((pmap[theItem->xLoc][theItem->yLoc].flags & IN_FIELD_OF_VIEW)
4021 && (player.xLoc - theItem->xLoc) * (player.xLoc - theItem->xLoc) + (player.yLoc - theItem->yLoc) * (player.yLoc - theItem->yLoc) <= distance * distance) {
4022
4023 theItem->flags &= ~(ITEM_MAGIC_DETECTED | ITEM_CURSED);
4024 switch (theItem->category) {
4025 case WEAPON:
4026 case ARMOR:
4027 theItem->enchant1 = theItem->enchant2 = theItem->charges = 0;
4028 theItem->flags &= ~(ITEM_RUNIC | ITEM_RUNIC_HINTED | ITEM_RUNIC_IDENTIFIED | ITEM_PROTECTED);
4029 identify(theItem);
4030 pmap[theItem->xLoc][theItem->yLoc].flags &= ~ITEM_DETECTED;
4031 refreshDungeonCell(theItem->xLoc, theItem->yLoc);
4032 break;
4033 case STAFF:
4034 theItem->charges = 0;
4035 break;
4036 case WAND:
4037 theItem->charges = 0;
4038 theItem->flags |= ITEM_MAX_CHARGES_KNOWN;
4039 break;
4040 case RING:
4041 theItem->enchant1 = 0;
4042 theItem->flags |= ITEM_IDENTIFIED; // Reveal that it is (now) +0, but not necessarily which kind of ring it is.
4043 updateIdentifiableItems();
4044 break;
4045 case CHARM:
4046 theItem->charges = charmRechargeDelay(theItem->kind, theItem->enchant1);
4047 break;
4048 default:
4049 break;
4050 }
4051 }
4052 }
4053 }
4054
discordBlast(const char * emitterName,const short distance)4055 void discordBlast(const char *emitterName, const short distance) {
4056 char buf[DCOLS];
4057
4058 sprintf(buf, "%s emits a wave of unsettling purple radiation!", emitterName);
4059 messageWithColor(buf, &itemMessageColor, 0);
4060 colorFlash(&discordColor, 0, IN_FIELD_OF_VIEW, 3 + distance / 5, distance, player.xLoc, player.yLoc);
4061 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
4062 creature *monst = nextCreature(&it);
4063 if ((pmap[monst->xLoc][monst->yLoc].flags & IN_FIELD_OF_VIEW)
4064 && (player.xLoc - monst->xLoc) * (player.xLoc - monst->xLoc) + (player.yLoc - monst->yLoc) * (player.yLoc - monst->yLoc) <= distance * distance) {
4065
4066 if (!(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
4067 if (canSeeMonster(monst)) {
4068 flashMonster(monst, &discordColor, 100);
4069 }
4070 monst->status[STATUS_DISCORDANT] = monst->maxStatus[STATUS_DISCORDANT] = 30;
4071 }
4072 }
4073 }
4074 }
4075
crystalize(short radius)4076 void crystalize(short radius) {
4077 extern color forceFieldColor;
4078 short i, j;
4079 creature *monst;
4080
4081 for (i=0; i<DCOLS; i++) {
4082 for (j=0; j < DROWS; j++) {
4083 if ((player.xLoc - i) * (player.xLoc - i) + (player.yLoc - j) * (player.yLoc - j) <= radius * radius
4084 && !(pmap[i][j].flags & IMPREGNABLE)) {
4085
4086 if (i == 0 || i == DCOLS - 1 || j == 0 || j == DROWS - 1) {
4087 pmap[i][j].layers[DUNGEON] = CRYSTAL_WALL; // don't dissolve the boundary walls
4088 } else if (tileCatalog[pmap[i][j].layers[DUNGEON]].flags & (T_OBSTRUCTS_PASSABILITY | T_OBSTRUCTS_VISION)) {
4089
4090 pmap[i][j].layers[DUNGEON] = FORCEFIELD;
4091 spawnDungeonFeature(i, j, &dungeonFeatureCatalog[DF_SHATTERING_SPELL], true, false);
4092
4093 if (pmap[i][j].flags & HAS_MONSTER) {
4094 monst = monsterAtLoc(i, j);
4095 if (monst->info.flags & MONST_ATTACKABLE_THRU_WALLS) {
4096 inflictLethalDamage(NULL, monst);
4097 } else {
4098 freeCaptivesEmbeddedAt(i, j);
4099 }
4100 }
4101 }
4102 }
4103 }
4104 }
4105 updateVision(false);
4106 colorFlash(&forceFieldColor, 0, 0, radius, radius, player.xLoc, player.yLoc);
4107 displayLevel();
4108 refreshSideBar(-1, -1, false);
4109 }
4110
imbueInvisibility(creature * monst,short duration)4111 boolean imbueInvisibility(creature *monst, short duration) {
4112 boolean autoID = false;
4113
4114 if (monst && !(monst->info.flags & (MONST_INANIMATE | MONST_INVISIBLE | MONST_INVULNERABLE))) {
4115 if (monst == &player || monst->creatureState == MONSTER_ALLY) {
4116 autoID = true;
4117 } else if (canSeeMonster(monst) && monsterRevealed(monst)) {
4118 autoID = true;
4119 }
4120 monst->status[STATUS_INVISIBLE] = monst->maxStatus[STATUS_INVISIBLE] = duration;
4121 refreshDungeonCell(monst->xLoc, monst->yLoc);
4122 refreshSideBar(-1, -1, false);
4123 if (boltCatalog[BOLT_POLYMORPH].backColor) {
4124 flashMonster(monst, boltCatalog[BOLT_INVISIBILITY].backColor, 100);
4125 }
4126 }
4127 return autoID;
4128 }
4129
projectileReflects(creature * attacker,creature * defender)4130 boolean projectileReflects(creature *attacker, creature *defender) {
4131 short prob;
4132 fixpt netReflectionLevel;
4133
4134 // immunity armor always reflects its vorpal enemy's projectiles
4135 if (defender == &player && rogue.armor && (rogue.armor->flags & ITEM_RUNIC) && rogue.armor->enchant2 == A_IMMUNITY
4136 && monsterIsInClass(attacker, rogue.armor->vorpalEnemy)
4137 && monstersAreEnemies(attacker, defender)) {
4138
4139 return true;
4140 }
4141
4142 if (defender == &player && rogue.armor && (rogue.armor->flags & ITEM_RUNIC) && rogue.armor->enchant2 == A_REFLECTION) {
4143 netReflectionLevel = netEnchant(rogue.armor);
4144 } else {
4145 netReflectionLevel = 0;
4146 }
4147
4148 if (defender && (defender->info.flags & MONST_REFLECT_4)) {
4149 if (defender->info.flags & MONST_ALWAYS_USE_ABILITY) {
4150 return true;
4151 }
4152 netReflectionLevel += 4 * FP_FACTOR;
4153 }
4154
4155 if (netReflectionLevel <= 0) {
4156 return false;
4157 }
4158
4159 prob = reflectionChance(netReflectionLevel);
4160
4161 return rand_percent(prob);
4162 }
4163
4164 // Alters listOfCoordinates to describe reflected path,
4165 // which diverges from the existing path at kinkCell,
4166 // and then returns the path length of the reflected path.
reflectBolt(short targetX,short targetY,short listOfCoordinates[][2],short kinkCell,boolean retracePath)4167 short reflectBolt(short targetX, short targetY, short listOfCoordinates[][2], short kinkCell, boolean retracePath) {
4168 short k, target[2], origin[2], newPath[DCOLS][2], newPathLength, failsafe, finalLength;
4169 boolean needRandomTarget;
4170
4171 needRandomTarget = (targetX < 0 || targetY < 0
4172 || (targetX == listOfCoordinates[kinkCell][0] && targetY == listOfCoordinates[kinkCell][1]));
4173
4174 if (retracePath) {
4175 // if reflecting back at caster, follow precise trajectory until we reach the caster
4176 for (k = 1; k <= kinkCell && kinkCell + k < MAX_BOLT_LENGTH; k++) {
4177 listOfCoordinates[kinkCell + k][0] = listOfCoordinates[kinkCell - k][0];
4178 listOfCoordinates[kinkCell + k][1] = listOfCoordinates[kinkCell - k][1];
4179 }
4180
4181 // Calculate a new "extension" path, with an origin at the caster, and a destination at
4182 // the caster's location translated by the vector from the reflection point to the caster.
4183 //
4184 // For example, if the player is at (0,0), and the caster is at (2,3), then the newpath
4185 // is from (2,3) to (4,6):
4186 // (2,3) + ((2,3) - (0,0)) = (4,6).
4187
4188 origin[0] = listOfCoordinates[2 * kinkCell][0];
4189 origin[1] = listOfCoordinates[2 * kinkCell][1];
4190 target[0] = targetX + (targetX - listOfCoordinates[kinkCell][0]);
4191 target[1] = targetY + (targetY - listOfCoordinates[kinkCell][1]);
4192
4193 // (NULL because the reflected bolt is not under the caster's control, so its path should not be tuned)
4194 newPathLength = getLineCoordinates(newPath, origin, target, NULL);
4195
4196 for (k=0; k<=newPathLength; k++) {
4197 listOfCoordinates[2 * kinkCell + k + 1][0] = newPath[k][0];
4198 listOfCoordinates[2 * kinkCell + k + 1][1] = newPath[k][1];
4199 }
4200 finalLength = 2 * kinkCell + newPathLength + 1;
4201 } else {
4202 failsafe = 50;
4203 do {
4204 if (needRandomTarget) {
4205 // pick random target
4206 perimeterCoords(target, rand_range(0, 39));
4207 target[0] += listOfCoordinates[kinkCell][0];
4208 target[1] += listOfCoordinates[kinkCell][1];
4209 } else {
4210 target[0] = targetX;
4211 target[1] = targetY;
4212 }
4213 newPathLength = getLineCoordinates(newPath, listOfCoordinates[kinkCell], target, NULL);
4214 if (newPathLength > 0
4215 && !cellHasTerrainFlag(newPath[0][0], newPath[0][1], (T_OBSTRUCTS_VISION | T_OBSTRUCTS_PASSABILITY))) {
4216
4217 needRandomTarget = false;
4218 }
4219 } while (needRandomTarget && --failsafe);
4220
4221 for (k = 0; k < newPathLength; k++) {
4222 listOfCoordinates[kinkCell + k + 1][0] = newPath[k][0];
4223 listOfCoordinates[kinkCell + k + 1][1] = newPath[k][1];
4224 }
4225
4226 finalLength = kinkCell + newPathLength + 1;
4227 }
4228
4229 listOfCoordinates[finalLength][0] = -1;
4230 listOfCoordinates[finalLength][1] = -1;
4231 return finalLength;
4232 }
4233
4234 // Update stuff that promotes without keys so players can't abuse item libraries with blinking/haste shenanigans
checkForMissingKeys(short x,short y)4235 void checkForMissingKeys(short x, short y) {
4236 short layer;
4237
4238 if (cellHasTMFlag(x, y, TM_PROMOTES_WITHOUT_KEY) && !keyOnTileAt(x, y)) {
4239 for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
4240 if (tileCatalog[pmap[x][y].layers[layer]].mechFlags & TM_PROMOTES_WITHOUT_KEY) {
4241 promoteTile(x, y, layer, false);
4242 }
4243 }
4244 }
4245 }
4246
beckonMonster(creature * monst,short x,short y)4247 void beckonMonster(creature *monst, short x, short y) {
4248 short from[2], to[2];
4249 bolt theBolt = boltCatalog[BOLT_BLINKING];
4250
4251 if (monst->bookkeepingFlags & MB_CAPTIVE) {
4252 freeCaptive(monst);
4253 }
4254 from[0] = monst->xLoc;
4255 from[1] = monst->yLoc;
4256 to[0] = x;
4257 to[1] = y;
4258 theBolt.magnitude = max(1, (distanceBetween(x, y, monst->xLoc, monst->yLoc) - 2) / 2);
4259 zap(from, to, &theBolt, false);
4260 if (monst->ticksUntilTurn < player.attackSpeed+1) {
4261 monst->ticksUntilTurn = player.attackSpeed+1;
4262 }
4263 }
4264
boltEffectForItem(item * theItem)4265 enum boltEffects boltEffectForItem(item *theItem) {
4266 if (theItem->category & (STAFF | WAND)) {
4267 return boltCatalog[tableForItemCategory(theItem->category, NULL)[theItem->kind].strengthRequired].boltEffect;
4268 } else {
4269 return BE_NONE;
4270 }
4271 }
4272
boltForItem(item * theItem)4273 enum boltType boltForItem(item *theItem) {
4274 if (theItem->category & (STAFF | WAND)) {
4275 return tableForItemCategory(theItem->category, NULL)[theItem->kind].strengthRequired;
4276 } else {
4277 return 0;
4278 }
4279 }
4280
4281 // Called on each space of the bolt's flight.
4282 // Returns true if the bolt terminates here.
4283 // Caster can be null.
4284 // Pass in true for boltInView if any part of the bolt is currently visible to the player.
4285 // Pass in true for alreadyReflected if the bolt has already reflected off of something.
4286 // If the effect is visible enough for the player to identify the shooting item,
4287 // *autoID will be set to true. (AutoID can be null.)
4288 // If the effect causes the level's lighting or vision to change, *lightingChanged
4289 // will be set to true. (LightingChanged can be null.)
updateBolt(bolt * theBolt,creature * caster,short x,short y,boolean boltInView,boolean alreadyReflected,boolean * autoID,boolean * lightingChanged)4290 boolean updateBolt(bolt *theBolt, creature *caster, short x, short y,
4291 boolean boltInView, boolean alreadyReflected,
4292 boolean *autoID, boolean *lightingChanged) {
4293 char buf[COLS], monstName[COLS];
4294 creature *monst; // Creature being hit by the bolt, if any.
4295 creature *newMonst; // Utility variable for plenty
4296 boolean terminateBolt = false;
4297 boolean negated = false;
4298
4299 if (lightingChanged) {
4300 *lightingChanged = false;
4301 }
4302
4303 // Handle collisions with monsters.
4304
4305 monst = monsterAtLoc(x, y);
4306 if (monst && !(monst->bookkeepingFlags & MB_SUBMERGED)) {
4307 monsterName(monstName, monst, true);
4308
4309 switch(theBolt->boltEffect) {
4310 case BE_ATTACK:
4311 if (!cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY)
4312 || (monst->info.flags & MONST_ATTACKABLE_THRU_WALLS)) {
4313
4314 attack(caster, monst, false);
4315 if (autoID) {
4316 *autoID = true;
4317 }
4318 }
4319 break;
4320 case BE_DAMAGE:
4321 if (autoID) {
4322 *autoID = true;
4323 }
4324 if (((theBolt->flags & BF_FIERY) && monst->status[STATUS_IMMUNE_TO_FIRE] > 0)
4325 || (monst->info.flags & MONST_INVULNERABLE)) {
4326
4327 if (canSeeMonster(monst)) {
4328 sprintf(buf, "%s ignore%s %s %s",
4329 monstName,
4330 (monst == &player ? "" : "s"),
4331 canSeeMonster(caster) ? "the" : "a",
4332 theBolt->name);
4333 combatMessage(buf, 0);
4334 }
4335 } else if (inflictDamage(caster, monst, staffDamage(theBolt->magnitude * FP_FACTOR), theBolt->backColor, false)) {
4336 // killed monster
4337 if (player.currentHP <= 0) {
4338 if (caster == &player) {
4339 sprintf(buf, "Killed by a reflected %s", theBolt->name);
4340 gameOver(buf, true);
4341 }
4342 terminateBolt = true;
4343 return true;
4344 }
4345 if (boltInView || canSeeMonster(monst)) {
4346 sprintf(buf, "%s %s %s %s",
4347 canSeeMonster(caster) ? "the" : "a",
4348 theBolt->name,
4349 ((monst->info.flags & MONST_INANIMATE) ? "destroys" : "kills"),
4350 monstName);
4351 combatMessage(buf, messageColorFromVictim(monst));
4352 } else {
4353 sprintf(buf, "you hear %s %s", monstName, ((monst->info.flags & MONST_INANIMATE) ? "get destroyed" : "die"));
4354 combatMessage(buf, 0);
4355 }
4356 } else {
4357 // monster lives
4358 if (monst->creatureMode != MODE_PERM_FLEEING
4359 && monst->creatureState != MONSTER_ALLY
4360 && (monst->creatureState != MONSTER_FLEEING || monst->status[STATUS_MAGICAL_FEAR])) {
4361
4362 monst->creatureState = MONSTER_TRACKING_SCENT;
4363 monst->status[STATUS_MAGICAL_FEAR] = 0;
4364 }
4365 if (boltInView) {
4366 sprintf(buf, "%s %s hits %s",
4367 canSeeMonster(caster) ? "the" : "a",
4368 theBolt->name,
4369 monstName);
4370 combatMessage(buf, messageColorFromVictim(monst));
4371 }
4372 if (theBolt->flags & BF_FIERY) {
4373 exposeCreatureToFire(monst);
4374 }
4375 if (!alreadyReflected
4376 || caster != &player) {
4377 moralAttack(caster, monst);
4378 }
4379 }
4380 if (theBolt->flags & BF_FIERY) {
4381 exposeTileToFire(x, y, true); // burninate
4382 }
4383 break;
4384 case BE_TELEPORT:
4385 if (!(monst->info.flags & MONST_IMMOBILE)) {
4386 if (monst->bookkeepingFlags & MB_CAPTIVE) {
4387 freeCaptive(monst);
4388 }
4389 teleport(monst, -1, -1, false);
4390 }
4391 break;
4392 case BE_BECKONING:
4393 if (!(monst->info.flags & MONST_IMMOBILE)
4394 && caster
4395 && distanceBetween(caster->xLoc, caster->yLoc, monst->xLoc, monst->yLoc) > 1) {
4396
4397 if (canSeeMonster(monst) && autoID) {
4398 *autoID = true;
4399 }
4400 beckonMonster(monst, caster->xLoc, caster->yLoc);
4401 if (canSeeMonster(monst) && autoID) {
4402 *autoID = true;
4403 }
4404 }
4405 break;
4406 case BE_SLOW:
4407 slow(monst, theBolt->magnitude * 5);
4408 if (boltCatalog[BOLT_SLOW].backColor) {
4409 flashMonster(monst, boltCatalog[BOLT_SLOW].backColor, 100);
4410 }
4411 if (autoID) {
4412 *autoID = true;
4413 }
4414 break;
4415 case BE_HASTE:
4416 haste(monst, staffHasteDuration(theBolt->magnitude * FP_FACTOR));
4417 if (boltCatalog[BOLT_HASTE].backColor) {
4418 flashMonster(monst, boltCatalog[BOLT_HASTE].backColor, 100);
4419 }
4420 if (autoID) {
4421 *autoID = true;
4422 }
4423 break;
4424 case BE_POLYMORPH:
4425 if (polymorph(monst)) {
4426 if (!monst->status[STATUS_INVISIBLE]) {
4427 if (autoID) {
4428 *autoID = true;
4429 }
4430 }
4431 }
4432 break;
4433 case BE_INVISIBILITY:
4434 if (imbueInvisibility(monst, 150) && autoID) {
4435 *autoID = true;
4436 }
4437 break;
4438 case BE_DOMINATION:
4439 if (monst != &player && !(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
4440 if (rand_percent(wandDominate(monst))) {
4441 // domination succeeded
4442 monst->status[STATUS_DISCORDANT] = 0;
4443 becomeAllyWith(monst);
4444 //refreshSideBar(-1, -1, false);
4445 refreshDungeonCell(monst->xLoc, monst->yLoc);
4446 if (canSeeMonster(monst)) {
4447 if (autoID) {
4448 *autoID = true;
4449 }
4450 sprintf(buf, "%s is bound to your will!", monstName);
4451 message(buf, 0);
4452 if (boltCatalog[BOLT_DOMINATION].backColor) {
4453 flashMonster(monst, boltCatalog[BOLT_DOMINATION].backColor, 100);
4454 }
4455 }
4456 } else if (canSeeMonster(monst)) {
4457 if (autoID) {
4458 *autoID = true;
4459 }
4460 sprintf(buf, "%s resists the bolt of domination.", monstName);
4461 message(buf, 0);
4462 }
4463 }
4464 break;
4465 case BE_NEGATION:
4466 negated = negate(monst);
4467 if (boltCatalog[BOLT_NEGATION].backColor) {
4468 flashMonster(monst, boltCatalog[BOLT_NEGATION].backColor, 100);
4469 }
4470 if (negated && autoID && canSeeMonster(monst)) {
4471 *autoID = true;
4472 }
4473
4474 break;
4475 case BE_EMPOWERMENT:
4476 if (monst != &player
4477 && !(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
4478
4479 empowerMonster(monst);
4480 createFlare(monst->xLoc, monst->yLoc, EMPOWERMENT_LIGHT);
4481 if (canSeeMonster(monst) && autoID) {
4482 *autoID = true;
4483 }
4484 }
4485 break;
4486 case BE_POISON:
4487 if (!(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
4488 addPoison(monst, staffPoison(theBolt->magnitude * FP_FACTOR), 1);
4489 if (canSeeMonster(monst)) {
4490 if (boltCatalog[BOLT_POISON].backColor) {
4491 flashMonster(monst, boltCatalog[BOLT_POISON].backColor, 100);
4492 }
4493 if (autoID) {
4494 *autoID = true;
4495 }
4496 if (monst != &player) {
4497 sprintf(buf, "%s %s %s sick",
4498 monstName,
4499 (monst == &player ? "feel" : "looks"),
4500 (monst->status[STATUS_POISONED] * monst->poisonAmount >= monst->currentHP && !player.status[STATUS_HALLUCINATING] ? "fatally" : "very"));
4501 combatMessage(buf, messageColorFromVictim(monst));
4502 }
4503 }
4504 }
4505 break;
4506 case BE_ENTRANCEMENT:
4507 if (monst == &player) {
4508 flashMonster(monst, &confusionGasColor, 100);
4509 monst->status[STATUS_CONFUSED] = staffEntrancementDuration(theBolt->magnitude * FP_FACTOR);
4510 monst->maxStatus[STATUS_CONFUSED] = max(monst->status[STATUS_CONFUSED], monst->maxStatus[STATUS_CONFUSED]);
4511 message("the bolt hits you and you suddenly feel disoriented.", REQUIRE_ACKNOWLEDGMENT);
4512 if (autoID) {
4513 *autoID = true;
4514 }
4515 } else if (!(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
4516 monst->status[STATUS_ENTRANCED] = monst->maxStatus[STATUS_ENTRANCED] = staffEntrancementDuration(theBolt->magnitude * FP_FACTOR);
4517 wakeUp(monst);
4518 if (canSeeMonster(monst)) {
4519 if (boltCatalog[BOLT_ENTRANCEMENT].backColor) {
4520 flashMonster(monst, boltCatalog[BOLT_ENTRANCEMENT].backColor, 100);
4521 }
4522 if (autoID) {
4523 *autoID = true;
4524 }
4525 sprintf(buf, "%s is entranced!", monstName);
4526 message(buf, 0);
4527 }
4528 }
4529 break;
4530 case BE_HEALING:
4531 heal(monst, theBolt->magnitude * 10, false);
4532 if (canSeeMonster(monst)) {
4533 if (autoID) {
4534 *autoID = true;
4535 }
4536 }
4537 break;
4538 case BE_PLENTY:
4539 if (!(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
4540 newMonst = cloneMonster(monst, true, true);
4541 if (newMonst) {
4542 monst->info.maxHP = newMonst->info.maxHP = (monst->info.maxHP + 1) / 2;
4543 monst->currentHP = newMonst->currentHP = min(monst->currentHP, monst->info.maxHP);
4544 if (boltCatalog[BOLT_PLENTY].backColor) {
4545 flashMonster(monst, boltCatalog[BOLT_PLENTY].backColor, 100);
4546 flashMonster(newMonst, boltCatalog[BOLT_PLENTY].backColor, 100);
4547 }
4548 if (autoID) {
4549 *autoID = true;
4550 }
4551 }
4552 }
4553 break;
4554 case BE_DISCORD:
4555 if (!(monst->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
4556 monst->status[STATUS_DISCORDANT] = monst->maxStatus[STATUS_DISCORDANT] = max(staffDiscordDuration(theBolt->magnitude * FP_FACTOR),
4557 monst->status[STATUS_DISCORDANT]);
4558 if (canSeeMonster(monst)) {
4559 if (boltCatalog[BOLT_DISCORD].backColor) {
4560 flashMonster(monst, boltCatalog[BOLT_DISCORD].backColor, 100);
4561 }
4562 if (autoID) {
4563 *autoID = true;
4564 }
4565 }
4566 }
4567 break;
4568 case BE_SHIELDING:
4569 if (staffProtection(theBolt->magnitude * FP_FACTOR) > monst->status[STATUS_SHIELDED]) {
4570 monst->status[STATUS_SHIELDED] = staffProtection(theBolt->magnitude * FP_FACTOR);
4571 }
4572 monst->maxStatus[STATUS_SHIELDED] = monst->status[STATUS_SHIELDED];
4573 if (boltCatalog[BOLT_SHIELDING].backColor) {
4574 flashMonster(monst, boltCatalog[BOLT_SHIELDING].backColor, 100);
4575 }
4576 if (autoID) {
4577 *autoID = true;
4578 }
4579 break;
4580 default:
4581 break;
4582 }
4583
4584 if (!(theBolt->flags & BF_PASSES_THRU_CREATURES)) {
4585 terminateBolt = true;
4586 }
4587 }
4588
4589 // Handle ordinary bolt updates that aren't dependent on hitting a creature.
4590 switch (theBolt->boltEffect) {
4591 case BE_BLINKING:
4592 if (caster == &player) {
4593 player.xLoc = x;
4594 player.yLoc = y;
4595 if (lightingChanged) {
4596 *lightingChanged = true;
4597 }
4598 }
4599 break;
4600 default:
4601 break;
4602 }
4603
4604 if (theBolt->pathDF) {
4605 spawnDungeonFeature(x, y, &dungeonFeatureCatalog[theBolt->pathDF], true, false);
4606 }
4607
4608 if ((theBolt->flags & BF_FIERY)
4609 && exposeTileToFire(x, y, true)) {
4610
4611 if (lightingChanged) {
4612 *lightingChanged = true;
4613 }
4614 if (autoID) {
4615 *autoID = true;
4616 }
4617 }
4618
4619 if ((theBolt->flags & BF_ELECTRIC)
4620 && exposeTileToElectricity(x, y)) {
4621
4622 if (lightingChanged) {
4623 *lightingChanged = true;
4624 }
4625 if (autoID) {
4626 *autoID = true;
4627 }
4628 }
4629
4630 return terminateBolt;
4631 }
4632
4633 // Called when the bolt hits something.
4634 // Caster can be null.
4635 // Pass in true for alreadyReflected if the bolt has already reflected off of something.
4636 // If the effect is visible enough for the player to identify the shooting item,
4637 // *autoID will be set to true. (AutoID can be null.)
detonateBolt(bolt * theBolt,creature * caster,short x,short y,boolean * autoID)4638 void detonateBolt(bolt *theBolt, creature *caster, short x, short y, boolean *autoID) {
4639 dungeonFeature feat;
4640 short i, x2, y2;
4641 creature *monst;
4642
4643 const fixpt POW_OBSTRUCTION[] = {
4644 // 0.8^x, with x from 2 to 40:
4645 41943, 33554, 26843, 21474, 17179, 13743, 10995, 8796, 7036, 5629, 4503, 3602,
4646 2882, 2305, 1844, 1475, 1180, 944, 755, 604, 483, 386, 309, 247, 198, 158, 126,
4647 101, 81, 64, 51, 41, 33, 26, 21, 17, 13, 10, 8, 6, 5};
4648
4649 switch(theBolt->boltEffect) {
4650 case BE_OBSTRUCTION:
4651 feat = dungeonFeatureCatalog[DF_FORCEFIELD];
4652 feat.probabilityDecrement = max(1, 75 * POW_OBSTRUCTION[min(40, theBolt->magnitude) - 2] / FP_FACTOR);
4653 spawnDungeonFeature(x, y, &feat, true, false);
4654 if (autoID) {
4655 *autoID = true;
4656 }
4657 break;
4658 case BE_CONJURATION:
4659 for (i = 0; i < (staffBladeCount(theBolt->magnitude * FP_FACTOR)); i++) {
4660 monst = generateMonster(MK_SPECTRAL_BLADE, true, false);
4661 getQualifyingPathLocNear(&(monst->xLoc), &(monst->yLoc), x, y, true,
4662 T_DIVIDES_LEVEL & avoidedFlagsForMonster(&(monst->info)) & ~T_SPONTANEOUSLY_IGNITES, HAS_PLAYER,
4663 avoidedFlagsForMonster(&(monst->info)) & ~T_SPONTANEOUSLY_IGNITES, (HAS_PLAYER | HAS_MONSTER | HAS_STAIRS), false);
4664 monst->bookkeepingFlags |= (MB_FOLLOWER | MB_BOUND_TO_LEADER | MB_DOES_NOT_TRACK_LEADER);
4665 monst->bookkeepingFlags &= ~MB_JUST_SUMMONED;
4666 monst->leader = &player;
4667 monst->creatureState = MONSTER_ALLY;
4668 monst->ticksUntilTurn = monst->info.attackSpeed + 1; // So they don't move before the player's next turn.
4669 pmap[monst->xLoc][monst->yLoc].flags |= HAS_MONSTER;
4670 //refreshDungeonCell(monst->xLoc, monst->yLoc);
4671 fadeInMonster(monst);
4672 }
4673 updateVision(true);
4674 //refreshSideBar(-1, -1, false);
4675 monst = NULL;
4676 if (autoID) {
4677 *autoID = true;
4678 }
4679 break;
4680 case BE_BLINKING:
4681 if (pmap[x][y].flags & HAS_MONSTER) { // We're blinking onto an area already occupied by a submerged monster.
4682 // Make sure we don't get the shooting monster by accident.
4683 caster->xLoc = caster->yLoc = -1; // Will be set back to the destination in a moment.
4684 monst = monsterAtLoc(x, y);
4685 findAlternativeHomeFor(monst, &x2, &y2, true);
4686 if (x2 >= 0) {
4687 // Found an alternative location.
4688 monst->xLoc = x2;
4689 monst->yLoc = y2;
4690 pmap[x][y].flags &= ~HAS_MONSTER;
4691 pmap[x2][y2].flags |= HAS_MONSTER;
4692 } else {
4693 // No alternative location?? Hard to imagine how this could happen.
4694 // Just bury the monster and never speak of this incident again.
4695 killCreature(monst, true);
4696 pmap[x][y].flags &= ~HAS_MONSTER;
4697 monst = NULL;
4698 }
4699 }
4700 caster->bookkeepingFlags &= ~MB_SUBMERGED;
4701 pmap[x][y].flags |= (caster == &player ? HAS_PLAYER : HAS_MONSTER);
4702 caster->xLoc = x;
4703 caster->yLoc = y;
4704 // Always break free on blink
4705 disentangle(caster);
4706 applyInstantTileEffectsToCreature(caster);
4707 if (caster == &player) {
4708 // increase scent turn number so monsters don't sniff around at the old cell like idiots
4709 rogue.scentTurnNumber += 30;
4710 // get any items at the destination location
4711 if (pmap[player.xLoc][player.yLoc].flags & HAS_ITEM) {
4712 pickUpItemAt(player.xLoc, player.yLoc);
4713 }
4714 updateVision(true);
4715 }
4716 if (autoID) {
4717 *autoID = true;
4718 }
4719 break;
4720 case BE_TUNNELING:
4721 setUpWaypoints(); // Recompute waypoints based on the new situation.
4722 break;
4723 }
4724
4725 if (theBolt->targetDF) {
4726 spawnDungeonFeature(x, y, &dungeonFeatureCatalog[theBolt->targetDF], true, false);
4727 }
4728 }
4729
4730 // returns whether the bolt effect should autoID any staff or wand it came from, if it came from a staff or wand
zap(short originLoc[2],short targetLoc[2],bolt * theBolt,boolean hideDetails)4731 boolean zap(short originLoc[2], short targetLoc[2], bolt *theBolt, boolean hideDetails) {
4732 short listOfCoordinates[MAX_BOLT_LENGTH][2];
4733 short i, j, k, x, y, x2, y2, numCells, blinkDistance = 0, boltLength, initialBoltLength, lights[DCOLS][DROWS][3];
4734 creature *monst = NULL, *shootingMonst;
4735 char buf[COLS], monstName[COLS];
4736 boolean autoID = false;
4737 boolean lightingChanged = false;
4738 boolean fastForward = false;
4739 boolean alreadyReflected = false;
4740 boolean boltInView;
4741 const color *boltColor;
4742 fixpt boltLightRadius;
4743
4744 enum displayGlyph theChar;
4745 color foreColor, backColor, multColor;
4746
4747 lightSource boltLights[500];
4748 color boltLightColors[500];
4749
4750 brogueAssert(originLoc[0] != targetLoc[0] || originLoc[1] != targetLoc[1]);
4751 if (originLoc[0] == targetLoc[0] && originLoc[1] == targetLoc[1]) {
4752 return false;
4753 }
4754
4755 x = originLoc[0];
4756 y = originLoc[1];
4757
4758 initialBoltLength = boltLength = 5 * theBolt->magnitude;
4759 numCells = getLineCoordinates(listOfCoordinates, originLoc, targetLoc, (hideDetails ? &boltCatalog[BOLT_NONE] : theBolt));
4760 shootingMonst = monsterAtLoc(originLoc[0], originLoc[1]);
4761
4762 if (hideDetails) {
4763 boltColor = &gray;
4764 } else {
4765 boltColor = theBolt->backColor;
4766 }
4767
4768 refreshSideBar(-1, -1, false);
4769 displayCombatText(); // To announce who fired the bolt while the animation plays.
4770
4771 if (theBolt->boltEffect == BE_BLINKING) {
4772 if (cellHasTerrainFlag(listOfCoordinates[0][0], listOfCoordinates[0][1], (T_OBSTRUCTS_PASSABILITY | T_OBSTRUCTS_VISION))
4773 || ((pmap[listOfCoordinates[0][0]][listOfCoordinates[0][1]].flags & (HAS_PLAYER | HAS_MONSTER))
4774 && !(monsterAtLoc(listOfCoordinates[0][0], listOfCoordinates[0][1])->bookkeepingFlags & MB_SUBMERGED))) {
4775 // shooting blink point-blank into an obstruction does nothing.
4776 return false;
4777 }
4778 theBolt->foreColor = &black;
4779 theBolt->theChar = shootingMonst->info.displayChar;
4780 pmap[originLoc[0]][originLoc[1]].flags &= ~(HAS_PLAYER | HAS_MONSTER);
4781 refreshDungeonCell(originLoc[0], originLoc[1]);
4782 blinkDistance = theBolt->magnitude * 2 + 1;
4783 checkForMissingKeys(originLoc[0], originLoc[1]);
4784 }
4785
4786 if (boltColor) {
4787 for (i=0; i<initialBoltLength; i++) {
4788 boltLightColors[i] = *boltColor;
4789 boltLights[i] = lightCatalog[BOLT_LIGHT_SOURCE];
4790 boltLights[i].lightColor = &boltLightColors[i];
4791 boltLightRadius = 50LL * ((3 * FP_FACTOR) + (theBolt->magnitude * FP_FACTOR) * 4/3) * (initialBoltLength - i) / initialBoltLength / FP_FACTOR;
4792 boltLights[i].lightRadius.lowerBound = boltLights[i].lightRadius.upperBound = boltLightRadius;
4793 //boltLights[i].lightRadius.lowerBound = boltLights[i].lightRadius.upperBound = 50 * (3 + theBolt->magnitude * 1.33) * (initialBoltLength - i) / initialBoltLength;
4794 //printf("\nStandard: %i, attempted new: %lli", boltLights[i].lightRadius.lowerBound, boltLightRadius);
4795 }
4796 }
4797
4798 if (theBolt->boltEffect == BE_TUNNELING) {
4799 tunnelize(originLoc[0], originLoc[1]);
4800 }
4801
4802 backUpLighting(lights);
4803 boltInView = true;
4804 for (i=0; i<numCells; i++) {
4805
4806 x = listOfCoordinates[i][0];
4807 y = listOfCoordinates[i][1];
4808
4809 monst = monsterAtLoc(x, y);
4810
4811 // Handle bolt reflection off of creatures (reflection off of terrain is handled further down).
4812 if (monst
4813 && !(theBolt->flags & BF_NEVER_REFLECTS)
4814 && projectileReflects(shootingMonst, monst)
4815 && i < MAX_BOLT_LENGTH - max(DCOLS, DROWS)) {
4816
4817 if (projectileReflects(shootingMonst, monst)) { // if it scores another reflection roll, reflect at caster
4818 numCells = reflectBolt(originLoc[0], originLoc[1], listOfCoordinates, i, !alreadyReflected);
4819 } else {
4820 numCells = reflectBolt(-1, -1, listOfCoordinates, i, false); // otherwise reflect randomly
4821 }
4822
4823 alreadyReflected = true;
4824
4825 if (boltInView) {
4826 monsterName(monstName, monst, true);
4827 sprintf(buf, "%s deflect%s the %s",
4828 monstName,
4829 (monst == &player ? "" : "s"),
4830 hideDetails ? "bolt" : theBolt->name);
4831 combatMessage(buf, 0);
4832 }
4833 if (monst == &player
4834 && rogue.armor
4835 && rogue.armor->enchant2 == A_REFLECTION
4836 && !(rogue.armor->flags & ITEM_RUNIC_IDENTIFIED)) {
4837
4838 autoIdentify(rogue.armor);
4839 }
4840 continue;
4841 }
4842
4843 if (updateBolt(theBolt, shootingMonst, x, y, boltInView, alreadyReflected, &autoID, &lightingChanged)) {
4844 break;
4845 }
4846
4847 if (lightingChanged) {
4848 updateVision(true);
4849 backUpLighting(lights);
4850 }
4851
4852 // Update the visual effect of the bolt.
4853 // First do lighting. This lighting effect is expensive; do it only if the player can see the bolt.
4854 if (boltInView && boltColor) {
4855 demoteVisibility();
4856 restoreLighting(lights);
4857 for (k = min(i, boltLength + 2); k >= 0; k--) {
4858 if (k < initialBoltLength) {
4859 paintLight(&boltLights[k], listOfCoordinates[i-k][0], listOfCoordinates[i-k][1], false, false);
4860 }
4861 }
4862 }
4863 boltInView = false;
4864 updateFieldOfViewDisplay(false, true);
4865 // Now draw the bolt itself.
4866 for (k = min(i, boltLength + 2); k >= 0; k--) {
4867 x2 = listOfCoordinates[i-k][0];
4868 y2 = listOfCoordinates[i-k][1];
4869 if (playerCanSeeOrSense(x2, y2)) {
4870 if (!fastForward) {
4871 getCellAppearance(x2, y2, &theChar, &foreColor, &backColor);
4872 if (boltColor) {
4873 applyColorAugment(&foreColor, boltColor, max(0, 100 - k * 100 / (boltLength)));
4874 applyColorAugment(&backColor, boltColor, max(0, 100 - k * 100 / (boltLength)));
4875 }
4876 const boolean displayChar = (k == 0 || (theBolt->flags & BF_DISPLAY_CHAR_ALONG_LENGTH));
4877 if (displayChar) {
4878 if (theBolt->foreColor) {
4879 foreColor = *(theBolt->foreColor);
4880 }
4881 if (theBolt->theChar) {
4882 theChar = theBolt->theChar;
4883 }
4884 }
4885 if (displayChar
4886 && theBolt->foreColor
4887 && theBolt->theChar) {
4888
4889 colorMultiplierFromDungeonLight(x2, y2, &multColor);
4890 applyColorMultiplier(&foreColor, &multColor);
4891 plotCharWithColor(theChar, mapToWindowX(x2), mapToWindowY(y2), &foreColor, &backColor);
4892 } else if (boltColor) {
4893 plotCharWithColor(theChar, mapToWindowX(x2), mapToWindowY(y2), &foreColor, &backColor);
4894 } else if (k == 1
4895 && theBolt->foreColor
4896 && theBolt->theChar) {
4897
4898 refreshDungeonCell(x2, y2); // Clean up the contrail so it doesn't leave a trail of characters.
4899 }
4900 }
4901 if (playerCanSee(x2, y2)) {
4902 // Don't want to let omniscience mode affect boltInView; causes OOS.
4903 boltInView = true;
4904 }
4905 }
4906 }
4907 if (!fastForward && (boltInView || rogue.playbackOmniscience)) {
4908 fastForward = rogue.playbackFastForward || pauseBrogue(16);
4909 }
4910
4911 if (theBolt->boltEffect == BE_BLINKING) {
4912 theBolt->magnitude = (blinkDistance - i) / 2 + 1;
4913 boltLength = theBolt->magnitude * 5;
4914 for (j=0; j<i; j++) {
4915 refreshDungeonCell(listOfCoordinates[j][0], listOfCoordinates[j][1]);
4916 }
4917 if (i >= blinkDistance) {
4918 break;
4919 }
4920 }
4921
4922 // Some bolts halt at the square before they hit something.
4923 if ((theBolt->flags & BF_HALTS_BEFORE_OBSTRUCTION)
4924 && i + 1 < numCells) {
4925
4926 x2 = listOfCoordinates[i+1][0];
4927 y2 = listOfCoordinates[i+1][1];
4928
4929 if (cellHasTerrainFlag(x2, y2, (T_OBSTRUCTS_VISION | T_OBSTRUCTS_PASSABILITY))) {
4930 break;
4931 }
4932
4933 if (!(theBolt->flags & BF_PASSES_THRU_CREATURES)) {
4934 monst = monsterAtLoc(listOfCoordinates[i+1][0], listOfCoordinates[i+1][1]);
4935 if (monst && !(monst->bookkeepingFlags & MB_SUBMERGED)) {
4936 break;
4937 }
4938 }
4939 }
4940
4941 // Tunnel if we hit a wall.
4942 if (cellHasTerrainFlag(x, y, (T_OBSTRUCTS_PASSABILITY | T_OBSTRUCTS_VISION))
4943 && theBolt->boltEffect == BE_TUNNELING
4944 && tunnelize(x, y)) {
4945
4946 updateVision(true);
4947 backUpLighting(lights);
4948 autoID = true;
4949 theBolt->magnitude--;
4950 boltLength = theBolt->magnitude * 5;
4951 for (j=0; j<i; j++) {
4952 refreshDungeonCell(listOfCoordinates[j][0], listOfCoordinates[j][1]);
4953 }
4954 if (theBolt->magnitude <= 0) {
4955 refreshDungeonCell(listOfCoordinates[i-1][0], listOfCoordinates[i-1][1]);
4956 refreshDungeonCell(x, y);
4957 break;
4958 }
4959 }
4960
4961 // Stop when we hit a wall.
4962 if (cellHasTerrainFlag(x, y, (T_OBSTRUCTS_PASSABILITY | T_OBSTRUCTS_VISION))) {
4963 break;
4964 }
4965
4966 // Does the bolt bounce before hitting a wall?
4967 // Can happen with a cursed deflection ring or a reflective terrain target, or when shooting a tunneling bolt into an impregnable wall.
4968 if (i + 1 < numCells
4969 && !(theBolt->flags & BF_NEVER_REFLECTS)) {
4970
4971 x2 = listOfCoordinates[i+1][0];
4972 y2 = listOfCoordinates[i+1][1];
4973 if (cellHasTerrainFlag(x2, y2, (T_OBSTRUCTS_VISION | T_OBSTRUCTS_PASSABILITY))
4974 && (projectileReflects(shootingMonst, NULL)
4975 || cellHasTMFlag(x2, y2, TM_REFLECTS_BOLTS)
4976 || (theBolt->boltEffect == BE_TUNNELING && (pmap[x2][y2].flags & IMPREGNABLE)))
4977 && i < MAX_BOLT_LENGTH - max(DCOLS, DROWS)) {
4978
4979 sprintf(buf, "the bolt reflects off of %s", tileText(x2, y2));
4980 if (projectileReflects(shootingMonst, NULL)) {
4981 // If it scores another reflection roll, reflect at caster, unless it's already reflected.
4982 numCells = reflectBolt(originLoc[0], originLoc[1], listOfCoordinates, i, !alreadyReflected);
4983 } else {
4984 numCells = reflectBolt(-1, -1, listOfCoordinates, i, false); // Otherwise reflect randomly.
4985 }
4986 alreadyReflected = true;
4987 if (boltInView) {
4988 combatMessage(buf, 0);
4989 }
4990 }
4991 }
4992 }
4993
4994 if (!fastForward) {
4995 refreshDungeonCell(x, y);
4996 if (i > 0) {
4997 refreshDungeonCell(listOfCoordinates[i-1][0], listOfCoordinates[i-1][1]);
4998 }
4999 }
5000
5001 if (pmap[x][y].flags & (HAS_MONSTER | HAS_PLAYER)) {
5002 monst = monsterAtLoc(x, y);
5003 monsterName(monstName, monst, true);
5004 } else {
5005 monst = NULL;
5006 }
5007
5008 detonateBolt(theBolt, shootingMonst, x, y, &autoID);
5009
5010 updateLighting();
5011 backUpLighting(lights);
5012 boltInView = true;
5013 refreshSideBar(-1, -1, false);
5014 if (boltLength > 0) {
5015 if (boltColor) {
5016 // j is where the front tip of the bolt would be if it hadn't collided at i
5017 for (j=i; j < i + boltLength + 2; j++) { // j can imply a bolt tip position that is off the map
5018
5019 // dynamic lighting
5020 if (boltInView) {
5021 demoteVisibility();
5022 restoreLighting(lights);
5023
5024 // k = j-i;
5025 // boltLights[k].lightRadius.lowerBound *= 2;
5026 // boltLights[k].lightRadius.upperBound *= 2;
5027 // boltLights[k].lightColor = &boltImpactColor;
5028
5029 for (k = min(j, boltLength + 2); k >= j-i; k--) {
5030 if (k < initialBoltLength) {
5031 paintLight(&boltLights[k], listOfCoordinates[j-k][0], listOfCoordinates[j-k][1], false, false);
5032 }
5033 }
5034 updateFieldOfViewDisplay(false, true);
5035 }
5036
5037 boltInView = false;
5038
5039 // beam graphic
5040 // k iterates from the tail tip of the visible portion of the bolt to the head
5041 for (k = min(j, boltLength + 2); k >= j-i; k--) {
5042 if (playerCanSee(listOfCoordinates[j-k][0], listOfCoordinates[j-k][1])) {
5043 if (boltColor) {
5044 hiliteCell(listOfCoordinates[j-k][0], listOfCoordinates[j-k][1], boltColor, max(0, 100 - k * 100 / (boltLength)), false);
5045 }
5046 boltInView = true;
5047 }
5048 }
5049
5050 if (!fastForward && boltInView) {
5051 fastForward = rogue.playbackFastForward || pauseBrogue(16);
5052 }
5053 }
5054 } else if (theBolt->flags & BF_DISPLAY_CHAR_ALONG_LENGTH) {
5055 for (j = 0; j < i; j++) {
5056 x2 = listOfCoordinates[j][0];
5057 y2 = listOfCoordinates[j][1];
5058 if (playerCanSeeOrSense(x2, y2)) {
5059 refreshDungeonCell(x2, y2);
5060 }
5061 }
5062 }
5063 }
5064 return autoID;
5065 }
5066
5067 // Relies on the sidebar entity list. If one is already selected, select the next qualifying. Otherwise, target the first qualifying.
nextTargetAfter(short * returnX,short * returnY,short targetX,short targetY,boolean targetEnemies,boolean targetAllies,boolean targetItems,boolean targetTerrain,boolean requireOpenPath,boolean reverseDirection)5068 boolean nextTargetAfter(short *returnX,
5069 short *returnY,
5070 short targetX,
5071 short targetY,
5072 boolean targetEnemies,
5073 boolean targetAllies,
5074 boolean targetItems,
5075 boolean targetTerrain,
5076 boolean requireOpenPath,
5077 boolean reverseDirection) {
5078 short i, n, targetCount, newX, newY;
5079 short selectedIndex = 0;
5080 creature *monst;
5081 item *theItem;
5082 short deduplicatedTargetList[ROWS][2];
5083
5084 targetCount = 0;
5085 for (i=0; i<ROWS; i++) {
5086 if (rogue.sidebarLocationList[i][0] != -1) {
5087 if (targetCount == 0
5088 || deduplicatedTargetList[targetCount-1][0] != rogue.sidebarLocationList[i][0]
5089 || deduplicatedTargetList[targetCount-1][1] != rogue.sidebarLocationList[i][1]) {
5090
5091 deduplicatedTargetList[targetCount][0] = rogue.sidebarLocationList[i][0];
5092 deduplicatedTargetList[targetCount][1] = rogue.sidebarLocationList[i][1];
5093 if (rogue.sidebarLocationList[i][0] == targetX
5094 && rogue.sidebarLocationList[i][1] == targetY) {
5095 selectedIndex = targetCount;
5096 }
5097 targetCount++;
5098 }
5099 }
5100 }
5101 for (i = reverseDirection ? targetCount - 1 : 0; reverseDirection ? i >= 0 : i < targetCount; reverseDirection ? i-- : i++) {
5102 n = (selectedIndex + i) % targetCount;
5103 newX = deduplicatedTargetList[n][0];
5104 newY = deduplicatedTargetList[n][1];
5105 if ((newX != player.xLoc || newY != player.yLoc)
5106 && (newX != targetX || newY != targetY)
5107 && (!requireOpenPath || openPathBetween(player.xLoc, player.yLoc, newX, newY))) {
5108
5109 brogueAssert(coordinatesAreInMap(newX, newY));
5110 brogueAssert(n >= 0 && n < targetCount);
5111 monst = monsterAtLoc(newX, newY);
5112 if (monst) {
5113 if (monstersAreEnemies(&player, monst)) {
5114 if (targetEnemies) {
5115 *returnX = newX;
5116 *returnY = newY;
5117 return true;
5118 }
5119 } else {
5120 if (targetAllies) {
5121 *returnX = newX;
5122 *returnY = newY;
5123 return true;
5124 }
5125 }
5126 }
5127 theItem = itemAtLoc(newX, newY);
5128 if (!monst && theItem && targetItems) {
5129 *returnX = newX;
5130 *returnY = newY;
5131 return true;
5132 }
5133 if (!monst && !theItem && targetTerrain) {
5134 *returnX = newX;
5135 *returnY = newY;
5136 return true;
5137 }
5138 }
5139 }
5140 return false;
5141 }
5142
5143 // Returns how far it went before hitting something.
hiliteTrajectory(short coordinateList[DCOLS][2],short numCells,boolean eraseHiliting,const bolt * theBolt,const color * hiliteColor)5144 short hiliteTrajectory(short coordinateList[DCOLS][2], short numCells, boolean eraseHiliting, const bolt *theBolt, const color *hiliteColor) {
5145 short x, y, i;
5146 creature *monst;
5147
5148 boolean isFiery = theBolt && (theBolt->flags & BF_FIERY);
5149 boolean isTunneling = theBolt && (theBolt->boltEffect == BE_TUNNELING);
5150 boolean passThroughMonsters = theBolt && (theBolt->flags & BF_PASSES_THRU_CREATURES);
5151
5152 for (i=0; i<numCells; i++) {
5153 x = coordinateList[i][0];
5154 y = coordinateList[i][1];
5155 if (eraseHiliting) {
5156 refreshDungeonCell(x, y);
5157 } else {
5158 hiliteCell(x, y, hiliteColor, 20, true);
5159 }
5160
5161 if (!(pmap[x][y].flags & DISCOVERED)) {
5162 if (isTunneling) {
5163 continue;
5164 } else {
5165 break;
5166 }
5167 } else if (!passThroughMonsters && pmap[x][y].flags & (HAS_MONSTER)
5168 && (playerCanSee(x, y) || player.status[STATUS_TELEPATHIC])) {
5169 monst = monsterAtLoc(x, y);
5170 if (!(monst->bookkeepingFlags & MB_SUBMERGED)
5171 && !monsterIsHidden(monst, &player)) {
5172
5173 i++;
5174 break;
5175 }
5176 } else if (cellHasTerrainFlag(x, y, T_IS_FLAMMABLE) && isFiery) {
5177 continue;
5178 } else if (isTunneling && cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY) && (pmap[x][y].flags & IMPREGNABLE)
5179 || !isTunneling && cellHasTerrainFlag(x, y, (T_OBSTRUCTS_VISION | T_OBSTRUCTS_PASSABILITY))) {
5180 i++;
5181 break;
5182 }
5183 }
5184 return i;
5185 }
5186
5187 // Event is optional. Returns true if the event should be executed by the parent function.
moveCursor(boolean * targetConfirmed,boolean * canceled,boolean * tabKey,short targetLoc[2],rogueEvent * event,buttonState * state,boolean colorsDance,boolean keysMoveCursor,boolean targetCanLeaveMap)5188 boolean moveCursor(boolean *targetConfirmed,
5189 boolean *canceled,
5190 boolean *tabKey,
5191 short targetLoc[2],
5192 rogueEvent *event,
5193 buttonState *state,
5194 boolean colorsDance,
5195 boolean keysMoveCursor,
5196 boolean targetCanLeaveMap) {
5197 signed long keystroke;
5198 short moveIncrement;
5199 short buttonInput;
5200 boolean cursorMovementCommand, again, movementKeystroke, sidebarHighlighted;
5201 rogueEvent theEvent;
5202 short oldRNG;
5203
5204 short *cursor = rogue.cursorLoc; // shorthand
5205
5206 cursor[0] = targetLoc[0];
5207 cursor[1] = targetLoc[1];
5208
5209 *targetConfirmed = *canceled = *tabKey = false;
5210 sidebarHighlighted = false;
5211
5212 do {
5213 again = false;
5214 cursorMovementCommand = false;
5215 movementKeystroke = false;
5216
5217 oldRNG = rogue.RNG;
5218 rogue.RNG = RNG_COSMETIC;
5219 //assureCosmeticRNG;
5220
5221 if (state) { // Also running a button loop.
5222
5223 // Update the display.
5224 overlayDisplayBuffer(state->dbuf, NULL);
5225
5226 // Get input.
5227 nextBrogueEvent(&theEvent, false, colorsDance, true);
5228
5229 // Process the input.
5230 buttonInput = processButtonInput(state, NULL, &theEvent);
5231
5232 if (buttonInput != -1) {
5233 state->buttonDepressed = state->buttonFocused = -1;
5234 drawButtonsInState(state);
5235 }
5236
5237 // Revert the display.
5238 overlayDisplayBuffer(state->rbuf, NULL);
5239
5240 } else { // No buttons to worry about.
5241 nextBrogueEvent(&theEvent, false, colorsDance, true);
5242 }
5243 restoreRNG;
5244
5245 if (theEvent.eventType == MOUSE_UP || theEvent.eventType == MOUSE_ENTERED_CELL) {
5246 if (theEvent.param1 >= 0
5247 && theEvent.param1 < mapToWindowX(0)
5248 && theEvent.param2 >= 0
5249 && theEvent.param2 < ROWS - 1
5250 && rogue.sidebarLocationList[theEvent.param2][0] > -1) {
5251
5252 // If the cursor is on an entity in the sidebar.
5253 cursor[0] = rogue.sidebarLocationList[theEvent.param2][0];
5254 cursor[1] = rogue.sidebarLocationList[theEvent.param2][1];
5255 sidebarHighlighted = true;
5256 cursorMovementCommand = true;
5257 refreshSideBar(cursor[0], cursor[1], false);
5258 if (theEvent.eventType == MOUSE_UP) {
5259 *targetConfirmed = true;
5260 }
5261 } else if (coordinatesAreInMap(windowToMapX(theEvent.param1), windowToMapY(theEvent.param2))
5262 || targetCanLeaveMap && theEvent.eventType != MOUSE_UP) {
5263
5264 // If the cursor is in the map area, or is allowed to leave the map and it isn't a click.
5265 if (theEvent.eventType == MOUSE_UP
5266 && !theEvent.shiftKey
5267 && (theEvent.controlKey || (cursor[0] == windowToMapX(theEvent.param1) && cursor[1] == windowToMapY(theEvent.param2)))) {
5268
5269 *targetConfirmed = true;
5270 }
5271 cursor[0] = windowToMapX(theEvent.param1);
5272 cursor[1] = windowToMapY(theEvent.param2);
5273 cursorMovementCommand = true;
5274 } else {
5275 cursorMovementCommand = false;
5276 again = theEvent.eventType != MOUSE_UP;
5277 }
5278 } else if (theEvent.eventType == KEYSTROKE) {
5279 keystroke = theEvent.param1;
5280 moveIncrement = ( (theEvent.controlKey || theEvent.shiftKey) ? 5 : 1 );
5281 stripShiftFromMovementKeystroke(&keystroke);
5282 switch(keystroke) {
5283 case LEFT_ARROW:
5284 case LEFT_KEY:
5285 case NUMPAD_4:
5286 if (keysMoveCursor && cursor[0] > 0) {
5287 cursor[0] -= moveIncrement;
5288 }
5289 cursorMovementCommand = movementKeystroke = keysMoveCursor;
5290 break;
5291 case RIGHT_ARROW:
5292 case RIGHT_KEY:
5293 case NUMPAD_6:
5294 if (keysMoveCursor && cursor[0] < DCOLS - 1) {
5295 cursor[0] += moveIncrement;
5296 }
5297 cursorMovementCommand = movementKeystroke = keysMoveCursor;
5298 break;
5299 case UP_ARROW:
5300 case UP_KEY:
5301 case NUMPAD_8:
5302 if (keysMoveCursor && cursor[1] > 0) {
5303 cursor[1] -= moveIncrement;
5304 }
5305 cursorMovementCommand = movementKeystroke = keysMoveCursor;
5306 break;
5307 case DOWN_ARROW:
5308 case DOWN_KEY:
5309 case NUMPAD_2:
5310 if (keysMoveCursor && cursor[1] < DROWS - 1) {
5311 cursor[1] += moveIncrement;
5312 }
5313 cursorMovementCommand = movementKeystroke = keysMoveCursor;
5314 break;
5315 case UPLEFT_KEY:
5316 case NUMPAD_7:
5317 if (keysMoveCursor && cursor[0] > 0 && cursor[1] > 0) {
5318 cursor[0] -= moveIncrement;
5319 cursor[1] -= moveIncrement;
5320 }
5321 cursorMovementCommand = movementKeystroke = keysMoveCursor;
5322 break;
5323 case UPRIGHT_KEY:
5324 case NUMPAD_9:
5325 if (keysMoveCursor && cursor[0] < DCOLS - 1 && cursor[1] > 0) {
5326 cursor[0] += moveIncrement;
5327 cursor[1] -= moveIncrement;
5328 }
5329 cursorMovementCommand = movementKeystroke = keysMoveCursor;
5330 break;
5331 case DOWNLEFT_KEY:
5332 case NUMPAD_1:
5333 if (keysMoveCursor && cursor[0] > 0 && cursor[1] < DROWS - 1) {
5334 cursor[0] -= moveIncrement;
5335 cursor[1] += moveIncrement;
5336 }
5337 cursorMovementCommand = movementKeystroke = keysMoveCursor;
5338 break;
5339 case DOWNRIGHT_KEY:
5340 case NUMPAD_3:
5341 if (keysMoveCursor && cursor[0] < DCOLS - 1 && cursor[1] < DROWS - 1) {
5342 cursor[0] += moveIncrement;
5343 cursor[1] += moveIncrement;
5344 }
5345 cursorMovementCommand = movementKeystroke = keysMoveCursor;
5346 break;
5347 case TAB_KEY:
5348 case SHIFT_TAB_KEY:
5349 case NUMPAD_0:
5350 *tabKey = true;
5351 break;
5352 case RETURN_KEY:
5353 *targetConfirmed = true;
5354 break;
5355 case ESCAPE_KEY:
5356 case ACKNOWLEDGE_KEY:
5357 *canceled = true;
5358 break;
5359 default:
5360 break;
5361 }
5362 } else if (theEvent.eventType == RIGHT_MOUSE_UP) {
5363 // do nothing
5364 } else {
5365 again = true;
5366 }
5367
5368 if (sidebarHighlighted
5369 && (!(pmap[cursor[0]][cursor[1]].flags & (HAS_PLAYER | HAS_MONSTER))
5370 || !canSeeMonster(monsterAtLoc(cursor[0], cursor[1])))
5371 && (!(pmap[cursor[0]][cursor[1]].flags & HAS_ITEM) || !playerCanSeeOrSense(cursor[0], cursor[1]))
5372 && (!cellHasTMFlag(cursor[0], cursor[1], TM_LIST_IN_SIDEBAR) || !playerCanSeeOrSense(cursor[0], cursor[1]))) {
5373
5374 // The sidebar is highlighted but the cursor is not on a visible item, monster or terrain. Un-highlight the sidebar.
5375 refreshSideBar(-1, -1, false);
5376 sidebarHighlighted = false;
5377 }
5378
5379 if (targetCanLeaveMap && !movementKeystroke) {
5380 // permit it to leave the map by up to 1 space in any direction if mouse controlled.
5381 cursor[0] = clamp(cursor[0], -1, DCOLS);
5382 cursor[1] = clamp(cursor[1], -1, DROWS);
5383 } else {
5384 cursor[0] = clamp(cursor[0], 0, DCOLS - 1);
5385 cursor[1] = clamp(cursor[1], 0, DROWS - 1);
5386 }
5387 } while (again && (!event || !cursorMovementCommand));
5388
5389 if (event) {
5390 *event = theEvent;
5391 }
5392
5393 if (sidebarHighlighted) {
5394 // Don't leave the sidebar highlighted when we exit.
5395 refreshSideBar(-1, -1, false);
5396 sidebarHighlighted = false;
5397 }
5398
5399 targetLoc[0] = cursor[0];
5400 targetLoc[1] = cursor[1];
5401
5402 return !cursorMovementCommand;
5403 }
5404
pullMouseClickDuringPlayback(short loc[2])5405 void pullMouseClickDuringPlayback(short loc[2]) {
5406 rogueEvent theEvent;
5407
5408 brogueAssert(rogue.playbackMode);
5409 nextBrogueEvent(&theEvent, false, false, false);
5410 loc[0] = windowToMapX(theEvent.param1);
5411 loc[1] = windowToMapY(theEvent.param2);
5412 }
5413
5414 // Returns whether monst is targetable with thrown items, staves, wands, etc.
5415 // i.e. would the player ever select it?
creatureIsTargetable(creature * monst)5416 static boolean creatureIsTargetable(creature *monst) {
5417 return monst != NULL
5418 && canSeeMonster(monst)
5419 && monst->depth == rogue.depthLevel
5420 && !(monst->bookkeepingFlags & MB_IS_DYING)
5421 && openPathBetween(player.xLoc, player.yLoc, monst->xLoc, monst->yLoc);
5422 }
5423
5424 // Return true if a target is chosen, or false if canceled.
chooseTarget(short returnLoc[2],short maxDistance,boolean stopAtTarget,boolean autoTarget,boolean targetAllies,const bolt * theBolt,const color * trajectoryColor)5425 boolean chooseTarget(short returnLoc[2],
5426 short maxDistance,
5427 boolean stopAtTarget,
5428 boolean autoTarget,
5429 boolean targetAllies,
5430 const bolt *theBolt,
5431 const color *trajectoryColor) {
5432 short originLoc[2], targetLoc[2], oldTargetLoc[2], coordinates[DCOLS][2], numCells, i, distance, newX, newY;
5433 creature *monst;
5434 boolean canceled, targetConfirmed, tabKey, cursorInTrajectory, focusedOnSomething = false;
5435 rogueEvent event = {0};
5436 short oldRNG;
5437 color trajColor = *trajectoryColor;
5438
5439 normColor(&trajColor, 100, 10);
5440
5441 if (rogue.playbackMode) {
5442 // In playback, pull the next event (a mouseclick) and use that location as the target.
5443 pullMouseClickDuringPlayback(returnLoc);
5444 rogue.cursorLoc[0] = rogue.cursorLoc[1] = -1;
5445 return true;
5446 }
5447
5448 oldRNG = rogue.RNG;
5449 rogue.RNG = RNG_COSMETIC;
5450 //assureCosmeticRNG;
5451
5452 originLoc[0] = player.xLoc;
5453 originLoc[1] = player.yLoc;
5454
5455 targetLoc[0] = oldTargetLoc[0] = player.xLoc;
5456 targetLoc[1] = oldTargetLoc[1] = player.yLoc;
5457
5458 if (autoTarget) {
5459 if (creatureIsTargetable(rogue.lastTarget) && (targetAllies == (rogue.lastTarget->creatureState == MONSTER_ALLY))) {
5460 monst = rogue.lastTarget;
5461 } else {
5462 //rogue.lastTarget = NULL;
5463 if (nextTargetAfter(&newX, &newY, targetLoc[0], targetLoc[1], !targetAllies, targetAllies, false, false, true, false)) {
5464 targetLoc[0] = newX;
5465 targetLoc[1] = newY;
5466 }
5467 monst = monsterAtLoc(targetLoc[0], targetLoc[1]);
5468 }
5469 if (monst) {
5470 targetLoc[0] = monst->xLoc;
5471 targetLoc[1] = monst->yLoc;
5472 refreshSideBar(monst->xLoc, monst->yLoc, false);
5473 focusedOnSomething = true;
5474 }
5475 }
5476
5477 numCells = getLineCoordinates(coordinates, originLoc, targetLoc, theBolt);
5478 if (maxDistance > 0) {
5479 numCells = min(numCells, maxDistance);
5480 }
5481 if (stopAtTarget) {
5482 numCells = min(numCells, distanceBetween(player.xLoc, player.yLoc, targetLoc[0], targetLoc[1]));
5483 }
5484
5485 targetConfirmed = canceled = tabKey = false;
5486
5487 do {
5488 printLocationDescription(targetLoc[0], targetLoc[1]);
5489
5490 if (canceled) {
5491 refreshDungeonCell(oldTargetLoc[0], oldTargetLoc[1]);
5492 hiliteTrajectory(coordinates, numCells, true, theBolt, trajectoryColor);
5493 confirmMessages();
5494 rogue.cursorLoc[0] = rogue.cursorLoc[1] = -1;
5495 restoreRNG;
5496 return false;
5497 }
5498
5499 if (tabKey) {
5500 if (nextTargetAfter(&newX, &newY, targetLoc[0], targetLoc[1], !targetAllies, targetAllies, false, false, true, event.shiftKey)) {
5501 targetLoc[0] = newX;
5502 targetLoc[1] = newY;
5503 }
5504 }
5505
5506 monst = monsterAtLoc(targetLoc[0], targetLoc[1]);
5507 if (monst != NULL && monst != &player && canSeeMonster(monst)) {
5508 focusedOnSomething = true;
5509 } else if (playerCanSeeOrSense(targetLoc[0], targetLoc[1])
5510 && (pmap[targetLoc[0]][targetLoc[1]].flags & HAS_ITEM) || cellHasTMFlag(targetLoc[0], targetLoc[1], TM_LIST_IN_SIDEBAR)) {
5511 focusedOnSomething = true;
5512 } else if (focusedOnSomething) {
5513 refreshSideBar(-1, -1, false);
5514 focusedOnSomething = false;
5515 }
5516 if (focusedOnSomething) {
5517 refreshSideBar(targetLoc[0], targetLoc[1], false);
5518 }
5519
5520 refreshDungeonCell(oldTargetLoc[0], oldTargetLoc[1]);
5521 hiliteTrajectory(coordinates, numCells, true, theBolt, &trajColor);
5522
5523 if (!targetConfirmed) {
5524 numCells = getLineCoordinates(coordinates, originLoc, targetLoc, theBolt);
5525 if (maxDistance > 0) {
5526 numCells = min(numCells, maxDistance);
5527 }
5528
5529 if (stopAtTarget) {
5530 numCells = min(numCells, distanceBetween(player.xLoc, player.yLoc, targetLoc[0], targetLoc[1]));
5531 }
5532 distance = hiliteTrajectory(coordinates, numCells, false, theBolt, &trajColor);
5533 cursorInTrajectory = false;
5534 for (i=0; i<distance; i++) {
5535 if (coordinates[i][0] == targetLoc[0] && coordinates[i][1] == targetLoc[1]) {
5536 cursorInTrajectory = true;
5537 break;
5538 }
5539 }
5540 hiliteCell(targetLoc[0], targetLoc[1], &white, (cursorInTrajectory ? 100 : 35), true);
5541 }
5542
5543 oldTargetLoc[0] = targetLoc[0];
5544 oldTargetLoc[1] = targetLoc[1];
5545 moveCursor(&targetConfirmed, &canceled, &tabKey, targetLoc, &event, NULL, false, true, false);
5546 if (event.eventType == RIGHT_MOUSE_UP) { // Right mouse cancels.
5547 canceled = true;
5548 }
5549 } while (!targetConfirmed);
5550 if (maxDistance > 0) {
5551 numCells = min(numCells, maxDistance);
5552 }
5553 hiliteTrajectory(coordinates, numCells, true, theBolt, trajectoryColor);
5554 refreshDungeonCell(oldTargetLoc[0], oldTargetLoc[1]);
5555
5556 if (originLoc[0] == targetLoc[0] && originLoc[1] == targetLoc[1]) {
5557 confirmMessages();
5558 restoreRNG;
5559 rogue.cursorLoc[0] = rogue.cursorLoc[1] = -1;
5560 return false;
5561 }
5562
5563 monst = monsterAtLoc(targetLoc[0], targetLoc[1]);
5564 if (monst && monst != &player && canSeeMonster(monst)) {
5565 rogue.lastTarget = monst;
5566 }
5567
5568 returnLoc[0] = targetLoc[0];
5569 returnLoc[1] = targetLoc[1];
5570 restoreRNG;
5571 rogue.cursorLoc[0] = rogue.cursorLoc[1] = -1;
5572 return true;
5573 }
5574
identifyItemKind(item * theItem)5575 void identifyItemKind(item *theItem) {
5576 itemTable *theTable;
5577 short tableCount, i, lastItem;
5578
5579 theTable = tableForItemCategory(theItem->category, NULL);
5580 if (theTable) {
5581 theItem->flags &= ~ITEM_KIND_AUTO_ID;
5582
5583 tableCount = 0;
5584 lastItem = -1;
5585
5586 switch (theItem->category) {
5587 case SCROLL:
5588 tableCount = NUMBER_SCROLL_KINDS;
5589 break;
5590 case POTION:
5591 tableCount = NUMBER_POTION_KINDS;
5592 break;
5593 case WAND:
5594 tableCount = NUMBER_WAND_KINDS;
5595 break;
5596 case STAFF:
5597 tableCount = NUMBER_STAFF_KINDS;
5598 break;
5599 case RING:
5600 tableCount = NUMBER_RING_KINDS;
5601 break;
5602 default:
5603 break;
5604 }
5605 if ((theItem->category & RING)
5606 && theItem->enchant1 <= 0) {
5607
5608 theItem->flags |= ITEM_IDENTIFIED;
5609 }
5610
5611 if ((theItem->category & WAND)
5612 && theTable[theItem->kind].range.lowerBound == theTable[theItem->kind].range.upperBound) {
5613
5614 theItem->flags |= ITEM_IDENTIFIED;
5615 }
5616 if (tableCount) {
5617 theTable[theItem->kind].identified = true;
5618 for (i=0; i<tableCount; i++) {
5619 if (!(theTable[i].identified)) {
5620 if (lastItem != -1) {
5621 return; // At least two unidentified items remain.
5622 }
5623 lastItem = i;
5624 }
5625 }
5626 if (lastItem != -1) {
5627 // Exactly one unidentified item remains; identify it.
5628 theTable[lastItem].identified = true;
5629 }
5630 }
5631 }
5632 }
5633
autoIdentify(item * theItem)5634 void autoIdentify(item *theItem) {
5635 short quantityBackup;
5636 char buf[COLS * 3], oldName[COLS * 3], newName[COLS * 3];
5637
5638 if (tableForItemCategory(theItem->category, NULL)
5639 && !tableForItemCategory(theItem->category, NULL)[theItem->kind].identified) {
5640
5641 identifyItemKind(theItem);
5642 quantityBackup = theItem->quantity;
5643 theItem->quantity = 1;
5644 itemName(theItem, newName, false, true, NULL);
5645 theItem->quantity = quantityBackup;
5646 sprintf(buf, "(It must %s %s.)",
5647 ((theItem->category & (POTION | SCROLL)) ? "have been" : "be"),
5648 newName);
5649 messageWithColor(buf, &itemMessageColor, 0);
5650 }
5651
5652 if ((theItem->category & (WEAPON | ARMOR))
5653 && (theItem->flags & ITEM_RUNIC)
5654 && !(theItem->flags & ITEM_RUNIC_IDENTIFIED)) {
5655
5656 itemName(theItem, oldName, false, false, NULL);
5657 theItem->flags |= (ITEM_RUNIC_IDENTIFIED | ITEM_RUNIC_HINTED);
5658 itemName(theItem, newName, true, true, NULL);
5659 sprintf(buf, "(Your %s must be %s.)", oldName, newName);
5660 messageWithColor(buf, &itemMessageColor, 0);
5661 }
5662 }
5663
5664 // returns whether the item disappeared
hitMonsterWithProjectileWeapon(creature * thrower,creature * monst,item * theItem)5665 boolean hitMonsterWithProjectileWeapon(creature *thrower, creature *monst, item *theItem) {
5666 char buf[DCOLS], theItemName[DCOLS], targetName[DCOLS], armorRunicString[DCOLS];
5667 boolean thrownWeaponHit;
5668 item *equippedWeapon;
5669 short damage;
5670
5671 if (!(theItem->category & WEAPON)) {
5672 return false;
5673 }
5674
5675 armorRunicString[0] = '\0';
5676
5677 itemName(theItem, theItemName, false, false, NULL);
5678 monsterName(targetName, monst, true);
5679
5680 monst->status[STATUS_ENTRANCED] = 0;
5681
5682 if (monst != &player
5683 && monst->creatureMode != MODE_PERM_FLEEING
5684 && (monst->creatureState != MONSTER_FLEEING || monst->status[STATUS_MAGICAL_FEAR])
5685 && !(monst->bookkeepingFlags & MB_CAPTIVE)
5686 && monst->creatureState != MONSTER_ALLY) {
5687
5688 monst->creatureState = MONSTER_TRACKING_SCENT;
5689 if (monst->status[STATUS_MAGICAL_FEAR]) {
5690 monst->status[STATUS_MAGICAL_FEAR] = 1;
5691 }
5692 }
5693
5694 if (thrower == &player) {
5695 equippedWeapon = rogue.weapon;
5696 equipItem(theItem, true, NULL);
5697 thrownWeaponHit = attackHit(&player, monst);
5698 if (equippedWeapon) {
5699 equipItem(equippedWeapon, true, NULL);
5700 } else {
5701 unequipItem(theItem, true);
5702 }
5703 } else {
5704 thrownWeaponHit = attackHit(thrower, monst);
5705 }
5706
5707 if (thrownWeaponHit) {
5708 damage = monst->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE) ? 0 :
5709 (randClump(theItem->damage) * damageFraction(netEnchant(theItem)) / FP_FACTOR);
5710
5711 if (monst == &player) {
5712 applyArmorRunicEffect(armorRunicString, thrower, &damage, false);
5713 }
5714
5715 if (inflictDamage(thrower, monst, damage, &red, false)) { // monster killed
5716 sprintf(buf, "the %s %s %s.",
5717 theItemName,
5718 (monst->info.flags & MONST_INANIMATE) ? "destroyed" : "killed",
5719 targetName);
5720 messageWithColor(buf, messageColorFromVictim(monst), 0);
5721 } else {
5722 sprintf(buf, "the %s hit %s.", theItemName, targetName);
5723 if (theItem->flags & ITEM_RUNIC) {
5724 magicWeaponHit(monst, theItem, false);
5725 }
5726 messageWithColor(buf, messageColorFromVictim(monst), 0);
5727 }
5728 moralAttack(thrower, monst);
5729 if (armorRunicString[0]) {
5730 message(armorRunicString, 0);
5731 }
5732 return true;
5733 } else {
5734 theItem->flags &= ~ITEM_PLAYER_AVOIDS; // Don't avoid thrown weapons that missed.
5735 sprintf(buf, "the %s missed %s.", theItemName, targetName);
5736 message(buf, 0);
5737 return false;
5738 }
5739 }
5740
throwItem(item * theItem,creature * thrower,short targetLoc[2],short maxDistance)5741 void throwItem(item *theItem, creature *thrower, short targetLoc[2], short maxDistance) {
5742 short listOfCoordinates[MAX_BOLT_LENGTH][2], originLoc[2];
5743 short i, x, y, numCells;
5744 creature *monst = NULL;
5745 char buf[COLS*3], buf2[COLS*3], buf3[COLS*3];
5746 enum displayGlyph displayChar;
5747 color foreColor, backColor, multColor;
5748 short dropLoc[2];
5749 boolean hitSomethingSolid = false, fastForward = false;
5750 enum dungeonLayers layer;
5751
5752 theItem->flags |= ITEM_PLAYER_AVOIDS; // Avoid thrown items, unless it's a weapon that misses a monster.
5753
5754 x = originLoc[0] = thrower->xLoc;
5755 y = originLoc[1] = thrower->yLoc;
5756
5757 // Using BOLT_NONE for throws because all flags are off, which means we'll try to avoid all obstacles in front of the target
5758 numCells = getLineCoordinates(listOfCoordinates, originLoc, targetLoc, &boltCatalog[BOLT_NONE]);
5759
5760 thrower->ticksUntilTurn = thrower->attackSpeed;
5761
5762 if (thrower != &player
5763 && (pmap[originLoc[0]][originLoc[1]].flags & IN_FIELD_OF_VIEW)) {
5764
5765 monsterName(buf2, thrower, true);
5766 itemName(theItem, buf3, false, true, NULL);
5767 sprintf(buf, "%s hurls %s.", buf2, buf3);
5768 message(buf, 0);
5769 }
5770
5771 for (i=0; i<numCells && i < maxDistance; i++) {
5772 x = listOfCoordinates[i][0];
5773 y = listOfCoordinates[i][1];
5774
5775 if (pmap[x][y].flags & (HAS_MONSTER | HAS_PLAYER)) {
5776 monst = monsterAtLoc(x, y);
5777 if (!(monst->bookkeepingFlags & MB_SUBMERGED)) {
5778 // if (projectileReflects(thrower, monst) && i < DCOLS*2) {
5779 // if (projectileReflects(thrower, monst)) { // if it scores another reflection roll, reflect at caster
5780 // numCells = reflectBolt(originLoc[0], originLoc[1], listOfCoordinates, i, true);
5781 // } else {
5782 // numCells = reflectBolt(-1, -1, listOfCoordinates, i, false); // otherwise reflect randomly
5783 // }
5784 //
5785 // monsterName(buf2, monst, true);
5786 // itemName(theItem, buf3, false, false, NULL);
5787 // sprintf(buf, "%s deflect%s the %s", buf2, (monst == &player ? "" : "s"), buf3);
5788 // combatMessage(buf, 0);
5789 // continue;
5790 // }
5791 if ((theItem->category & WEAPON)
5792 && theItem->kind != INCENDIARY_DART
5793 && hitMonsterWithProjectileWeapon(thrower, monst, theItem)) {
5794 deleteItem(theItem);
5795 return;
5796 }
5797 break;
5798 }
5799 }
5800
5801 // We hit something!
5802 if (cellHasTerrainFlag(x, y, (T_OBSTRUCTS_PASSABILITY | T_OBSTRUCTS_VISION))) {
5803 if ((theItem->category & WEAPON)
5804 && (theItem->kind == INCENDIARY_DART)
5805 && (cellHasTerrainFlag(x, y, T_IS_FLAMMABLE) || (pmap[x][y].flags & (HAS_MONSTER | HAS_PLAYER)))) {
5806 // Incendiary darts thrown at flammable obstructions (foliage, wooden barricades, doors) will hit the obstruction
5807 // instead of bursting a cell earlier.
5808 } else if (cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY)
5809 && cellHasTMFlag(x, y, TM_PROMOTES_ON_PLAYER_ENTRY)
5810 && tileCatalog[pmap[x][y].layers[layerWithTMFlag(x, y, TM_PROMOTES_ON_PLAYER_ENTRY)]].flags & T_OBSTRUCTS_PASSABILITY) {
5811 layer = layerWithTMFlag(x, y, TM_PROMOTES_ON_PLAYER_ENTRY);
5812 if (tileCatalog[pmap[x][y].layers[layer]].flags & T_OBSTRUCTS_PASSABILITY) {
5813 message(tileCatalog[pmap[x][y].layers[layer]].flavorText, 0);
5814 promoteTile(x, y, layer, false);
5815 }
5816 } else {
5817 i--;
5818 if (i >= 0) {
5819 x = listOfCoordinates[i][0];
5820 y = listOfCoordinates[i][1];
5821 } else { // it was aimed point-blank into an obstruction
5822 x = thrower->xLoc;
5823 y = thrower->yLoc;
5824 }
5825 }
5826 hitSomethingSolid = true;
5827 break;
5828 }
5829
5830 if (playerCanSee(x, y)) { // show the graphic
5831 getCellAppearance(x, y, &displayChar, &foreColor, &backColor);
5832 foreColor = *(theItem->foreColor);
5833 if (playerCanDirectlySee(x, y)) {
5834 colorMultiplierFromDungeonLight(x, y, &multColor);
5835 applyColorMultiplier(&foreColor, &multColor);
5836 } else { // clairvoyant visible
5837 applyColorMultiplier(&foreColor, &clairvoyanceColor);
5838 }
5839 plotCharWithColor(theItem->displayChar, mapToWindowX(x), mapToWindowY(y), &foreColor, &backColor);
5840
5841 if (!fastForward) {
5842 fastForward = rogue.playbackFastForward || pauseBrogue(25);
5843 }
5844
5845 refreshDungeonCell(x, y);
5846 }
5847
5848 if (x == targetLoc[0] && y == targetLoc[1]) { // reached its target
5849 break;
5850 }
5851 }
5852
5853 if ((theItem->category & POTION) && (hitSomethingSolid || !cellHasTerrainFlag(x, y, T_AUTO_DESCENT))) {
5854 if (theItem->kind == POTION_CONFUSION || theItem->kind == POTION_POISON
5855 || theItem->kind == POTION_PARALYSIS || theItem->kind == POTION_INCINERATION
5856 || theItem->kind == POTION_DARKNESS || theItem->kind == POTION_LICHEN
5857 || theItem->kind == POTION_DESCENT) {
5858 switch (theItem->kind) {
5859 case POTION_POISON:
5860 strcpy(buf, "the flask shatters and a deadly purple cloud billows out!");
5861 spawnDungeonFeature(x, y, &dungeonFeatureCatalog[DF_POISON_GAS_CLOUD_POTION], true, false);
5862 message(buf, 0);
5863 break;
5864 case POTION_CONFUSION:
5865 strcpy(buf, "the flask shatters and a multi-hued cloud billows out!");
5866 spawnDungeonFeature(x, y, &dungeonFeatureCatalog[DF_CONFUSION_GAS_CLOUD_POTION], true, false);
5867 message(buf, 0);
5868 break;
5869 case POTION_PARALYSIS:
5870 strcpy(buf, "the flask shatters and a cloud of pink gas billows out!");
5871 spawnDungeonFeature(x, y, &dungeonFeatureCatalog[DF_PARALYSIS_GAS_CLOUD_POTION], true, false);
5872 message(buf, 0);
5873 break;
5874 case POTION_INCINERATION:
5875 strcpy(buf, "the flask shatters and its contents burst violently into flame!");
5876 message(buf, 0);
5877 spawnDungeonFeature(x, y, &dungeonFeatureCatalog[DF_INCINERATION_POTION], true, false);
5878 break;
5879 case POTION_DARKNESS:
5880 strcpy(buf, "the flask shatters and the lights in the area start fading.");
5881 spawnDungeonFeature(x, y, &dungeonFeatureCatalog[DF_DARKNESS_POTION], true, false);
5882 message(buf, 0);
5883 break;
5884 case POTION_DESCENT:
5885 strcpy(buf, "as the flask shatters, the ground vanishes!");
5886 message(buf, 0);
5887 spawnDungeonFeature(x, y, &dungeonFeatureCatalog[DF_HOLE_POTION], true, false);
5888 break;
5889 case POTION_LICHEN:
5890 strcpy(buf, "the flask shatters and deadly spores spill out!");
5891 message(buf, 0);
5892 spawnDungeonFeature(x, y, &dungeonFeatureCatalog[DF_LICHEN_PLANTED], true, false);
5893 break;
5894 }
5895
5896 autoIdentify(theItem);
5897
5898 refreshDungeonCell(x, y);
5899
5900 //if (pmap[x][y].flags & (HAS_MONSTER | HAS_PLAYER)) {
5901 // monst = monsterAtLoc(x, y);
5902 // applyInstantTileEffectsToCreature(monst);
5903 //}
5904 } else {
5905 if (cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY)) {
5906 strcpy(buf2, "against");
5907 } else if (tileCatalog[pmap[x][y].layers[highestPriorityLayer(x, y, false)]].mechFlags & TM_STAND_IN_TILE) {
5908 strcpy(buf2, "into");
5909 } else {
5910 strcpy(buf2, "on");
5911 }
5912 sprintf(buf, "the flask shatters and %s liquid splashes harmlessly %s %s.",
5913 potionTable[theItem->kind].flavor, buf2, tileText(x, y));
5914 message(buf, 0);
5915 if (theItem->kind == POTION_HALLUCINATION && (theItem->flags & ITEM_MAGIC_DETECTED)) {
5916 autoIdentify(theItem);
5917 }
5918 }
5919 deleteItem(theItem);
5920 return; // potions disappear when they break
5921 }
5922 if ((theItem->category & WEAPON) && theItem->kind == INCENDIARY_DART) {
5923 spawnDungeonFeature(x, y, &dungeonFeatureCatalog[DF_DART_EXPLOSION], true, false);
5924 if (pmap[x][y].flags & (HAS_MONSTER | HAS_PLAYER)) {
5925 exposeCreatureToFire(monsterAtLoc(x, y));
5926 }
5927 deleteItem(theItem);
5928 return;
5929 }
5930 getQualifyingLocNear(dropLoc, x, y, true, 0, (T_OBSTRUCTS_ITEMS | T_OBSTRUCTS_PASSABILITY), (HAS_ITEM), false, false);
5931 placeItem(theItem, dropLoc[0], dropLoc[1]);
5932 refreshDungeonCell(dropLoc[0], dropLoc[1]);
5933 }
5934
5935 /*
5936 Called when the player chooses to throw an item. theItem is optional; if it is
5937 NULL, the player is prompted to choose one. If autoThrow is true and the last
5938 targeted creature is still targetable, the item is thrown at it without prompting.
5939 */
throwCommand(item * theItem,boolean autoThrow)5940 void throwCommand(item *theItem, boolean autoThrow) {
5941 item *thrownItem;
5942 char buf[COLS], theName[COLS];
5943 unsigned char command[10];
5944 short maxDistance, zapTarget[2], quantity;
5945
5946 command[0] = THROW_KEY;
5947
5948 //
5949 // From inventory, we know item
5950 // Else ask ITEM
5951 //
5952 if (theItem == NULL) {
5953 theItem = promptForItemOfType((ALL_ITEMS), 0, 0,
5954 KEYBOARD_LABELS ? "Throw what? (a-z, shift for more info; or <esc> to cancel)" : "Throw what?", true);
5955 }
5956 if (theItem == NULL) {
5957 return;
5958 }
5959
5960 //
5961 // Change quantity to 1 to generate name of item ("a" and not "some, the, etc")
5962 //
5963 quantity = theItem->quantity;
5964 theItem->quantity = 1;
5965 itemName(theItem, theName, false, false, NULL);
5966 theItem->quantity = quantity;
5967
5968 command[1] = theItem->inventoryLetter;
5969 confirmMessages();
5970
5971 //
5972 // If special item (not throw item)
5973 // -> Confirm before throw
5974 if (((theItem->flags & ITEM_EQUIPPED) || theItem->timesEnchanted > 0)
5975 && theItem->quantity <= 1) {
5976
5977 sprintf(buf, "Are you sure you want to throw your %s?", theName);
5978 if (!confirm(buf, false)) {
5979 return;
5980 }
5981 if (theItem->flags & ITEM_CURSED) {
5982 sprintf(buf, "You cannot unequip your %s; it appears to be cursed.", theName);
5983 messageWithColor(buf, &itemMessageColor, 0);
5984 return;
5985 }
5986 }
5987
5988 //
5989 // Ask location to throw
5990 //
5991 sprintf(buf, "Throw %s %s where? (<hjklyubn>, mouse, or <tab>)",
5992 (theItem->quantity > 1 ? "a" : "your"),
5993 theName);
5994 temporaryMessage(buf, REFRESH_SIDEBAR);
5995 maxDistance = (12 + 2 * max(rogue.strength - player.weaknessAmount - 12, 2));
5996
5997 // Automatically pick a target for known bad potions and throwing weapons
5998 boolean autoTarget = false;
5999 switch (theItem->category) {
6000 case WEAPON:
6001 if (theItem->kind == DART || theItem->kind == INCENDIARY_DART || theItem->kind == JAVELIN) {
6002 autoTarget = true;
6003 }
6004 break;
6005 case POTION:
6006 if ((theItem->flags & ITEM_MAGIC_DETECTED || potionTable[theItem->kind].identified)
6007 && itemMagicPolarity(theItem) == -1) {
6008 autoTarget = true;
6009 }
6010 break;
6011 default:
6012 break;
6013 }
6014
6015 if (autoThrow && creatureIsTargetable(rogue.lastTarget)) {
6016 zapTarget[0] = rogue.lastTarget->xLoc;
6017 zapTarget[1] = rogue.lastTarget->yLoc;
6018 } else if (!chooseTarget(zapTarget, maxDistance, true, autoTarget, false, &boltCatalog[BOLT_NONE], &red)) {
6019 // player doesn't choose a target? return
6020 return;
6021 }
6022
6023 if ((theItem->flags & ITEM_EQUIPPED) && theItem->quantity <= 1) {
6024 unequipItem(theItem, false);
6025 }
6026 command[2] = '\0';
6027 recordKeystrokeSequence(command);
6028 recordMouseClick(mapToWindowX(zapTarget[0]), mapToWindowY(zapTarget[1]), true, false);
6029
6030 confirmMessages();
6031
6032 thrownItem = generateItem(ALL_ITEMS, -1); // generate item object in memory
6033 *thrownItem = *theItem; // clone the item
6034 thrownItem->flags &= ~ITEM_EQUIPPED; // item not equiped
6035 thrownItem->quantity = 1; // item thrown, so quantity == 1
6036
6037 itemName(thrownItem, theName, false, false, NULL); // update name of the thrown item
6038
6039 throwItem(thrownItem, &player, zapTarget, maxDistance);
6040
6041 // Update inventory
6042 // -> Now decrement or delete the thrown item out of the inventory.
6043 // -> Save last item thrown
6044 if (theItem->quantity > 1) {
6045 theItem->quantity--;
6046 rogue.lastItemThrown = theItem;
6047 } else {
6048 rogue.lastItemThrown = NULL;
6049 if (rogue.swappedIn == theItem || rogue.swappedOut == theItem) {
6050 rogue.swappedIn = NULL;
6051 rogue.swappedOut = NULL;
6052 }
6053 removeItemFromChain(theItem, packItems);
6054 deleteItem(theItem);
6055 }
6056 playerTurnEnded();
6057 }
6058
relabel(item * theItem)6059 void relabel(item *theItem) {
6060 item *oldItem;
6061 char buf[COLS * 3], theName[COLS], newLabel;
6062 unsigned char command[10];
6063
6064 if (!KEYBOARD_LABELS && !rogue.playbackMode) {
6065 return;
6066 }
6067 if (theItem == NULL) {
6068 theItem = promptForItemOfType((ALL_ITEMS), 0, 0,
6069 KEYBOARD_LABELS ? "Relabel what? (a-z, shift for more info; or <esc> to cancel)" : "Relabel what?", true);
6070 }
6071 if (theItem == NULL) {
6072 return;
6073 }
6074 temporaryMessage("New letter? (a-z)", 0);
6075 newLabel = '\0';
6076 do {
6077 newLabel = nextKeyPress(true);
6078 } while (!newLabel);
6079
6080 if (newLabel >= 'A' && newLabel <= 'Z') {
6081 newLabel += 'a' - 'A'; // lower-case.
6082 }
6083 if (newLabel >= 'a' && newLabel <= 'z') {
6084 if (newLabel != theItem->inventoryLetter) {
6085 command[0] = RELABEL_KEY;
6086 command[1] = theItem->inventoryLetter;
6087 command[2] = newLabel;
6088 command[3] = '\0';
6089 recordKeystrokeSequence(command);
6090
6091 oldItem = itemOfPackLetter(newLabel);
6092 if (oldItem) {
6093 oldItem->inventoryLetter = theItem->inventoryLetter;
6094 itemName(oldItem, theName, true, true, NULL);
6095 sprintf(buf, "Relabeled %s as (%c);", theName, oldItem->inventoryLetter);
6096 messageWithColor(buf, &itemMessageColor, 0);
6097 }
6098 theItem->inventoryLetter = newLabel;
6099 itemName(theItem, theName, true, true, NULL);
6100 sprintf(buf, "%selabeled %s as (%c).", oldItem ? " r" : "R", theName, newLabel);
6101 messageWithColor(buf, &itemMessageColor, 0);
6102 } else {
6103 itemName(theItem, theName, true, true, NULL);
6104 sprintf(buf, "%s %s already labeled (%c).",
6105 theName,
6106 theItem->quantity == 1 ? "is" : "are",
6107 theItem->inventoryLetter);
6108 messageWithColor(buf, &itemMessageColor, 0);
6109 }
6110 }
6111 }
6112
6113 // If the most recently equipped item caused another item to be unequiped, is
6114 // uncursed, and both haven't left the inventory since, swap them back.
swapLastEquipment()6115 void swapLastEquipment() {
6116 item *theItem;
6117 unsigned char command[10];
6118
6119 if (rogue.swappedIn == NULL || rogue.swappedOut == NULL) {
6120 confirmMessages();
6121 message("You have nothing to swap.", 0);
6122 return;
6123 }
6124
6125 if (!equipItem(rogue.swappedOut, false, rogue.swappedIn)) {
6126 // Cursed
6127 return;
6128 }
6129
6130 command[0] = SWAP_KEY;
6131 command[1] = '\0';
6132 recordKeystrokeSequence(command);
6133
6134 theItem = rogue.swappedIn;
6135 rogue.swappedIn = rogue.swappedOut;
6136 rogue.swappedOut = theItem;
6137
6138 playerTurnEnded();
6139 }
6140
6141 // If the blink trajectory lands in lava based on the player's knowledge, abort.
6142 // If the blink trajectory might land in lava based on the player's knowledge,
6143 // prompt for confirmation.
playerCancelsBlinking(const short originLoc[2],const short targetLoc[2],const short maxDistance)6144 boolean playerCancelsBlinking(const short originLoc[2], const short targetLoc[2], const short maxDistance) {
6145 short coordinates[DCOLS][2], impactLoc[2];
6146 short numCells, i, x, y;
6147 boolean certainDeath = false;
6148 boolean possibleDeath = false;
6149 unsigned long tFlags, tmFlags;
6150
6151 if (rogue.playbackMode) {
6152 return false;
6153 }
6154
6155 if (player.status[STATUS_IMMUNE_TO_FIRE]
6156 || player.status[STATUS_LEVITATING]) {
6157 return false;
6158 }
6159
6160 getImpactLoc(impactLoc, originLoc, targetLoc, maxDistance > 0 ? maxDistance : DCOLS, true, &boltCatalog[BOLT_BLINKING]);
6161 getLocationFlags(impactLoc[0], impactLoc[1], &tFlags, &tmFlags, NULL, true);
6162 if (maxDistance > 0) {
6163 if ((pmap[impactLoc[0]][impactLoc[1]].flags & DISCOVERED)
6164 && (tFlags & T_LAVA_INSTA_DEATH)
6165 && !(tFlags & (T_ENTANGLES | T_AUTO_DESCENT))
6166 && !(tmFlags & TM_EXTINGUISHES_FIRE)) {
6167
6168 certainDeath = possibleDeath = true;
6169 }
6170 } else {
6171 certainDeath = true;
6172 numCells = getLineCoordinates(coordinates, originLoc, targetLoc, &boltCatalog[BOLT_BLINKING]);
6173 for (i = 0; i < numCells; i++) {
6174 x = coordinates[i][0];
6175 y = coordinates[i][1];
6176 if (pmap[x][y].flags & DISCOVERED) {
6177 getLocationFlags(x, y, &tFlags, NULL, NULL, true);
6178 if ((tFlags & T_LAVA_INSTA_DEATH)
6179 && !(tFlags & (T_ENTANGLES | T_AUTO_DESCENT))
6180 && !(tmFlags & TM_EXTINGUISHES_FIRE)) {
6181
6182 possibleDeath = true;
6183 } else if (i >= staffBlinkDistance(2 * FP_FACTOR) - 1) {
6184 // Found at least one possible safe landing spot.
6185 certainDeath = false;
6186 }
6187 }
6188 if (x == impactLoc[0]
6189 && y == impactLoc[1]) {
6190
6191 break;
6192 }
6193 }
6194 }
6195 if (possibleDeath && certainDeath) {
6196 message("that would be certain death!", 0);
6197 return true;
6198 }
6199 if (possibleDeath
6200 && !confirm("Blink across lava with unknown range?", false)) {
6201 return true;
6202 }
6203 return false;
6204 }
6205
useStaffOrWand(item * theItem,boolean * commandsRecorded)6206 boolean useStaffOrWand(item *theItem, boolean *commandsRecorded) {
6207 char buf[COLS], buf2[COLS];
6208 unsigned char command[10];
6209 short zapTarget[2], originLoc[2], maxDistance, c;
6210 boolean autoTarget, targetAllies, autoID, boltKnown, confirmedTarget;
6211 bolt theBolt;
6212 color trajectoryHiliteColor;
6213
6214 c = 0;
6215 command[c++] = APPLY_KEY;
6216 command[c++] = theItem->inventoryLetter;
6217
6218 if (theItem->charges <= 0 && (theItem->flags & ITEM_IDENTIFIED)) {
6219 itemName(theItem, buf2, false, false, NULL);
6220 sprintf(buf, "Your %s has no charges.", buf2);
6221 messageWithColor(buf, &itemMessageColor, 0);
6222 return false;
6223 }
6224 temporaryMessage("Direction? (<hjklyubn>, mouse, or <tab>; <return> to confirm)", REFRESH_SIDEBAR);
6225 itemName(theItem, buf2, false, false, NULL);
6226 sprintf(buf, "Zapping your %s:", buf2);
6227 printString(buf, mapToWindowX(0), 1, &itemMessageColor, &black, NULL);
6228
6229 theBolt = boltCatalog[tableForItemCategory(theItem->category, NULL)[theItem->kind].strengthRequired];
6230 if (theItem->category == STAFF) {
6231 theBolt.magnitude = theItem->enchant1;
6232 }
6233
6234 if ((theItem->category & STAFF) && theItem->kind == STAFF_BLINKING
6235 && theItem->flags & (ITEM_IDENTIFIED | ITEM_MAX_CHARGES_KNOWN)) {
6236
6237 maxDistance = staffBlinkDistance(netEnchant(theItem));
6238 } else {
6239 maxDistance = -1;
6240 }
6241 if (tableForItemCategory(theItem->category, NULL)[theItem->kind].identified) {
6242 autoTarget = targetAllies = false;
6243 if (!player.status[STATUS_HALLUCINATING]) {
6244 if (theBolt.flags & (BF_TARGET_ALLIES | BF_TARGET_ENEMIES)) {
6245 autoTarget = true;
6246 }
6247 if (theBolt.flags & BF_TARGET_ALLIES) {
6248 targetAllies = true;
6249 }
6250 }
6251 } else {
6252 autoTarget = true;
6253 targetAllies = false;
6254 }
6255 boltKnown = (((theItem->category & WAND) && wandTable[theItem->kind].identified)
6256 || ((theItem->category & STAFF) && staffTable[theItem->kind].identified));
6257 if (!boltKnown) {
6258 trajectoryHiliteColor = gray;
6259 } else if (theBolt.backColor == NULL) {
6260 trajectoryHiliteColor = red;
6261 } else {
6262 trajectoryHiliteColor = *theBolt.backColor;
6263 }
6264
6265 originLoc[0] = player.xLoc;
6266 originLoc[1] = player.yLoc;
6267 confirmedTarget = chooseTarget(zapTarget, maxDistance, false, autoTarget,
6268 targetAllies, (boltKnown ? &theBolt : &boltCatalog[BOLT_NONE]), &trajectoryHiliteColor);
6269 if (confirmedTarget
6270 && boltKnown
6271 && theBolt.boltEffect == BE_BLINKING
6272 && playerCancelsBlinking(originLoc, zapTarget, maxDistance)) {
6273
6274 confirmedTarget = false;
6275 }
6276 if (confirmedTarget) {
6277
6278 command[c] = '\0';
6279 if (!(*commandsRecorded)) {
6280 recordKeystrokeSequence(command);
6281 recordMouseClick(mapToWindowX(zapTarget[0]), mapToWindowY(zapTarget[1]), true, false);
6282 *commandsRecorded = true;
6283 }
6284 confirmMessages();
6285
6286 rogue.featRecord[FEAT_PURE_WARRIOR] = false;
6287
6288 if (theItem->charges > 0) {
6289 autoID = zap(originLoc, zapTarget,
6290 &theBolt,
6291 !boltKnown); // hide bolt details
6292 if (autoID) {
6293 if (!tableForItemCategory(theItem->category, NULL)[theItem->kind].identified) {
6294 itemName(theItem, buf2, false, false, NULL);
6295 sprintf(buf, "(Your %s must be ", buf2);
6296 identifyItemKind(theItem);
6297 itemName(theItem, buf2, false, true, NULL);
6298 strcat(buf, buf2);
6299 strcat(buf, ".)");
6300 messageWithColor(buf, &itemMessageColor, 0);
6301 }
6302 }
6303 } else {
6304 itemName(theItem, buf2, false, false, NULL);
6305 if (theItem->category == STAFF) {
6306 sprintf(buf, "Your %s fizzles; it must be out of charges for now.", buf2);
6307 } else {
6308 sprintf(buf, "Your %s fizzles; it must be depleted.", buf2);
6309 }
6310 messageWithColor(buf, &itemMessageColor, 0);
6311 theItem->flags |= ITEM_MAX_CHARGES_KNOWN;
6312 playerTurnEnded();
6313 return false;
6314 }
6315 } else {
6316 return false;
6317 }
6318 return true;
6319 }
6320
summonGuardian(item * theItem)6321 void summonGuardian(item *theItem) {
6322 short x = player.xLoc, y = player.yLoc;
6323 creature *monst;
6324
6325 monst = generateMonster(MK_CHARM_GUARDIAN, false, false);
6326 getQualifyingPathLocNear(&(monst->xLoc), &(monst->yLoc), x, y, true,
6327 T_DIVIDES_LEVEL & avoidedFlagsForMonster(&(monst->info)) & ~T_SPONTANEOUSLY_IGNITES, HAS_PLAYER,
6328 avoidedFlagsForMonster(&(monst->info)) & ~T_SPONTANEOUSLY_IGNITES, (HAS_PLAYER | HAS_MONSTER | HAS_STAIRS), false);
6329 monst->bookkeepingFlags |= (MB_FOLLOWER | MB_BOUND_TO_LEADER | MB_DOES_NOT_TRACK_LEADER);
6330 monst->bookkeepingFlags &= ~MB_JUST_SUMMONED;
6331 monst->leader = &player;
6332 monst->creatureState = MONSTER_ALLY;
6333 monst->ticksUntilTurn = monst->info.attackSpeed + 1; // So they don't move before the player's next turn.
6334 monst->status[STATUS_LIFESPAN_REMAINING] = monst->maxStatus[STATUS_LIFESPAN_REMAINING] = charmGuardianLifespan(netEnchant(theItem));
6335 pmap[monst->xLoc][monst->yLoc].flags |= HAS_MONSTER;
6336 fadeInMonster(monst);
6337 }
6338
useCharm(item * theItem)6339 void useCharm(item *theItem) {
6340 fixpt enchant = netEnchant(theItem);
6341
6342 rogue.featRecord[FEAT_PURE_WARRIOR] = false;
6343
6344 switch (theItem->kind) {
6345 case CHARM_HEALTH:
6346 heal(&player, charmHealing(enchant), false);
6347 message("You feel much healthier.", 0);
6348 break;
6349 case CHARM_PROTECTION:
6350 if (charmProtection(enchant) > player.status[STATUS_SHIELDED]) {
6351 player.status[STATUS_SHIELDED] = charmProtection(enchant);
6352 }
6353 player.maxStatus[STATUS_SHIELDED] = player.status[STATUS_SHIELDED];
6354 if (boltCatalog[BOLT_SHIELDING].backColor) {
6355 flashMonster(&player, boltCatalog[BOLT_SHIELDING].backColor, 100);
6356 }
6357 message("A shimmering shield coalesces around you.", 0);
6358 break;
6359 case CHARM_HASTE:
6360 haste(&player, charmEffectDuration(theItem->kind, theItem->enchant1));
6361 break;
6362 case CHARM_FIRE_IMMUNITY:
6363 player.status[STATUS_IMMUNE_TO_FIRE] = player.maxStatus[STATUS_IMMUNE_TO_FIRE] = charmEffectDuration(theItem->kind, theItem->enchant1);
6364 if (player.status[STATUS_BURNING]) {
6365 extinguishFireOnCreature(&player);
6366 }
6367 message("you no longer fear fire.", 0);
6368 break;
6369 case CHARM_INVISIBILITY:
6370 imbueInvisibility(&player, charmEffectDuration(theItem->kind, theItem->enchant1));
6371 message("You shiver as a chill runs up your spine.", 0);
6372 break;
6373 case CHARM_TELEPATHY:
6374 makePlayerTelepathic(charmEffectDuration(theItem->kind, theItem->enchant1));
6375 break;
6376 case CHARM_LEVITATION:
6377 player.status[STATUS_LEVITATING] = player.maxStatus[STATUS_LEVITATING] = charmEffectDuration(theItem->kind, theItem->enchant1);
6378 player.bookkeepingFlags &= ~MB_SEIZED; // break free of holding monsters
6379 message("you float into the air!", 0);
6380 break;
6381 case CHARM_SHATTERING:
6382 messageWithColor("your charm emits a wave of turquoise light that pierces the nearby walls!", &itemMessageColor, 0);
6383 crystalize(charmShattering(enchant));
6384 break;
6385 case CHARM_GUARDIAN:
6386 messageWithColor("your charm flashes and the form of a mythical guardian coalesces!", &itemMessageColor, 0);
6387 summonGuardian(theItem);
6388 break;
6389 case CHARM_TELEPORTATION:
6390 teleport(&player, -1, -1, true);
6391 break;
6392 case CHARM_RECHARGING:
6393 rechargeItems(STAFF);
6394 break;
6395 case CHARM_NEGATION:
6396 negationBlast("your charm", charmNegationRadius(enchant) + 1); // Add 1 because otherwise radius 1 would affect only the player.
6397 break;
6398 default:
6399 break;
6400 }
6401 }
6402
apply(item * theItem,boolean recordCommands)6403 void apply(item *theItem, boolean recordCommands) {
6404 char buf[COLS * 3], buf2[COLS * 3];
6405 boolean commandsRecorded, revealItemType;
6406 unsigned char command[10] = "";
6407 short c;
6408
6409 commandsRecorded = !recordCommands;
6410 c = 0;
6411 command[c++] = APPLY_KEY;
6412
6413 revealItemType = false;
6414
6415 if (!theItem) {
6416 theItem = promptForItemOfType((SCROLL|FOOD|POTION|STAFF|WAND|CHARM), 0, 0,
6417 KEYBOARD_LABELS ? "Apply what? (a-z, shift for more info; or <esc> to cancel)" : "Apply what?",
6418 true);
6419 }
6420
6421 if (theItem == NULL) {
6422 return;
6423 }
6424
6425 if ((theItem->category == SCROLL || theItem->category == POTION)
6426 && magicCharDiscoverySuffix(theItem->category, theItem->kind) == -1
6427 && ((theItem->flags & ITEM_MAGIC_DETECTED) || tableForItemCategory(theItem->category, NULL)[theItem->kind].identified)) {
6428
6429 if (tableForItemCategory(theItem->category, NULL)[theItem->kind].identified) {
6430 sprintf(buf,
6431 "Really %s a %s of %s?",
6432 theItem->category == SCROLL ? "read" : "drink",
6433 theItem->category == SCROLL ? "scroll" : "potion",
6434 tableForItemCategory(theItem->category, NULL)[theItem->kind].name);
6435 } else {
6436 sprintf(buf,
6437 "Really %s a cursed %s?",
6438 theItem->category == SCROLL ? "read" : "drink",
6439 theItem->category == SCROLL ? "scroll" : "potion");
6440 }
6441 if (!confirm(buf, false)) {
6442 return;
6443 }
6444 }
6445
6446 command[c++] = theItem->inventoryLetter;
6447 confirmMessages();
6448 switch (theItem->category) {
6449 case FOOD:
6450 if (STOMACH_SIZE - player.status[STATUS_NUTRITION] < foodTable[theItem->kind].strengthRequired) { // Not hungry enough.
6451 sprintf(buf, "You're not hungry enough to fully enjoy the %s. Eat it anyway?",
6452 (theItem->kind == RATION ? "food" : "mango"));
6453 if (!confirm(buf, false)) {
6454 return;
6455 }
6456 }
6457 player.status[STATUS_NUTRITION] = min(foodTable[theItem->kind].strengthRequired + player.status[STATUS_NUTRITION], STOMACH_SIZE);
6458 if (theItem->kind == RATION) {
6459 messageWithColor("That food tasted delicious!", &itemMessageColor, 0);
6460 } else {
6461 messageWithColor("My, what a yummy mango!", &itemMessageColor, 0);
6462 }
6463 rogue.featRecord[FEAT_ASCETIC] = false;
6464 break;
6465 case POTION:
6466 command[c] = '\0';
6467 if (!commandsRecorded) {
6468 recordKeystrokeSequence(command);
6469 commandsRecorded = true;
6470 }
6471 if (!potionTable[theItem->kind].identified) {
6472 revealItemType = true;
6473 }
6474 drinkPotion(theItem);
6475 break;
6476 case SCROLL:
6477 command[c] = '\0';
6478 if (!commandsRecorded) {
6479 recordKeystrokeSequence(command);
6480 commandsRecorded = true; // have to record in case further keystrokes are necessary (e.g. enchant scroll)
6481 }
6482 if (!scrollTable[theItem->kind].identified
6483 && theItem->kind != SCROLL_ENCHANTING
6484 && theItem->kind != SCROLL_IDENTIFY) {
6485
6486 revealItemType = true;
6487 }
6488 readScroll(theItem);
6489 break;
6490 case STAFF:
6491 case WAND:
6492 if (!useStaffOrWand(theItem, &commandsRecorded)) {
6493 return;
6494 }
6495 break;
6496 case CHARM:
6497 if (theItem->charges > 0) {
6498 itemName(theItem, buf2, false, false, NULL);
6499 sprintf(buf, "Your %s hasn't finished recharging.", buf2);
6500 messageWithColor(buf, &itemMessageColor, 0);
6501 return;
6502 }
6503 if (!commandsRecorded) {
6504 command[c] = '\0';
6505 recordKeystrokeSequence(command);
6506 commandsRecorded = true;
6507 }
6508 useCharm(theItem);
6509 break;
6510 default:
6511 itemName(theItem, buf2, false, true, NULL);
6512 sprintf(buf, "you can't apply %s.", buf2);
6513 message(buf, 0);
6514 return;
6515 }
6516
6517 if (!commandsRecorded) { // to make sure we didn't already record the keystrokes above with staff/wand targeting
6518 command[c] = '\0';
6519 recordKeystrokeSequence(command);
6520 commandsRecorded = true;
6521 }
6522
6523 // Reveal the item type if appropriate.
6524 if (revealItemType) {
6525 autoIdentify(theItem);
6526 }
6527
6528 theItem->lastUsed[2] = theItem->lastUsed[1];
6529 theItem->lastUsed[1] = theItem->lastUsed[0];
6530 theItem->lastUsed[0] = rogue.absoluteTurnNumber;
6531
6532 if (theItem->category & CHARM) {
6533 theItem->charges = charmRechargeDelay(theItem->kind, theItem->enchant1);
6534 } else if (theItem->charges > 0) {
6535 theItem->charges--;
6536 if (theItem->category == WAND) {
6537 theItem->enchant2++; // keeps track of how many times the wand has been discharged for the player's convenience
6538 }
6539 } else if (theItem->quantity > 1) {
6540 theItem->quantity--;
6541 } else {
6542 removeItemFromChain(theItem, packItems);
6543 deleteItem(theItem);
6544 }
6545 playerTurnEnded();
6546 }
6547
identify(item * theItem)6548 void identify(item *theItem) {
6549 theItem->flags |= ITEM_IDENTIFIED;
6550 theItem->flags &= ~ITEM_CAN_BE_IDENTIFIED;
6551 if (theItem->flags & ITEM_RUNIC) {
6552 theItem->flags |= (ITEM_RUNIC_IDENTIFIED | ITEM_RUNIC_HINTED);
6553 }
6554 if (theItem->category & RING) {
6555 updateRingBonuses();
6556 }
6557 identifyItemKind(theItem);
6558 }
6559 /*
6560 enum monsterTypes chooseVorpalEnemy() {
6561 short i, index, possCount = 0, deepestLevel = 0, deepestHorde, chosenHorde, failsafe = 25;
6562 enum monsterTypes candidate;
6563
6564 for (i=0; i<NUMBER_HORDES; i++) {
6565 if (hordeCatalog[i].minLevel >= rogue.depthLevel && !hordeCatalog[i].flags) {
6566 possCount += hordeCatalog[i].frequency;
6567 }
6568 if (hordeCatalog[i].minLevel > deepestLevel) {
6569 deepestHorde = i;
6570 deepestLevel = hordeCatalog[i].minLevel;
6571 }
6572 }
6573
6574 do {
6575 if (possCount == 0) {
6576 chosenHorde = deepestHorde;
6577 } else {
6578 index = rand_range(1, possCount);
6579 for (i=0; i<NUMBER_HORDES; i++) {
6580 if (hordeCatalog[i].minLevel >= rogue.depthLevel && !hordeCatalog[i].flags) {
6581 if (index <= hordeCatalog[i].frequency) {
6582 chosenHorde = i;
6583 break;
6584 }
6585 index -= hordeCatalog[i].frequency;
6586 }
6587 }
6588 }
6589
6590 index = rand_range(-1, hordeCatalog[chosenHorde].numberOfMemberTypes - 1);
6591 if (index == -1) {
6592 candidate = hordeCatalog[chosenHorde].leaderType;
6593 } else {
6594 candidate = hordeCatalog[chosenHorde].memberType[index];
6595 }
6596 } while (((monsterCatalog[candidate].flags & MONST_NEVER_VORPAL_ENEMY)
6597 || (monsterCatalog[candidate].abilityFlags & MA_NEVER_VORPAL_ENEMY))
6598 && --failsafe > 0);
6599 return candidate;
6600 }*/
6601
lotteryDraw(short * frequencies,short itemCount)6602 short lotteryDraw(short *frequencies, short itemCount) {
6603 short i, maxFreq, randIndex;
6604 maxFreq = 0;
6605 for (i = 0; i < itemCount; i++) {
6606 maxFreq += frequencies[i];
6607 }
6608 brogueAssert(maxFreq > 0);
6609 randIndex = rand_range(0, maxFreq - 1);
6610 for (i = 0; i < itemCount; i++) {
6611 if (frequencies[i] > randIndex) {
6612 return i;
6613 } else {
6614 randIndex -= frequencies[i];
6615 }
6616 }
6617 brogueAssert(false);
6618 return 0;
6619 }
6620
chooseVorpalEnemy()6621 short chooseVorpalEnemy() {
6622 short i, frequencies[MONSTER_CLASS_COUNT];
6623 for (i = 0; i < MONSTER_CLASS_COUNT; i++) {
6624 if (monsterClassCatalog[i].maxDepth <= 0
6625 || rogue.depthLevel <= monsterClassCatalog[i].maxDepth) {
6626
6627 frequencies[i] = monsterClassCatalog[i].frequency;
6628 } else {
6629 frequencies[i] = 0;
6630 }
6631 }
6632 return lotteryDraw(frequencies, MONSTER_CLASS_COUNT);
6633 }
6634
describeMonsterClass(char * buf,const short classID,boolean conjunctionAnd)6635 void describeMonsterClass(char *buf, const short classID, boolean conjunctionAnd) {
6636 short i;
6637 char buf2[50];
6638
6639 buf[0] = '\0';
6640 for (i = 0; monsterClassCatalog[classID].memberList[i] != 0; i++) {
6641 strcpy(buf2, monsterCatalog[monsterClassCatalog[classID].memberList[i]].monsterName);
6642 if (monsterClassCatalog[classID].memberList[i + 1] != 0) {
6643 if (monsterClassCatalog[classID].memberList[i + 2] == 0) {
6644 strcat(buf2, conjunctionAnd ? " and " : " or ");
6645 } else {
6646 strcat(buf2, ", ");
6647 }
6648 }
6649 strcat(buf, buf2);
6650 }
6651 }
6652
updateIdentifiableItem(item * theItem)6653 void updateIdentifiableItem(item *theItem) {
6654 if ((theItem->category & SCROLL) && scrollTable[theItem->kind].identified) {
6655 theItem->flags &= ~ITEM_CAN_BE_IDENTIFIED;
6656 } else if ((theItem->category & POTION) && potionTable[theItem->kind].identified) {
6657 theItem->flags &= ~ITEM_CAN_BE_IDENTIFIED;
6658 } else if ((theItem->category & (RING | STAFF | WAND))
6659 && (theItem->flags & ITEM_IDENTIFIED)
6660 && tableForItemCategory(theItem->category, NULL)[theItem->kind].identified) {
6661
6662 theItem->flags &= ~ITEM_CAN_BE_IDENTIFIED;
6663 } else if ((theItem->category & (WEAPON | ARMOR))
6664 && (theItem->flags & ITEM_IDENTIFIED)
6665 && (!(theItem->flags & ITEM_RUNIC) || (theItem->flags & ITEM_RUNIC_IDENTIFIED))) {
6666
6667 theItem->flags &= ~ITEM_CAN_BE_IDENTIFIED;
6668 } else if (theItem->category & NEVER_IDENTIFIABLE) {
6669 theItem->flags &= ~ITEM_CAN_BE_IDENTIFIED;
6670 }
6671 }
6672
updateIdentifiableItems()6673 void updateIdentifiableItems() {
6674 item *theItem;
6675 for (theItem = packItems->nextItem; theItem != NULL; theItem = theItem->nextItem) {
6676 updateIdentifiableItem(theItem);
6677 }
6678 for (theItem = floorItems; theItem != NULL; theItem = theItem->nextItem) {
6679 updateIdentifiableItem(theItem);
6680 }
6681 }
6682
magicMapCell(short x,short y)6683 void magicMapCell(short x, short y) {
6684 pmap[x][y].flags |= MAGIC_MAPPED;
6685 pmap[x][y].rememberedTerrainFlags = tileCatalog[pmap[x][y].layers[DUNGEON]].flags | tileCatalog[pmap[x][y].layers[LIQUID]].flags;
6686 pmap[x][y].rememberedTMFlags = tileCatalog[pmap[x][y].layers[DUNGEON]].mechFlags | tileCatalog[pmap[x][y].layers[LIQUID]].mechFlags;
6687 if (pmap[x][y].layers[LIQUID] && tileCatalog[pmap[x][y].layers[LIQUID]].drawPriority < tileCatalog[pmap[x][y].layers[DUNGEON]].drawPriority) {
6688 pmap[x][y].rememberedTerrain = pmap[x][y].layers[LIQUID];
6689 } else {
6690 pmap[x][y].rememberedTerrain = pmap[x][y].layers[DUNGEON];
6691 }
6692 }
6693
readScroll(item * theItem)6694 void readScroll(item *theItem) {
6695 short i, j, x, y, numberOfMonsters = 0;
6696 item *tempItem;
6697 creature *monst;
6698 boolean hadEffect = false;
6699 char buf[COLS * 3], buf2[COLS * 3];
6700
6701 rogue.featRecord[FEAT_ARCHIVIST] = false;
6702
6703 switch (theItem->kind) {
6704 case SCROLL_IDENTIFY:
6705 identify(theItem);
6706 updateIdentifiableItems();
6707 messageWithColor("this is a scroll of identify.", &itemMessageColor, REQUIRE_ACKNOWLEDGMENT);
6708 if (numberOfMatchingPackItems(ALL_ITEMS, ITEM_CAN_BE_IDENTIFIED, 0, false) == 0) {
6709 message("everything in your pack is already identified.", 0);
6710 break;
6711 }
6712 do {
6713 theItem = promptForItemOfType((ALL_ITEMS), ITEM_CAN_BE_IDENTIFIED, 0,
6714 KEYBOARD_LABELS ? "Identify what? (a-z; shift for more info)" : "Identify what?",
6715 false);
6716 if (rogue.gameHasEnded) {
6717 return;
6718 }
6719 if (theItem && !(theItem->flags & ITEM_CAN_BE_IDENTIFIED)) {
6720 confirmMessages();
6721 itemName(theItem, buf2, true, true, NULL);
6722 sprintf(buf, "you already know %s %s.", (theItem->quantity > 1 ? "they're" : "it's"), buf2);
6723 messageWithColor(buf, &itemMessageColor, 0);
6724 }
6725 } while (theItem == NULL || !(theItem->flags & ITEM_CAN_BE_IDENTIFIED));
6726 recordKeystroke(theItem->inventoryLetter, false, false);
6727 confirmMessages();
6728 identify(theItem);
6729 itemName(theItem, buf, true, true, NULL);
6730 sprintf(buf2, "%s %s.", (theItem->quantity == 1 ? "this is" : "these are"), buf);
6731 messageWithColor(buf2, &itemMessageColor, 0);
6732 break;
6733 case SCROLL_TELEPORT:
6734 teleport(&player, -1, -1, true);
6735 break;
6736 case SCROLL_REMOVE_CURSE:
6737 for (tempItem = packItems->nextItem; tempItem != NULL; tempItem = tempItem->nextItem) {
6738 if (tempItem->flags & ITEM_CURSED) {
6739 hadEffect = true;
6740 tempItem->flags &= ~ITEM_CURSED;
6741 }
6742 }
6743 if (hadEffect) {
6744 message("your pack glows with a cleansing light, and a malevolent energy disperses.", 0);
6745 } else {
6746 message("your pack glows with a cleansing light, but nothing happens.", 0);
6747 }
6748 break;
6749 case SCROLL_ENCHANTING:
6750 identify(theItem);
6751 messageWithColor("this is a scroll of enchanting.", &itemMessageColor, REQUIRE_ACKNOWLEDGMENT);
6752 if (!numberOfMatchingPackItems((WEAPON | ARMOR | RING | STAFF | WAND | CHARM), 0, 0, false)) {
6753 confirmMessages();
6754 message("you have nothing that can be enchanted.", 0);
6755 break;
6756 }
6757 do {
6758 theItem = promptForItemOfType((WEAPON | ARMOR | RING | STAFF | WAND | CHARM), 0, 0,
6759 KEYBOARD_LABELS ? "Enchant what? (a-z; shift for more info)" : "Enchant what?",
6760 false);
6761 confirmMessages();
6762 if (theItem == NULL || !(theItem->category & (WEAPON | ARMOR | RING | STAFF | WAND | CHARM))) {
6763 message("Can't enchant that.", REQUIRE_ACKNOWLEDGMENT);
6764 }
6765 if (rogue.gameHasEnded) {
6766 return;
6767 }
6768 } while (theItem == NULL || !(theItem->category & (WEAPON | ARMOR | RING | STAFF | WAND | CHARM)));
6769 recordKeystroke(theItem->inventoryLetter, false, false);
6770 confirmMessages();
6771 switch (theItem->category) {
6772 case WEAPON:
6773 theItem->strengthRequired = max(0, theItem->strengthRequired - 1);
6774 theItem->enchant1++;
6775 if (theItem->quiverNumber) {
6776 theItem->quiverNumber = rand_range(1, 60000);
6777 }
6778 break;
6779 case ARMOR:
6780 theItem->strengthRequired = max(0, theItem->strengthRequired - 1);
6781 theItem->enchant1++;
6782 break;
6783 case RING:
6784 theItem->enchant1++;
6785 updateRingBonuses();
6786 if (theItem->kind == RING_CLAIRVOYANCE) {
6787 updateClairvoyance();
6788 displayLevel();
6789 }
6790 break;
6791 case STAFF:
6792 theItem->enchant1++;
6793 theItem->charges++;
6794 theItem->enchant2 = 500 / theItem->enchant1;
6795 break;
6796 case WAND:
6797 theItem->charges += wandTable[theItem->kind].range.lowerBound;
6798 break;
6799 case CHARM:
6800 theItem->enchant1++;
6801 theItem->charges = min(0, theItem->charges); // Enchanting instantly recharges charms.
6802 // theItem->charges = theItem->charges
6803 // * charmRechargeDelay(theItem->kind, theItem->enchant1)
6804 // / charmRechargeDelay(theItem->kind, theItem->enchant1 - 1);
6805
6806 break;
6807 default:
6808 break;
6809 }
6810 theItem->timesEnchanted++;
6811 if ((theItem->category & (WEAPON | ARMOR | STAFF | RING | CHARM))
6812 && theItem->enchant1 >= 16) {
6813
6814 rogue.featRecord[FEAT_SPECIALIST] = true;
6815 }
6816 if (theItem->flags & ITEM_EQUIPPED) {
6817 equipItem(theItem, true, NULL);
6818 }
6819 itemName(theItem, buf, false, false, NULL);
6820 sprintf(buf2, "your %s gleam%s briefly in the darkness.", buf, (theItem->quantity == 1 ? "s" : ""));
6821 messageWithColor(buf2, &itemMessageColor, 0);
6822 if (theItem->flags & ITEM_CURSED) {
6823 sprintf(buf2, "a malevolent force leaves your %s.", buf);
6824 messageWithColor(buf2, &itemMessageColor, 0);
6825 theItem->flags &= ~ITEM_CURSED;
6826 }
6827 createFlare(player.xLoc, player.yLoc, SCROLL_ENCHANTMENT_LIGHT);
6828 break;
6829 case SCROLL_RECHARGING:
6830 rechargeItems(STAFF | CHARM);
6831 break;
6832 case SCROLL_PROTECT_ARMOR:
6833 if (rogue.armor) {
6834 tempItem = rogue.armor;
6835 tempItem->flags |= ITEM_PROTECTED;
6836 itemName(tempItem, buf2, false, false, NULL);
6837 sprintf(buf, "a protective golden light covers your %s.", buf2);
6838 messageWithColor(buf, &itemMessageColor, 0);
6839 if (tempItem->flags & ITEM_CURSED) {
6840 sprintf(buf, "a malevolent force leaves your %s.", buf2);
6841 messageWithColor(buf, &itemMessageColor, 0);
6842 tempItem->flags &= ~ITEM_CURSED;
6843 }
6844 } else {
6845 message("a protective golden light surrounds you, but it quickly disperses.", 0);
6846 }
6847 createFlare(player.xLoc, player.yLoc, SCROLL_PROTECTION_LIGHT);
6848 break;
6849 case SCROLL_PROTECT_WEAPON:
6850 if (rogue.weapon) {
6851 tempItem = rogue.weapon;
6852 tempItem->flags |= ITEM_PROTECTED;
6853 itemName(tempItem, buf2, false, false, NULL);
6854 sprintf(buf, "a protective golden light covers your %s.", buf2);
6855 messageWithColor(buf, &itemMessageColor, 0);
6856 if (tempItem->flags & ITEM_CURSED) {
6857 sprintf(buf, "a malevolent force leaves your %s.", buf2);
6858 messageWithColor(buf, &itemMessageColor, 0);
6859 tempItem->flags &= ~ITEM_CURSED;
6860 }
6861 if (rogue.weapon->quiverNumber) {
6862 rogue.weapon->quiverNumber = rand_range(1, 60000);
6863 }
6864 } else {
6865 message("a protective golden light covers your empty hands, but it quickly disperses.", 0);
6866 }
6867 createFlare(player.xLoc, player.yLoc, SCROLL_PROTECTION_LIGHT);
6868 break;
6869 case SCROLL_SANCTUARY:
6870 spawnDungeonFeature(player.xLoc, player.yLoc, &dungeonFeatureCatalog[DF_SACRED_GLYPHS], true, false);
6871 messageWithColor("sprays of color arc to the ground, forming glyphs where they alight.", &itemMessageColor, 0);
6872 break;
6873 case SCROLL_MAGIC_MAPPING:
6874 confirmMessages();
6875 messageWithColor("this scroll has a map on it!", &itemMessageColor, 0);
6876 for (i=0; i<DCOLS; i++) {
6877 for (j=0; j<DROWS; j++) {
6878 if (cellHasTMFlag(i, j, TM_IS_SECRET)) {
6879 discover(i, j);
6880 magicMapCell(i, j);
6881 pmap[i][j].flags &= ~(STABLE_MEMORY | DISCOVERED);
6882 }
6883 }
6884 }
6885 for (i=0; i<DCOLS; i++) {
6886 for (j=0; j<DROWS; j++) {
6887 if (!(pmap[i][j].flags & DISCOVERED) && pmap[i][j].layers[DUNGEON] != GRANITE) {
6888 magicMapCell(i, j);
6889 }
6890 }
6891 }
6892 colorFlash(&magicMapFlashColor, 0, MAGIC_MAPPED, 15, DCOLS + DROWS, player.xLoc, player.yLoc);
6893 break;
6894 case SCROLL_AGGRAVATE_MONSTER:
6895 aggravateMonsters(DCOLS + DROWS, player.xLoc, player.yLoc, &gray);
6896 message("the scroll emits a piercing shriek that echoes throughout the dungeon!", 0);
6897 break;
6898 case SCROLL_SUMMON_MONSTER:
6899 for (j=0; j<25 && numberOfMonsters < 3; j++) {
6900 for (i=0; i<8; i++) {
6901 x = player.xLoc + nbDirs[i][0];
6902 y = player.yLoc + nbDirs[i][1];
6903 if (!cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY) && !(pmap[x][y].flags & HAS_MONSTER)
6904 && rand_percent(10) && (numberOfMonsters < 3)) {
6905 monst = spawnHorde(0, x, y, (HORDE_LEADER_CAPTIVE | HORDE_NO_PERIODIC_SPAWN | HORDE_IS_SUMMONED | HORDE_MACHINE_ONLY), 0);
6906 if (monst) {
6907 // refreshDungeonCell(x, y);
6908 // monst->creatureState = MONSTER_TRACKING_SCENT;
6909 // monst->ticksUntilTurn = player.movementSpeed;
6910 wakeUp(monst);
6911 fadeInMonster(monst);
6912 numberOfMonsters++;
6913 }
6914 }
6915 }
6916 }
6917 if (numberOfMonsters > 1) {
6918 message("the fabric of space ripples, and monsters appear!", 0);
6919 } else if (numberOfMonsters == 1) {
6920 message("the fabric of space ripples, and a monster appears!", 0);
6921 } else {
6922 message("the fabric of space boils violently around you, but nothing happens.", 0);
6923 }
6924 break;
6925 case SCROLL_NEGATION:
6926 negationBlast("the scroll", DCOLS);
6927 break;
6928 case SCROLL_SHATTERING:
6929 messageWithColor("the scroll emits a wave of turquoise light that pierces the nearby walls!", &itemMessageColor, 0);
6930 crystalize(9);
6931 break;
6932 case SCROLL_DISCORD:
6933 discordBlast("the scroll", DCOLS);
6934 break;
6935 }
6936 }
6937
detectMagicOnItem(item * theItem)6938 void detectMagicOnItem(item *theItem) {
6939 theItem->flags |= ITEM_MAGIC_DETECTED;
6940 if ((theItem->category & (WEAPON | ARMOR))
6941 && theItem->enchant1 == 0
6942 && !(theItem->flags & ITEM_RUNIC)) {
6943
6944 identify(theItem);
6945 }
6946 }
6947
drinkPotion(item * theItem)6948 void drinkPotion(item *theItem) {
6949 item *tempItem = NULL;
6950 boolean hadEffect = false;
6951 boolean hadEffect2 = false;
6952 char buf[1000] = "";
6953
6954 brogueAssert(rogue.RNG == RNG_SUBSTANTIVE);
6955
6956 rogue.featRecord[FEAT_ARCHIVIST] = false;
6957
6958 switch (theItem->kind) {
6959 case POTION_LIFE:
6960 sprintf(buf, "%syour maximum health increases by %i%%.",
6961 ((player.currentHP < player.info.maxHP) ? "you heal completely and " : ""),
6962 (player.info.maxHP + 10) * 100 / player.info.maxHP - 100);
6963
6964 player.info.maxHP += 10;
6965 heal(&player, 100, true);
6966 updatePlayerRegenerationDelay();
6967 messageWithColor(buf, &advancementMessageColor, 0);
6968 break;
6969 case POTION_HALLUCINATION:
6970 player.status[STATUS_HALLUCINATING] = player.maxStatus[STATUS_HALLUCINATING] = 300;
6971 message("colors are everywhere! The walls are singing!", 0);
6972 break;
6973 case POTION_INCINERATION:
6974 //colorFlash(&darkOrange, 0, IN_FIELD_OF_VIEW, 4, 4, player.xLoc, player.yLoc);
6975 message("as you uncork the flask, it explodes in flame!", 0);
6976 spawnDungeonFeature(player.xLoc, player.yLoc, &dungeonFeatureCatalog[DF_INCINERATION_POTION], true, false);
6977 exposeCreatureToFire(&player);
6978 break;
6979 case POTION_DARKNESS:
6980 player.status[STATUS_DARKNESS] = max(400, player.status[STATUS_DARKNESS]);
6981 player.maxStatus[STATUS_DARKNESS] = max(400, player.maxStatus[STATUS_DARKNESS]);
6982 updateMinersLightRadius();
6983 updateVision(true);
6984 message("your vision flickers as a cloak of darkness settles around you!", 0);
6985 break;
6986 case POTION_DESCENT:
6987 colorFlash(&darkBlue, 0, IN_FIELD_OF_VIEW, 3, 3, player.xLoc, player.yLoc);
6988 message("vapor pours out of the flask and causes the floor to disappear!", 0);
6989 spawnDungeonFeature(player.xLoc, player.yLoc, &dungeonFeatureCatalog[DF_HOLE_POTION], true, false);
6990 if (!player.status[STATUS_LEVITATING]) {
6991 player.bookkeepingFlags |= MB_IS_FALLING;
6992 }
6993 break;
6994 case POTION_STRENGTH:
6995 rogue.strength++;
6996 if (player.status[STATUS_WEAKENED]) {
6997 player.status[STATUS_WEAKENED] = 1;
6998 }
6999 updateEncumbrance();
7000 messageWithColor("newfound strength surges through your body.", &advancementMessageColor, 0);
7001 createFlare(player.xLoc, player.yLoc, POTION_STRENGTH_LIGHT);
7002 break;
7003 case POTION_POISON:
7004 spawnDungeonFeature(player.xLoc, player.yLoc, &dungeonFeatureCatalog[DF_POISON_GAS_CLOUD_POTION], true, false);
7005 message("caustic gas billows out of the open flask!", 0);
7006 break;
7007 case POTION_PARALYSIS:
7008 spawnDungeonFeature(player.xLoc, player.yLoc, &dungeonFeatureCatalog[DF_PARALYSIS_GAS_CLOUD_POTION], true, false);
7009 message("your muscles stiffen as a cloud of pink gas bursts from the open flask!", 0);
7010 break;
7011 case POTION_TELEPATHY:
7012 makePlayerTelepathic(300);
7013 break;
7014 case POTION_LEVITATION:
7015 player.status[STATUS_LEVITATING] = player.maxStatus[STATUS_LEVITATING] = 100;
7016 player.bookkeepingFlags &= ~MB_SEIZED; // break free of holding monsters
7017 message("you float into the air!", 0);
7018 break;
7019 case POTION_CONFUSION:
7020 spawnDungeonFeature(player.xLoc, player.yLoc, &dungeonFeatureCatalog[DF_CONFUSION_GAS_CLOUD_POTION], true, false);
7021 message("a shimmering cloud of rainbow-colored gas billows out of the open flask!", 0);
7022 break;
7023 case POTION_LICHEN:
7024 message("a handful of tiny spores burst out of the open flask!", 0);
7025 spawnDungeonFeature(player.xLoc, player.yLoc, &dungeonFeatureCatalog[DF_LICHEN_PLANTED], true, false);
7026 break;
7027 case POTION_DETECT_MAGIC:
7028 hadEffect = false;
7029 hadEffect2 = false;
7030 for (tempItem = floorItems->nextItem; tempItem != NULL; tempItem = tempItem->nextItem) {
7031 if (tempItem->category & CAN_BE_DETECTED) {
7032 detectMagicOnItem(tempItem);
7033 if (itemMagicPolarity(tempItem)) {
7034 pmap[tempItem->xLoc][tempItem->yLoc].flags |= ITEM_DETECTED;
7035 hadEffect = true;
7036 refreshDungeonCell(tempItem->xLoc, tempItem->yLoc);
7037 }
7038 }
7039 }
7040 for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
7041 creature *monst = nextCreature(&it);
7042 if (monst->carriedItem && (monst->carriedItem->category & CAN_BE_DETECTED)) {
7043 detectMagicOnItem(monst->carriedItem);
7044 if (itemMagicPolarity(monst->carriedItem)) {
7045 hadEffect = true;
7046 refreshDungeonCell(monst->xLoc, monst->yLoc);
7047 }
7048 }
7049 }
7050 for (tempItem = packItems->nextItem; tempItem != NULL; tempItem = tempItem->nextItem) {
7051 if (tempItem->category & CAN_BE_DETECTED) {
7052 detectMagicOnItem(tempItem);
7053 if (itemMagicPolarity(tempItem)) {
7054 if (tempItem->flags & ITEM_MAGIC_DETECTED) {
7055 hadEffect2 = true;
7056 }
7057 }
7058 }
7059 }
7060 if (hadEffect || hadEffect2) {
7061 if (hadEffect && hadEffect2) {
7062 message("you can somehow feel the presence of magic on the level and in your pack.", 0);
7063 } else if (hadEffect) {
7064 message("you can somehow feel the presence of magic on the level.", 0);
7065 } else {
7066 message("you can somehow feel the presence of magic in your pack.", 0);
7067 }
7068 } else {
7069 message("you can somehow feel the absence of magic on the level and in your pack.", 0);
7070 }
7071 break;
7072 case POTION_HASTE_SELF:
7073 haste(&player, 25);
7074 break;
7075 case POTION_FIRE_IMMUNITY:
7076 player.status[STATUS_IMMUNE_TO_FIRE] = player.maxStatus[STATUS_IMMUNE_TO_FIRE] = 150;
7077 if (player.status[STATUS_BURNING]) {
7078 extinguishFireOnCreature(&player);
7079 }
7080 message("a comforting breeze envelops you, and you no longer fear fire.", 0);
7081 break;
7082 case POTION_INVISIBILITY:
7083 player.status[STATUS_INVISIBLE] = player.maxStatus[STATUS_INVISIBLE] = 75;
7084 message("you shiver as a chill runs up your spine.", 0);
7085 break;
7086 default:
7087 message("you feel very strange, as though your body doesn't know how to react!", REQUIRE_ACKNOWLEDGMENT);
7088 }
7089 }
7090
7091 // Used for the Discoveries screen. Returns a number: 1 == good, -1 == bad, 0 == could go either way.
magicCharDiscoverySuffix(short category,short kind)7092 short magicCharDiscoverySuffix(short category, short kind) {
7093 short result = 0;
7094
7095 switch (category) {
7096 case SCROLL:
7097 switch (kind) {
7098 case SCROLL_AGGRAVATE_MONSTER:
7099 case SCROLL_SUMMON_MONSTER:
7100 result = -1;
7101 break;
7102 default:
7103 result = 1;
7104 break;
7105 }
7106 break;
7107 case POTION:
7108 switch (kind) {
7109 case POTION_HALLUCINATION:
7110 case POTION_INCINERATION:
7111 case POTION_DESCENT:
7112 case POTION_POISON:
7113 case POTION_PARALYSIS:
7114 case POTION_CONFUSION:
7115 case POTION_LICHEN:
7116 case POTION_DARKNESS:
7117 result = -1;
7118 break;
7119 default:
7120 result = 1;
7121 break;
7122 }
7123 break;
7124 case WAND:
7125 case STAFF:
7126 if (boltCatalog[tableForItemCategory(category, NULL)[kind].strengthRequired].flags & (BF_TARGET_ALLIES)) {
7127 result = -1;
7128 } else {
7129 result = 1;
7130 }
7131 break;
7132 case RING:
7133 result = 0;
7134 break;
7135 case CHARM:
7136 result = 1;
7137 break;
7138 }
7139 return result;
7140 }
7141
7142 /* Returns
7143 -1 if the item is of bad magic
7144 0 if it is neutral
7145 1 if it is of good magic */
itemMagicPolarity(item * theItem)7146 int itemMagicPolarity(item *theItem) {
7147 switch (theItem->category) {
7148 case WEAPON:
7149 case ARMOR:
7150 if ((theItem->flags & ITEM_CURSED) || theItem->enchant1 < 0) {
7151 return -1;
7152 } else if (theItem->enchant1 > 0) {
7153 return 1;
7154 }
7155 return 0;
7156 break;
7157 case SCROLL:
7158 switch (theItem->kind) {
7159 case SCROLL_AGGRAVATE_MONSTER:
7160 case SCROLL_SUMMON_MONSTER:
7161 return -1;
7162 default:
7163 return 1;
7164 }
7165 case POTION:
7166 switch (theItem->kind) {
7167 case POTION_HALLUCINATION:
7168 case POTION_INCINERATION:
7169 case POTION_DESCENT:
7170 case POTION_POISON:
7171 case POTION_PARALYSIS:
7172 case POTION_CONFUSION:
7173 case POTION_LICHEN:
7174 case POTION_DARKNESS:
7175 return -1;
7176 default:
7177 return 1;
7178 }
7179 case WAND:
7180 if (theItem->charges == 0) {
7181 return 0;
7182 }
7183 case STAFF:
7184 if (boltCatalog[tableForItemCategory(theItem->category, NULL)[theItem->kind].strengthRequired].flags & (BF_TARGET_ALLIES)) {
7185 return -1;
7186 } else {
7187 return 1;
7188 }
7189 case RING:
7190 if (theItem->flags & ITEM_CURSED || theItem->enchant1 < 0) {
7191 return -1;
7192 } else if (theItem->enchant1 > 0) {
7193 return 1;
7194 } else {
7195 return 0;
7196 }
7197 case CHARM:
7198 return 1;
7199 case AMULET:
7200 return 1;
7201 }
7202 return 0;
7203 }
7204
unequip(item * theItem)7205 void unequip(item *theItem) {
7206 char buf[COLS * 3], buf2[COLS * 3];
7207 unsigned char command[3];
7208
7209 command[0] = UNEQUIP_KEY;
7210 if (theItem == NULL) {
7211 theItem = promptForItemOfType(ALL_ITEMS, ITEM_EQUIPPED, 0,
7212 KEYBOARD_LABELS ? "Remove (unequip) what? (a-z or <esc> to cancel)" : "Remove (unequip) what?",
7213 true);
7214 }
7215 if (theItem == NULL) {
7216 return;
7217 }
7218
7219 command[1] = theItem->inventoryLetter;
7220 command[2] = '\0';
7221
7222 if (!(theItem->flags & ITEM_EQUIPPED)) {
7223 itemName(theItem, buf2, false, false, NULL);
7224 sprintf(buf, "your %s %s not equipped.",
7225 buf2,
7226 theItem->quantity == 1 ? "was" : "were");
7227 confirmMessages();
7228 messageWithColor(buf, &itemMessageColor, 0);
7229 return;
7230 } else {
7231 if (!unequipItem(theItem, false)) {
7232 return; // cursed
7233 }
7234 recordKeystrokeSequence(command);
7235 itemName(theItem, buf2, true, true, NULL);
7236 if (strLenWithoutEscapes(buf2) > 52) {
7237 itemName(theItem, buf2, false, true, NULL);
7238 }
7239 confirmMessages();
7240 sprintf(buf, "you are no longer %s %s.", (theItem->category & WEAPON ? "wielding" : "wearing"), buf2);
7241 messageWithColor(buf, &itemMessageColor, 0);
7242 }
7243 playerTurnEnded();
7244 }
7245
canDrop()7246 boolean canDrop() {
7247 if (cellHasTerrainFlag(player.xLoc, player.yLoc, T_OBSTRUCTS_ITEMS)) {
7248 return false;
7249 }
7250 return true;
7251 }
7252
drop(item * theItem)7253 void drop(item *theItem) {
7254 char buf[COLS * 3], buf2[COLS * 3];
7255 unsigned char command[3];
7256
7257 command[0] = DROP_KEY;
7258 if (theItem == NULL) {
7259 theItem = promptForItemOfType(ALL_ITEMS, 0, 0,
7260 KEYBOARD_LABELS ? "Drop what? (a-z, shift for more info; or <esc> to cancel)" : "Drop what?",
7261 true);
7262 }
7263 if (theItem == NULL) {
7264 return;
7265 }
7266 command[1] = theItem->inventoryLetter;
7267 command[2] = '\0';
7268
7269 if ((theItem->flags & ITEM_EQUIPPED) && (theItem->flags & ITEM_CURSED)) {
7270 itemName(theItem, buf2, false, false, NULL);
7271 sprintf(buf, "you can't; your %s appears to be cursed.", buf2);
7272 confirmMessages();
7273 messageWithColor(buf, &itemMessageColor, 0);
7274 } else if (canDrop()) {
7275 recordKeystrokeSequence(command);
7276 if (theItem->flags & ITEM_EQUIPPED) {
7277 unequipItem(theItem, false);
7278 }
7279 theItem = dropItem(theItem); // This is where it gets dropped.
7280 theItem->flags |= ITEM_PLAYER_AVOIDS; // Try not to pick up stuff you've already dropped.
7281 itemName(theItem, buf2, true, true, NULL);
7282 sprintf(buf, "You dropped %s.", buf2);
7283 messageWithColor(buf, &itemMessageColor, 0);
7284 playerTurnEnded();
7285 } else {
7286 confirmMessages();
7287 message("There is already something there.", 0);
7288 }
7289 }
7290
promptForItemOfType(unsigned short category,unsigned long requiredFlags,unsigned long forbiddenFlags,char * prompt,boolean allowInventoryActions)7291 item *promptForItemOfType(unsigned short category,
7292 unsigned long requiredFlags,
7293 unsigned long forbiddenFlags,
7294 char *prompt,
7295 boolean allowInventoryActions) {
7296 char keystroke;
7297 item *theItem;
7298
7299 if (!numberOfMatchingPackItems(ALL_ITEMS, requiredFlags, forbiddenFlags, true)) {
7300 return NULL;
7301 }
7302
7303 temporaryMessage(prompt, 0);
7304
7305 keystroke = displayInventory(category, requiredFlags, forbiddenFlags, false, allowInventoryActions);
7306
7307 if (!keystroke) {
7308 // This can happen if the player does an action with an item directly from the inventory screen via a button.
7309 return NULL;
7310 }
7311
7312 if (keystroke < 'a' || keystroke > 'z') {
7313 confirmMessages();
7314 if (keystroke != ESCAPE_KEY && keystroke != ACKNOWLEDGE_KEY) {
7315 message("Invalid entry.", 0);
7316 }
7317 return NULL;
7318 }
7319
7320 theItem = itemOfPackLetter(keystroke);
7321 if (theItem == NULL) {
7322 confirmMessages();
7323 message("No such item.", 0);
7324 return NULL;
7325 }
7326
7327 return theItem;
7328 }
7329
itemOfPackLetter(char letter)7330 item *itemOfPackLetter(char letter) {
7331 item *theItem;
7332 for (theItem = packItems->nextItem; theItem != NULL; theItem = theItem->nextItem) {
7333 if (theItem->inventoryLetter == letter) {
7334 return theItem;
7335 }
7336 }
7337 return NULL;
7338 }
7339
itemAtLoc(short x,short y)7340 item *itemAtLoc(short x, short y) {
7341 item *theItem;
7342
7343 if (!(pmap[x][y].flags & HAS_ITEM)) {
7344 return NULL; // easy optimization
7345 }
7346 for (theItem = floorItems->nextItem; theItem != NULL && (theItem->xLoc != x || theItem->yLoc != y); theItem = theItem->nextItem);
7347 if (theItem == NULL) {
7348 pmap[x][y].flags &= ~HAS_ITEM;
7349 hiliteCell(x, y, &white, 75, true);
7350 rogue.automationActive = false;
7351 message("ERROR: An item was supposed to be here, but I couldn't find it.", REQUIRE_ACKNOWLEDGMENT);
7352 refreshDungeonCell(x, y);
7353 }
7354 return theItem;
7355 }
7356
dropItem(item * theItem)7357 item *dropItem(item *theItem) {
7358 item *itemFromTopOfStack, *itemOnFloor;
7359
7360 if (cellHasTerrainFlag(player.xLoc, player.yLoc, T_OBSTRUCTS_ITEMS)) {
7361 return NULL;
7362 }
7363
7364 itemOnFloor = itemAtLoc(player.xLoc, player.yLoc);
7365
7366 if (theItem->quantity > 1 && !(theItem->category & (WEAPON | GEM))) { // peel off the top item and drop it
7367 itemFromTopOfStack = generateItem(ALL_ITEMS, -1);
7368 *itemFromTopOfStack = *theItem; // clone the item
7369 theItem->quantity--;
7370 itemFromTopOfStack->quantity = 1;
7371 if (itemOnFloor) {
7372 itemOnFloor->inventoryLetter = theItem->inventoryLetter; // just in case all letters are taken
7373 pickUpItemAt(player.xLoc, player.yLoc);
7374 }
7375 placeItem(itemFromTopOfStack, player.xLoc, player.yLoc);
7376 return itemFromTopOfStack;
7377 } else { // drop the entire item
7378 if (rogue.swappedIn == theItem || rogue.swappedOut == theItem) {
7379 rogue.swappedIn = NULL;
7380 rogue.swappedOut = NULL;
7381 }
7382 removeItemFromChain(theItem, packItems);
7383 if (itemOnFloor) {
7384 itemOnFloor->inventoryLetter = theItem->inventoryLetter;
7385 pickUpItemAt(player.xLoc, player.yLoc);
7386 }
7387 placeItem(theItem, player.xLoc, player.yLoc);
7388 return theItem;
7389 }
7390 }
7391
recalculateEquipmentBonuses()7392 void recalculateEquipmentBonuses() {
7393 fixpt enchant;
7394 item *theItem;
7395 if (rogue.weapon) {
7396 theItem = rogue.weapon;
7397 enchant = netEnchant(theItem);
7398 player.info.damage = theItem->damage;
7399 player.info.damage.lowerBound = player.info.damage.lowerBound * damageFraction(enchant) / FP_FACTOR;
7400 player.info.damage.upperBound = player.info.damage.upperBound * damageFraction(enchant) / FP_FACTOR;
7401 if (player.info.damage.lowerBound < 1) {
7402 player.info.damage.lowerBound = 1;
7403 }
7404 if (player.info.damage.upperBound < 1) {
7405 player.info.damage.upperBound = 1;
7406 }
7407 }
7408
7409 if (rogue.armor) {
7410 theItem = rogue.armor;
7411 enchant = netEnchant(theItem);
7412 enchant -= player.status[STATUS_DONNING] * FP_FACTOR;
7413 player.info.defense = (theItem->armor * FP_FACTOR + enchant * 10) / FP_FACTOR;
7414 if (player.info.defense < 0) {
7415 player.info.defense = 0;
7416 }
7417 }
7418 }
7419
7420 // Returns true on success, false otherwise (for example, if failing to remove
7421 // a cursed item) If something must be first unequipped and it is not clear
7422 // what, unequipHint will be used if passed.
equipItem(item * theItem,boolean force,item * unequipHint)7423 boolean equipItem(item *theItem, boolean force, item *unequipHint) {
7424 char buf1[COLS * 3], buf2[COLS * 3], buf3[COLS * 3];
7425 item *previouslyEquippedItem = NULL;
7426
7427 if ((theItem->category & RING) && (theItem->flags & ITEM_EQUIPPED)) {
7428 return false;
7429 }
7430
7431 if (theItem->category & WEAPON) {
7432 previouslyEquippedItem = rogue.weapon;
7433 } else if (theItem->category & ARMOR) {
7434 previouslyEquippedItem = rogue.armor;
7435 } else if (theItem->category & RING
7436 && unequipHint && rogue.ringLeft && rogue.ringRight
7437 && (unequipHint == rogue.ringLeft || unequipHint == rogue.ringRight)) {
7438 previouslyEquippedItem = unequipHint;
7439 }
7440
7441 if (previouslyEquippedItem && !unequipItem(previouslyEquippedItem, force)) {
7442 return false; // already using a cursed item
7443 }
7444 if (theItem->category & WEAPON) {
7445 rogue.weapon = theItem;
7446 strengthCheck(theItem, !force);
7447 } else if (theItem->category & ARMOR) {
7448 if (!force) {
7449 player.status[STATUS_DONNING] = player.maxStatus[STATUS_DONNING] = theItem->armor / 10;
7450 }
7451 rogue.armor = theItem;
7452 strengthCheck(theItem, !force);
7453 } else if (theItem->category & RING) {
7454 if (rogue.ringLeft && rogue.ringRight) {
7455 return false; // no available ring slot and no hint, see equip()
7456 }
7457 if (rogue.ringLeft) {
7458 rogue.ringRight = theItem;
7459 } else {
7460 rogue.ringLeft = theItem;
7461 }
7462 updateRingBonuses();
7463 if (theItem->kind == RING_CLAIRVOYANCE) {
7464 updateClairvoyance();
7465 displayLevel();
7466 identifyItemKind(theItem);
7467 } else if (theItem->kind == RING_LIGHT
7468 || theItem->kind == RING_STEALTH) {
7469 identifyItemKind(theItem);
7470 }
7471 updateEncumbrance();
7472 }
7473 theItem->flags |= ITEM_EQUIPPED;
7474
7475 itemName(theItem, buf2, true, true, NULL);
7476
7477 if (!force) {
7478 if (previouslyEquippedItem) {
7479 itemName(previouslyEquippedItem, buf3, false, false, NULL);
7480 sprintf(buf1, "Now %s %s instead of your %s.", (theItem->category & WEAPON ? "wielding" : "wearing"), buf2, buf3);
7481 } else {
7482 sprintf(buf1, "Now %s %s.", (theItem->category & WEAPON ? "wielding" : "wearing"), buf2);
7483 }
7484
7485 confirmMessages();
7486 messageWithColor(buf1, &itemMessageColor, false);
7487
7488 if (theItem->flags & ITEM_CURSED) {
7489 itemName(theItem, buf2, false, false, NULL);
7490 switch(theItem->category) {
7491 case WEAPON:
7492 sprintf(buf1, "you wince as your grip involuntarily tightens around your %s.", buf2);
7493 break;
7494 case ARMOR:
7495 sprintf(buf1, "your %s constricts around you painfully.", buf2);
7496 break;
7497 case RING:
7498 sprintf(buf1, "your %s tightens around your finger painfully.", buf2);
7499 break;
7500 default:
7501 sprintf(buf1, "your %s seizes you with a malevolent force.", buf2);
7502 break;
7503 }
7504 messageWithColor(buf1, &itemMessageColor, 0);
7505 }
7506 }
7507
7508 return true;
7509 }
7510
7511 // Returns true on success, false otherwise (for example, if cursed and not forced)
unequipItem(item * theItem,boolean force)7512 boolean unequipItem(item *theItem, boolean force) {
7513 char buf[COLS * 3], buf2[COLS * 3];
7514
7515 if (theItem == NULL || !(theItem->flags & ITEM_EQUIPPED)) {
7516 return false;
7517 }
7518 if ((theItem->flags & ITEM_CURSED) && !force) {
7519 itemName(theItem, buf2, false, false, NULL);
7520 sprintf(buf, "you can't; your %s appear%s to be cursed.",
7521 buf2,
7522 theItem->quantity == 1 ? "s" : "");
7523 confirmMessages();
7524 messageWithColor(buf, &itemMessageColor, 0);
7525 return false;
7526 }
7527 theItem->flags &= ~ITEM_EQUIPPED;
7528 if (theItem->category & WEAPON) {
7529 player.info.damage.lowerBound = 1;
7530 player.info.damage.upperBound = 2;
7531 player.info.damage.clumpFactor = 1;
7532 rogue.weapon = NULL;
7533 }
7534 if (theItem->category & ARMOR) {
7535 player.info.defense = 0;
7536 rogue.armor = NULL;
7537 player.status[STATUS_DONNING] = 0;
7538 }
7539 if (theItem->category & RING) {
7540 if (rogue.ringLeft == theItem) {
7541 rogue.ringLeft = NULL;
7542 } else if (rogue.ringRight == theItem) {
7543 rogue.ringRight = NULL;
7544 }
7545 updateRingBonuses();
7546 if (theItem->kind == RING_CLAIRVOYANCE) {
7547 updateClairvoyance();
7548 updateFieldOfViewDisplay(false, false);
7549 updateClairvoyance(); // Yes, we have to call this a second time.
7550 displayLevel();
7551 }
7552 }
7553 updateEncumbrance();
7554 return true;
7555 }
7556
updateRingBonuses()7557 void updateRingBonuses() {
7558 short i;
7559 item *rings[2] = {rogue.ringLeft, rogue.ringRight};
7560
7561 rogue.clairvoyance = rogue.stealthBonus = rogue.transference
7562 = rogue.awarenessBonus = rogue.regenerationBonus = rogue.wisdomBonus = rogue.reaping = 0;
7563 rogue.lightMultiplier = 1;
7564
7565 for (i=0; i<= 1; i++) {
7566 if (rings[i]) {
7567 switch (rings[i]->kind) {
7568 case RING_CLAIRVOYANCE:
7569 rogue.clairvoyance += effectiveRingEnchant(rings[i]);
7570 break;
7571 case RING_STEALTH:
7572 rogue.stealthBonus += effectiveRingEnchant(rings[i]);
7573 break;
7574 case RING_REGENERATION:
7575 rogue.regenerationBonus += effectiveRingEnchant(rings[i]);
7576 break;
7577 case RING_TRANSFERENCE:
7578 rogue.transference += effectiveRingEnchant(rings[i]);
7579 break;
7580 case RING_LIGHT:
7581 rogue.lightMultiplier += effectiveRingEnchant(rings[i]);
7582 break;
7583 case RING_AWARENESS:
7584 rogue.awarenessBonus += 20 * effectiveRingEnchant(rings[i]);
7585 break;
7586 case RING_WISDOM:
7587 rogue.wisdomBonus += effectiveRingEnchant(rings[i]);
7588 break;
7589 case RING_REAPING:
7590 rogue.reaping += effectiveRingEnchant(rings[i]);
7591 break;
7592 }
7593 }
7594 }
7595
7596 if (rogue.lightMultiplier <= 0) {
7597 rogue.lightMultiplier--; // because it starts at positive 1 instead of 0
7598 }
7599
7600 updateMinersLightRadius();
7601 updatePlayerRegenerationDelay();
7602
7603 if (rogue.stealthBonus < 0) {
7604 rogue.stealthBonus *= 4;
7605 }
7606 }
7607
updatePlayerRegenerationDelay()7608 void updatePlayerRegenerationDelay() {
7609 short maxHP;
7610 long turnsForFull; // In thousandths of a turn.
7611 maxHP = player.info.maxHP;
7612 turnsForFull = turnsForFullRegenInThousandths(rogue.regenerationBonus * FP_FACTOR);
7613
7614 player.regenPerTurn = 0;
7615 while (maxHP > turnsForFull / 1000) {
7616 player.regenPerTurn++;
7617 maxHP -= turnsForFull / 1000;
7618 }
7619
7620 player.info.turnsBetweenRegen = (turnsForFull / maxHP);
7621 // DEBUG printf("\nTurnsForFull: %i; regenPerTurn: %i; (thousandths of) turnsBetweenRegen: %i", turnsForFull, player.regenPerTurn, player.info.turnsBetweenRegen);
7622 }
7623
removeItemFromChain(item * theItem,item * theChain)7624 boolean removeItemFromChain(item *theItem, item *theChain) {
7625 item *previousItem;
7626
7627 for (previousItem = theChain;
7628 previousItem->nextItem;
7629 previousItem = previousItem->nextItem) {
7630 if (previousItem->nextItem == theItem) {
7631 previousItem->nextItem = theItem->nextItem;
7632 return true;
7633 }
7634 }
7635 return false;
7636 }
7637
addItemToChain(item * theItem,item * theChain)7638 void addItemToChain(item *theItem, item *theChain) {
7639 theItem->nextItem = theChain->nextItem;
7640 theChain->nextItem = theItem;
7641 }
7642
deleteItem(item * theItem)7643 void deleteItem(item *theItem) {
7644 free(theItem);
7645 }
7646
resetItemTableEntry(itemTable * theEntry)7647 void resetItemTableEntry(itemTable *theEntry) {
7648 theEntry->identified = false;
7649 theEntry->called = false;
7650 theEntry->callTitle[0] = '\0';
7651 }
7652
shuffleFlavors()7653 void shuffleFlavors() {
7654 short i, j, randIndex, randNumber;
7655 char buf[COLS];
7656
7657 for (i=0; i<NUMBER_POTION_KINDS; i++) {
7658 resetItemTableEntry(potionTable + i);
7659 }
7660 for (i=0; i<NUMBER_STAFF_KINDS; i++) {
7661 resetItemTableEntry(staffTable+ i);
7662 }
7663 for (i=0; i<NUMBER_WAND_KINDS; i++) {
7664 resetItemTableEntry(wandTable + i);
7665 }
7666 for (i=0; i<NUMBER_SCROLL_KINDS; i++) {
7667 resetItemTableEntry(scrollTable + i);
7668 }
7669 for (i=0; i<NUMBER_RING_KINDS; i++) {
7670 resetItemTableEntry(ringTable + i);
7671 }
7672
7673 for (i=0; i<NUMBER_ITEM_COLORS; i++) {
7674 strcpy(itemColors[i], itemColorsRef[i]);
7675 }
7676 for (i=0; i<NUMBER_ITEM_COLORS; i++) {
7677 randIndex = rand_range(0, NUMBER_ITEM_COLORS - 1);
7678 if (randIndex != i) {
7679 strcpy(buf, itemColors[i]);
7680 strcpy(itemColors[i], itemColors[randIndex]);
7681 strcpy(itemColors[randIndex], buf);
7682 }
7683 }
7684
7685 for (i=0; i<NUMBER_ITEM_WOODS; i++) {
7686 strcpy(itemWoods[i], itemWoodsRef[i]);
7687 }
7688 for (i=0; i<NUMBER_ITEM_WOODS; i++) {
7689 randIndex = rand_range(0, NUMBER_ITEM_WOODS - 1);
7690 if (randIndex != i) {
7691 strcpy(buf, itemWoods[i]);
7692 strcpy(itemWoods[i], itemWoods[randIndex]);
7693 strcpy(itemWoods[randIndex], buf);
7694 }
7695 }
7696
7697 for (i=0; i<NUMBER_ITEM_GEMS; i++) {
7698 strcpy(itemGems[i], itemGemsRef[i]);
7699 }
7700 for (i=0; i<NUMBER_ITEM_GEMS; i++) {
7701 randIndex = rand_range(0, NUMBER_ITEM_GEMS - 1);
7702 if (randIndex != i) {
7703 strcpy(buf, itemGems[i]);
7704 strcpy(itemGems[i], itemGems[randIndex]);
7705 strcpy(itemGems[randIndex], buf);
7706 }
7707 }
7708
7709 for (i=0; i<NUMBER_ITEM_METALS; i++) {
7710 strcpy(itemMetals[i], itemMetalsRef[i]);
7711 }
7712 for (i=0; i<NUMBER_ITEM_METALS; i++) {
7713 randIndex = rand_range(0, NUMBER_ITEM_METALS - 1);
7714 if (randIndex != i) {
7715 strcpy(buf, itemMetals[i]);
7716 strcpy(itemMetals[i], itemMetals[randIndex]);
7717 strcpy(itemMetals[randIndex], buf);
7718 }
7719 }
7720
7721 for (i=0; i<NUMBER_SCROLL_KINDS; i++) {
7722 itemTitles[i][0] = '\0';
7723 randNumber = rand_range(3, 4);
7724 for (j=0; j<randNumber; j++) {
7725 randIndex = rand_range(0, NUMBER_TITLE_PHONEMES - 1);
7726 strcpy(buf, itemTitles[i]);
7727 sprintf(itemTitles[i], "%s%s%s", buf, ((rand_percent(50) && j>0) ? " " : ""), titlePhonemes[randIndex]);
7728 }
7729 }
7730 }
7731
itemValue(item * theItem)7732 unsigned long itemValue(item *theItem) {
7733 switch (theItem->category) {
7734 case AMULET:
7735 return 35000;
7736 break;
7737 case GEM:
7738 return 5000 * theItem->quantity;
7739 break;
7740 default:
7741 return 0;
7742 break;
7743 }
7744 }
7745