1 /*
2  *  Combat.c
3  *  Brogue
4  *
5  *  Created by Brian Walker on 6/11/09.
6  *  Copyright 2012. All rights reserved.
7  *
8  *  This file is part of Brogue.
9  *
10  *  This program is free software: you can redistribute it and/or modify
11  *  it under the terms of the GNU Affero General Public License as
12  *  published by the Free Software Foundation, either version 3 of the
13  *  License, or (at your option) any later version.
14  *
15  *  This program is distributed in the hope that it will be useful,
16  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  *  GNU Affero General Public License for more details.
19  *
20  *  You should have received a copy of the GNU Affero General Public License
21  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  */
23 
24 #include "Rogue.h"
25 #include "IncludeGlobals.h"
26 
27 
28 /* Combat rules:
29  * Each combatant has an accuracy rating. This is the percentage of their attacks that will ordinarily hit;
30  * higher numbers are better for them. Numbers over 100 are permitted.
31  *
32  * Each combatant also has a defense rating. The "hit probability" is calculated as given by this formula:
33  *
34  *          hit probability = (accuracy) * 0.987 ^ (defense)
35  *
36  * when hit determinations are made. Negative numbers and numbers over 100 are permitted.
37  * The hit is then randomly determined according to this final percentage.
38  *
39  * Some environmental factors can modify these numbers. An unaware, sleeping, stuck or paralyzed
40  * combatant is always hit. An unaware, sleeping or paralyzed combatant also takes treble damage.
41  *
42  * If the hit lands, damage is calculated in the range provided. However, the clumping factor affects the
43  * probability distribution. If the range is 0-10 with a clumping factor of 1, it's a uniform distribution.
44  * With a clumping factor of 2, it's calculated as 2d5 (with d5 meaing a die numbered from 0 through 5).
45  * With 3, it's 3d3, and so on. Note that a range not divisible by the clumping factor is defective,
46  * as it will never be resolved in the top few numbers of the range. In fact, the top
47  * (rangeWidth % clumpingFactor) will never succeed. Thus we increment the maximum of the first
48  * (rangeWidth % clumpingFactor) die by 1, so that in fact 0-10 with a CF of 3 would be 1d4 + 2d3. Similarly,
49  * 0-10 with CF 4 would be 2d3 + 2d2. By playing with the numbers, one can approximate a gaussian
50  * distribution of any mean and standard deviation.
51  *
52  * Player combatants take their base defense value of their actual armor. Their accuracy is a combination of weapon, armor
53  * and strength.
54  *
55  * Players have a base accuracy value of 100 throughout the game. Each point of weapon enchantment (net of
56  * strength penalty/benefit) increases
57  */
58 
strengthModifier(item * theItem)59 fixpt strengthModifier(item *theItem) {
60     int difference = (rogue.strength - player.weaknessAmount) - theItem->strengthRequired;
61     if (difference > 0) {
62         return difference * FP_FACTOR / 4; // 0.25x
63     } else {
64         return difference * FP_FACTOR * 5/2; // 2.5x
65     }
66 }
67 
netEnchant(item * theItem)68 fixpt netEnchant(item *theItem) {
69     fixpt retval = theItem->enchant1 * FP_FACTOR;
70     if (theItem->category & (WEAPON | ARMOR)) {
71         retval += strengthModifier(theItem);
72     }
73     // Clamp all net enchantment values to [-20, 50].
74     return clamp(retval, -20 * FP_FACTOR, 50 * FP_FACTOR);
75 }
76 
monsterDamageAdjustmentAmount(const creature * monst)77 fixpt monsterDamageAdjustmentAmount(const creature *monst) {
78     if (monst == &player) {
79         // Handled through player strength routines elsewhere.
80         return FP_FACTOR;
81     } else {
82         return damageFraction(monst->weaknessAmount * FP_FACTOR * -3/2);
83     }
84 }
85 
monsterDefenseAdjusted(const creature * monst)86 short monsterDefenseAdjusted(const creature *monst) {
87     short retval;
88     if (monst == &player) {
89         // Weakness is already taken into account in recalculateEquipmentBonuses() for the player.
90         retval = monst->info.defense;
91     } else {
92         retval = monst->info.defense - 25 * monst->weaknessAmount;
93     }
94     return max(retval, 0);
95 }
96 
monsterAccuracyAdjusted(const creature * monst)97 short monsterAccuracyAdjusted(const creature *monst) {
98     short retval = monst->info.accuracy * accuracyFraction(monst->weaknessAmount * FP_FACTOR * -3/2) / FP_FACTOR;
99     return max(retval, 0);
100 }
101 
102 // does NOT account for auto-hit from sleeping or unaware defenders; does account for auto-hit from
103 // stuck or captive defenders and from weapons of slaying.
hitProbability(creature * attacker,creature * defender)104 short hitProbability(creature *attacker, creature *defender) {
105     short accuracy = monsterAccuracyAdjusted(attacker);
106     short defense = monsterDefenseAdjusted(defender);
107     short hitProbability;
108 
109     if (defender->status[STATUS_STUCK] || (defender->bookkeepingFlags & MB_CAPTIVE)) {
110         return 100;
111     }
112     if ((defender->bookkeepingFlags & MB_SEIZED)
113         && (attacker->bookkeepingFlags & MB_SEIZING)) {
114 
115         return 100;
116     }
117     if (attacker == &player && rogue.weapon) {
118         if ((rogue.weapon->flags & ITEM_RUNIC)
119             && rogue.weapon->enchant2 == W_SLAYING
120             && monsterIsInClass(defender, rogue.weapon->vorpalEnemy)) {
121 
122             return 100;
123         }
124         accuracy = player.info.accuracy * accuracyFraction(netEnchant(rogue.weapon)) / FP_FACTOR;
125     }
126     hitProbability = accuracy * defenseFraction(defense * FP_FACTOR) / FP_FACTOR;
127     if (hitProbability > 100) {
128         hitProbability = 100;
129     } else if (hitProbability < 0) {
130         hitProbability = 0;
131     }
132     return hitProbability;
133 }
134 
attackHit(creature * attacker,creature * defender)135 boolean attackHit(creature *attacker, creature *defender) {
136     // automatically hit if the monster is sleeping or captive or stuck in a web
137     if (defender->status[STATUS_STUCK]
138         || defender->status[STATUS_PARALYZED]
139         || (defender->bookkeepingFlags & MB_CAPTIVE)) {
140 
141         return true;
142     }
143 
144     return rand_percent(hitProbability(attacker, defender));
145 }
146 
addMonsterToContiguousMonsterGrid(short x,short y,creature * monst,char grid[DCOLS][DROWS])147 void addMonsterToContiguousMonsterGrid(short x, short y, creature *monst, char grid[DCOLS][DROWS]) {
148     short newX, newY;
149     enum directions dir;
150     creature *tempMonst;
151 
152     grid[x][y] = true;
153     for (dir=0; dir<4; dir++) {
154         newX = x + nbDirs[dir][0];
155         newY = y + nbDirs[dir][1];
156 
157         if (coordinatesAreInMap(newX, newY) && !grid[newX][newY]) {
158             tempMonst = monsterAtLoc(newX, newY);
159             if (tempMonst && monstersAreTeammates(monst, tempMonst)) {
160                 addMonsterToContiguousMonsterGrid(newX, newY, monst, grid);
161             }
162         }
163     }
164 }
165 
166 // Splits a monster in half.
167 // The split occurs only if there is a spot adjacent to the contiguous
168 // group of monsters that the monster would not avoid.
169 // The contiguous group is supplemented with the given (x, y) coordinates, if any;
170 // this is so that jellies et al. can spawn behind the player in a hallway.
splitMonster(creature * monst,short x,short y)171 void splitMonster(creature *monst, short x, short y) {
172     short i, j, b, dir, newX, newY, eligibleLocationCount, randIndex;
173     char buf[DCOLS * 3];
174     char monstName[DCOLS];
175     char monsterGrid[DCOLS][DROWS], eligibleGrid[DCOLS][DROWS];
176     creature *clone;
177 
178     zeroOutGrid(monsterGrid);
179     zeroOutGrid(eligibleGrid);
180     eligibleLocationCount = 0;
181 
182     // Add the (x, y) location to the contiguous group, if any.
183     if (x > 0 && y > 0) {
184         monsterGrid[x][y] = true;
185     }
186 
187     // Find the contiguous group of monsters.
188     addMonsterToContiguousMonsterGrid(monst->xLoc, monst->yLoc, monst, monsterGrid);
189 
190     // Find the eligible edges around the group of monsters.
191     for (i=0; i<DCOLS; i++) {
192         for (j=0; j<DROWS; j++) {
193             if (monsterGrid[i][j]) {
194                 for (dir=0; dir<4; dir++) {
195                     newX = i + nbDirs[dir][0];
196                     newY = j + nbDirs[dir][1];
197                     if (coordinatesAreInMap(newX, newY)
198                         && !eligibleGrid[newX][newY]
199                         && !monsterGrid[newX][newY]
200                         && !(pmap[newX][newY].flags & (HAS_PLAYER | HAS_MONSTER))
201                         && !monsterAvoids(monst, newX, newY)) {
202 
203                         eligibleGrid[newX][newY] = true;
204                         eligibleLocationCount++;
205                     }
206                 }
207             }
208         }
209     }
210 //    DEBUG {
211 //        hiliteCharGrid(eligibleGrid, &green, 75);
212 //        hiliteCharGrid(monsterGrid, &blue, 75);
213 //        temporaryMessage("Jelly spawn possibilities (green = eligible, blue = monster):", REQUIRE_ACKNOWLEDGMENT);
214 //        displayLevel();
215 //    }
216 
217     // Pick a random location on the eligibleGrid and add the clone there.
218     if (eligibleLocationCount) {
219         randIndex = rand_range(1, eligibleLocationCount);
220         for (i=0; i<DCOLS; i++) {
221             for (j=0; j<DROWS; j++) {
222                 if (eligibleGrid[i][j] && !--randIndex) {
223                     // Found the spot!
224 
225                     monsterName(monstName, monst, true);
226                     monst->currentHP = (monst->currentHP + 1) / 2;
227                     clone = cloneMonster(monst, false, false);
228 
229                     // Split monsters don't inherit the learnings of their parents.
230                     // Sorry, but self-healing jelly armies are too much.
231                     // Mutation effects can be inherited, however; they're not learned abilities.
232                     if (monst->mutationIndex >= 0) {
233                         clone->info.flags           &= (monsterCatalog[clone->info.monsterID].flags | mutationCatalog[monst->mutationIndex].monsterFlags);
234                         clone->info.abilityFlags    &= (monsterCatalog[clone->info.monsterID].abilityFlags | mutationCatalog[monst->mutationIndex].monsterAbilityFlags);
235                     } else {
236                         clone->info.flags           &= monsterCatalog[clone->info.monsterID].flags;
237                         clone->info.abilityFlags    &= monsterCatalog[clone->info.monsterID].abilityFlags;
238                     }
239                     for (b = 0; b < 20; b++) {
240                         clone->info.bolts[b] = monsterCatalog[clone->info.monsterID].bolts[b];
241                     }
242 
243                     if (!(clone->info.flags & MONST_FLIES)
244                         && clone->status[STATUS_LEVITATING] == 1000) {
245 
246                         clone->status[STATUS_LEVITATING] = 0;
247                     }
248 
249                     clone->xLoc = i;
250                     clone->yLoc = j;
251                     pmap[i][j].flags |= HAS_MONSTER;
252                     clone->ticksUntilTurn = max(clone->ticksUntilTurn, 101);
253                     fadeInMonster(clone);
254                     refreshSideBar(-1, -1, false);
255 
256                     if (canDirectlySeeMonster(monst)) {
257                         sprintf(buf, "%s splits in two!", monstName);
258                         message(buf, 0);
259                     }
260 
261                     return;
262                 }
263             }
264         }
265     }
266 }
267 
alliedCloneCount(creature * monst)268 short alliedCloneCount(creature *monst) {
269     short count = 0;
270     for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
271         creature *temp = nextCreature(&it);
272         if (temp != monst
273             && temp->info.monsterID == monst->info.monsterID
274             && monstersAreTeammates(temp, monst)) {
275 
276             count++;
277         }
278     }
279     if (rogue.depthLevel > 1) {
280         for (creatureIterator it = iterateCreatures(&levels[rogue.depthLevel - 2].monsters); hasNextCreature(it);) {
281             creature *temp = nextCreature(&it);
282             if (temp != monst
283                 && temp->info.monsterID == monst->info.monsterID
284                 && monstersAreTeammates(temp, monst)) {
285 
286                 count++;
287             }
288         }
289     }
290     if (rogue.depthLevel < DEEPEST_LEVEL) {
291         for (creatureIterator it = iterateCreatures(&levels[rogue.depthLevel].monsters); hasNextCreature(it);) {
292             creature *temp = nextCreature(&it);
293             if (temp != monst
294                 && temp->info.monsterID == monst->info.monsterID
295                 && monstersAreTeammates(temp, monst)) {
296 
297                 count++;
298             }
299         }
300     }
301     return count;
302 }
303 
304 // This function is called whenever one creature acts aggressively against another in a way that directly causes damage.
305 // This can be things like melee attacks, fire/lightning attacks or throwing a weapon.
moralAttack(creature * attacker,creature * defender)306 void moralAttack(creature *attacker, creature *defender) {
307 
308     if (attacker == &player && canSeeMonster(defender)) {
309         rogue.featRecord[FEAT_PACIFIST] = false;
310         if (defender->creatureState != MONSTER_TRACKING_SCENT) {
311             rogue.featRecord[FEAT_PALADIN] = false;
312         }
313     }
314 
315     if (defender->currentHP > 0
316         && !(defender->bookkeepingFlags & MB_IS_DYING)) {
317 
318         if (defender->status[STATUS_PARALYZED]) {
319             defender->status[STATUS_PARALYZED] = 0;
320              // Paralyzed creature gets a turn to react before the attacker moves again.
321             defender->ticksUntilTurn = min(attacker->attackSpeed, 100) - 1;
322         }
323         if (defender->status[STATUS_MAGICAL_FEAR]) {
324             defender->status[STATUS_MAGICAL_FEAR] = 1;
325         }
326         defender->status[STATUS_ENTRANCED] = 0;
327 
328         if ((defender->info.abilityFlags & MA_AVOID_CORRIDORS)) {
329             defender->status[STATUS_ENRAGED] = defender->maxStatus[STATUS_ENRAGED] = 4;
330         }
331 
332         if (attacker == &player
333             && defender->creatureState == MONSTER_ALLY
334             && !defender->status[STATUS_DISCORDANT]
335             && !attacker->status[STATUS_CONFUSED]
336             && !(attacker->bookkeepingFlags & MB_IS_DYING)) {
337 
338             unAlly(defender);
339         }
340 
341         if ((attacker == &player || attacker->creatureState == MONSTER_ALLY)
342             && defender != &player
343             && defender->creatureState != MONSTER_ALLY) {
344 
345             alertMonster(defender); // this alerts the monster that you're nearby
346         }
347 
348         if ((defender->info.abilityFlags & MA_CLONE_SELF_ON_DEFEND) && alliedCloneCount(defender) < 100) {
349             if (distanceBetween(defender->xLoc, defender->yLoc, attacker->xLoc, attacker->yLoc) <= 1) {
350                 splitMonster(defender, attacker->xLoc, attacker->yLoc);
351             } else {
352                 splitMonster(defender, 0, 0);
353             }
354         }
355     }
356 }
357 
playerImmuneToMonster(creature * monst)358 boolean playerImmuneToMonster(creature *monst) {
359     if (monst != &player
360         && rogue.armor
361         && (rogue.armor->flags & ITEM_RUNIC)
362         && (rogue.armor->enchant2 == A_IMMUNITY)
363         && monsterIsInClass(monst, rogue.armor->vorpalEnemy)) {
364 
365         return true;
366     } else {
367         return false;
368     }
369 }
370 
specialHit(creature * attacker,creature * defender,short damage)371 void specialHit(creature *attacker, creature *defender, short damage) {
372     short itemCandidates, randItemIndex, stolenQuantity;
373     item *theItem = NULL, *itemFromTopOfStack;
374     char buf[COLS], buf2[COLS], buf3[COLS];
375 
376     if (!(attacker->info.abilityFlags & SPECIAL_HIT)) {
377         return;
378     }
379 
380     // Special hits that can affect only the player:
381     if (defender == &player) {
382         if (playerImmuneToMonster(attacker)) {
383             return;
384         }
385 
386         if (attacker->info.abilityFlags & MA_HIT_DEGRADE_ARMOR
387             && defender == &player
388             && rogue.armor
389             && !(rogue.armor->flags & ITEM_PROTECTED)
390             && (rogue.armor->enchant1 + rogue.armor->armor/10 > -10)) {
391 
392             rogue.armor->enchant1--;
393             equipItem(rogue.armor, true, NULL);
394             itemName(rogue.armor, buf2, false, false, NULL);
395             sprintf(buf, "your %s weakens!", buf2);
396             messageWithColor(buf, &itemMessageColor, 0);
397             checkForDisenchantment(rogue.armor);
398         }
399         if (attacker->info.abilityFlags & MA_HIT_HALLUCINATE) {
400             if (!player.status[STATUS_HALLUCINATING]) {
401                 combatMessage("you begin to hallucinate", 0);
402             }
403             if (!player.status[STATUS_HALLUCINATING]) {
404                 player.maxStatus[STATUS_HALLUCINATING] = 0;
405             }
406             player.status[STATUS_HALLUCINATING] += 20;
407             player.maxStatus[STATUS_HALLUCINATING] = max(player.maxStatus[STATUS_HALLUCINATING], player.status[STATUS_HALLUCINATING]);
408         }
409         if (attacker->info.abilityFlags & MA_HIT_BURN
410              && !defender->status[STATUS_IMMUNE_TO_FIRE]) {
411 
412             exposeCreatureToFire(defender);
413         }
414 
415         if (attacker->info.abilityFlags & MA_HIT_STEAL_FLEE
416             && !(attacker->carriedItem)
417             && (packItems->nextItem)
418             && attacker->currentHP > 0
419             && !attacker->status[STATUS_CONFUSED] // No stealing from the player if you bump him while confused.
420             && attackHit(attacker, defender)) {
421 
422             itemCandidates = numberOfMatchingPackItems(ALL_ITEMS, 0, (ITEM_EQUIPPED), false);
423             if (itemCandidates) {
424                 randItemIndex = rand_range(1, itemCandidates);
425                 for (theItem = packItems->nextItem; theItem != NULL; theItem = theItem->nextItem) {
426                     if (!(theItem->flags & (ITEM_EQUIPPED))) {
427                         if (randItemIndex == 1) {
428                             break;
429                         } else {
430                             randItemIndex--;
431                         }
432                     }
433                 }
434                 if (theItem) {
435                     if (theItem->category & WEAPON) { // Monkeys will steal half of a stack of weapons, and one of any other stack.
436                         if (theItem->quantity > 3) {
437                             stolenQuantity = (theItem->quantity + 1) / 2;
438                         } else {
439                             stolenQuantity = theItem->quantity;
440                         }
441                     } else {
442                         stolenQuantity = 1;
443                     }
444                     if (stolenQuantity < theItem->quantity) { // Peel off stolen item(s).
445                         itemFromTopOfStack = generateItem(ALL_ITEMS, -1);
446                         *itemFromTopOfStack = *theItem; // Clone the item.
447                         theItem->quantity -= stolenQuantity;
448                         itemFromTopOfStack->quantity = stolenQuantity;
449                         theItem = itemFromTopOfStack; // Redirect pointer.
450                     } else {
451                         if (rogue.swappedIn == theItem || rogue.swappedOut == theItem) {
452                             rogue.swappedIn = NULL;
453                             rogue.swappedOut = NULL;
454                         }
455                         removeItemFromChain(theItem, packItems);
456                     }
457                     theItem->flags &= ~ITEM_PLAYER_AVOIDS; // Explore will seek the item out if it ends up on the floor again.
458                     attacker->carriedItem = theItem;
459                     attacker->creatureMode = MODE_PERM_FLEEING;
460                     attacker->creatureState = MONSTER_FLEEING;
461                     monsterName(buf2, attacker, true);
462                     itemName(theItem, buf3, false, true, NULL);
463                     sprintf(buf, "%s stole %s!", buf2, buf3);
464                     messageWithColor(buf, &badMessageColor, 0);
465                 }
466             }
467         }
468     }
469     if ((attacker->info.abilityFlags & MA_POISONS)
470         && damage > 0
471         && !(defender->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
472 
473         addPoison(defender, damage, 1);
474     }
475     if ((attacker->info.abilityFlags & MA_CAUSES_WEAKNESS)
476         && damage > 0
477         && !(defender->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
478 
479         weaken(defender, 300);
480     }
481     if (attacker->info.abilityFlags & MA_ATTACKS_STAGGER) {
482         processStaggerHit(attacker, defender);
483     }
484 }
485 
forceWeaponHit(creature * defender,item * theItem)486 boolean forceWeaponHit(creature *defender, item *theItem) {
487     short oldLoc[2], newLoc[2], forceDamage;
488     char buf[DCOLS*3], buf2[COLS], monstName[DCOLS];
489     creature *otherMonster = NULL;
490     boolean knowFirstMonsterDied = false, autoID = false;
491     bolt theBolt;
492 
493     monsterName(monstName, defender, true);
494 
495     oldLoc[0] = defender->xLoc;
496     oldLoc[1] = defender->yLoc;
497     newLoc[0] = defender->xLoc + clamp(defender->xLoc - player.xLoc, -1, 1);
498     newLoc[1] = defender->yLoc + clamp(defender->yLoc - player.yLoc, -1, 1);
499     if (canDirectlySeeMonster(defender)
500         && !cellHasTerrainFlag(newLoc[0], newLoc[1], T_OBSTRUCTS_PASSABILITY | T_OBSTRUCTS_VISION)
501         && !(pmap[newLoc[0]][newLoc[1]].flags & (HAS_MONSTER | HAS_PLAYER))) {
502         sprintf(buf, "you launch %s backward with the force of your blow", monstName);
503         buf[DCOLS] = '\0';
504         combatMessage(buf, messageColorFromVictim(defender));
505         autoID = true;
506     }
507     theBolt = boltCatalog[BOLT_BLINKING];
508     theBolt.magnitude = max(1, netEnchant(theItem) / FP_FACTOR);
509     zap(oldLoc, newLoc, &theBolt, false);
510     if (!(defender->bookkeepingFlags & MB_IS_DYING)
511         && distanceBetween(oldLoc[0], oldLoc[1], defender->xLoc, defender->yLoc) > 0
512         && distanceBetween(oldLoc[0], oldLoc[1], defender->xLoc, defender->yLoc) < weaponForceDistance(netEnchant(theItem))) {
513 
514         if (pmap[defender->xLoc + newLoc[0] - oldLoc[0]][defender->yLoc + newLoc[1] - oldLoc[1]].flags & (HAS_MONSTER | HAS_PLAYER)) {
515             otherMonster = monsterAtLoc(defender->xLoc + newLoc[0] - oldLoc[0], defender->yLoc + newLoc[1] - oldLoc[1]);
516             monsterName(buf2, otherMonster, true);
517         } else {
518             otherMonster = NULL;
519             strcpy(buf2, tileCatalog[pmap[defender->xLoc + newLoc[0] - oldLoc[0]][defender->yLoc + newLoc[1] - oldLoc[1]].layers[highestPriorityLayer(defender->xLoc + newLoc[0] - oldLoc[0], defender->yLoc + newLoc[1] - oldLoc[1], true)]].description);
520         }
521 
522         forceDamage = distanceBetween(oldLoc[0], oldLoc[1], defender->xLoc, defender->yLoc);
523 
524         if (!(defender->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE))
525             && inflictDamage(NULL, defender, forceDamage, &white, false)) {
526 
527             if (canDirectlySeeMonster(defender)) {
528                 knowFirstMonsterDied = true;
529                 sprintf(buf, "%s %s on impact with %s",
530                         monstName,
531                         (defender->info.flags & MONST_INANIMATE) ? "is destroyed" : "dies",
532                         buf2);
533                 buf[DCOLS] = '\0';
534                 combatMessage(buf, messageColorFromVictim(defender));
535                 autoID = true;
536             }
537         } else {
538             if (canDirectlySeeMonster(defender)) {
539                 sprintf(buf, "%s slams against %s",
540                         monstName,
541                         buf2);
542                 buf[DCOLS] = '\0';
543                 combatMessage(buf, messageColorFromVictim(defender));
544                 autoID = true;
545             }
546         }
547         moralAttack(&player, defender);
548 
549         if (otherMonster
550             && !(defender->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE))) {
551 
552             if (inflictDamage(NULL, otherMonster, forceDamage, &white, false)) {
553                 if (canDirectlySeeMonster(otherMonster)) {
554                     sprintf(buf, "%s %s%s when %s slams into $HIMHER",
555                             buf2,
556                             (knowFirstMonsterDied ? "also " : ""),
557                             (defender->info.flags & MONST_INANIMATE) ? "is destroyed" : "dies",
558                             monstName);
559                     resolvePronounEscapes(buf, otherMonster);
560                     buf[DCOLS] = '\0';
561                     combatMessage(buf, messageColorFromVictim(otherMonster));
562                     autoID = true;
563                 }
564             }
565             if (otherMonster->creatureState != MONSTER_ALLY) {
566                 // Allies won't defect if you throw another monster at them, even though it hurts.
567                 moralAttack(&player, otherMonster);
568             }
569         }
570     }
571     return autoID;
572 }
573 
magicWeaponHit(creature * defender,item * theItem,boolean backstabbed)574 void magicWeaponHit(creature *defender, item *theItem, boolean backstabbed) {
575     char buf[DCOLS*3], monstName[DCOLS], theItemName[DCOLS];
576 
577     color *effectColors[NUMBER_WEAPON_RUNIC_KINDS] = {&white, &black,
578         &yellow, &pink, &green, &confusionGasColor, NULL, NULL, &darkRed, &rainbow};
579     //  W_SPEED, W_QUIETUS, W_PARALYSIS, W_MULTIPLICITY, W_SLOWING, W_CONFUSION, W_FORCE, W_SLAYING, W_MERCY, W_PLENTY
580     short chance, i;
581     fixpt enchant;
582     enum weaponEnchants enchantType = theItem->enchant2;
583     creature *newMonst;
584     boolean autoID = false;
585 
586     // If the defender is already dead, proceed only if the runic is speed or multiplicity.
587     // (Everything else acts on the victim, which would literally be overkill.)
588     if ((defender->bookkeepingFlags & MB_IS_DYING)
589         && theItem->enchant2 != W_SPEED
590         && theItem->enchant2 != W_MULTIPLICITY) {
591         return;
592     }
593 
594     enchant = netEnchant(theItem);
595 
596     if (theItem->enchant2 == W_SLAYING) {
597         chance = (monsterIsInClass(defender, theItem->vorpalEnemy) ? 100 : 0);
598     } else if (defender->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE)) {
599         chance = 0;
600     } else {
601         chance = runicWeaponChance(theItem, false, 0);
602         if (backstabbed && chance < 100) {
603             chance = min(chance * 2, (chance + 100) / 2);
604         }
605     }
606     if (chance > 0 && rand_percent(chance)) {
607         if (!(defender->bookkeepingFlags & MB_SUBMERGED)) {
608             switch (enchantType) {
609                 case W_SPEED:
610                     createFlare(player.xLoc, player.yLoc, SCROLL_ENCHANTMENT_LIGHT);
611                     break;
612                 case W_QUIETUS:
613                     createFlare(defender->xLoc, defender->yLoc, QUIETUS_FLARE_LIGHT);
614                     break;
615                 case W_SLAYING:
616                     createFlare(defender->xLoc, defender->yLoc, SLAYING_FLARE_LIGHT);
617                     break;
618                 default:
619                     flashMonster(defender, effectColors[enchantType], 100);
620                     break;
621             }
622             autoID = true;
623         }
624         rogue.disturbed = true;
625         monsterName(monstName, defender, true);
626         itemName(theItem, theItemName, false, false, NULL);
627 
628         switch (enchantType) {
629             case W_SPEED:
630                 if (player.ticksUntilTurn != -1) {
631                     sprintf(buf, "your %s trembles and time freezes for a moment", theItemName);
632                     buf[DCOLS] = '\0';
633                     combatMessage(buf, 0);
634                     player.ticksUntilTurn = -1; // free turn!
635                     autoID = true;
636                 }
637                 break;
638             case W_SLAYING:
639             case W_QUIETUS:
640                 inflictLethalDamage(&player, defender);
641                 sprintf(buf, "%s suddenly %s",
642                         monstName,
643                         (defender->info.flags & MONST_INANIMATE) ? "shatters" : "dies");
644                 buf[DCOLS] = '\0';
645                 combatMessage(buf, messageColorFromVictim(defender));
646                 autoID = true;
647                 break;
648             case W_PARALYSIS:
649                 defender->status[STATUS_PARALYZED] = max(defender->status[STATUS_PARALYZED], weaponParalysisDuration(enchant));
650                 defender->maxStatus[STATUS_PARALYZED] = defender->status[STATUS_PARALYZED];
651                 if (canDirectlySeeMonster(defender)) {
652                     sprintf(buf, "%s is frozen in place", monstName);
653                     buf[DCOLS] = '\0';
654                     combatMessage(buf, messageColorFromVictim(defender));
655                     autoID = true;
656                 }
657                 break;
658             case W_MULTIPLICITY:
659                 sprintf(buf, "Your %s emits a flash of light, and %sspectral duplicate%s appear%s!",
660                         theItemName,
661                         (weaponImageCount(enchant) == 1 ? "a " : ""),
662                         (weaponImageCount(enchant) == 1 ? "" : "s"),
663                         (weaponImageCount(enchant) == 1 ? "s" : ""));
664                 buf[DCOLS] = '\0';
665 
666                 for (i = 0; i < (weaponImageCount(enchant)); i++) {
667                     newMonst = generateMonster(MK_SPECTRAL_IMAGE, true, false);
668                     getQualifyingPathLocNear(&(newMonst->xLoc), &(newMonst->yLoc), defender->xLoc, defender->yLoc, true,
669                                              T_DIVIDES_LEVEL & avoidedFlagsForMonster(&(newMonst->info)), HAS_PLAYER,
670                                              avoidedFlagsForMonster(&(newMonst->info)), (HAS_PLAYER | HAS_MONSTER | HAS_STAIRS), false);
671                     newMonst->bookkeepingFlags |= (MB_FOLLOWER | MB_BOUND_TO_LEADER | MB_DOES_NOT_TRACK_LEADER | MB_TELEPATHICALLY_REVEALED);
672                     newMonst->bookkeepingFlags &= ~MB_JUST_SUMMONED;
673                     newMonst->leader = &player;
674                     newMonst->creatureState = MONSTER_ALLY;
675                     if (theItem->flags & ITEM_ATTACKS_STAGGER) {
676                         newMonst->info.attackSpeed *= 2;
677                         newMonst->info.abilityFlags |= MA_ATTACKS_STAGGER;
678                     }
679                     if (theItem->flags & ITEM_ATTACKS_QUICKLY) {
680                         newMonst->info.attackSpeed /= 2;
681                     }
682                     if (theItem->flags & ITEM_ATTACKS_PENETRATE) {
683                         newMonst->info.abilityFlags |= MA_ATTACKS_PENETRATE;
684                     }
685                     if (theItem->flags & ITEM_ATTACKS_ALL_ADJACENT) {
686                         newMonst->info.abilityFlags |= MA_ATTACKS_ALL_ADJACENT;
687                     }
688                     if (theItem->flags & ITEM_ATTACKS_EXTEND) {
689                         newMonst->info.abilityFlags |= MA_ATTACKS_EXTEND;
690                     }
691                     newMonst->ticksUntilTurn = 100;
692                     newMonst->info.accuracy = player.info.accuracy + (5 * netEnchant(theItem) / FP_FACTOR);
693                     newMonst->info.damage = player.info.damage;
694                     newMonst->status[STATUS_LIFESPAN_REMAINING] = newMonst->maxStatus[STATUS_LIFESPAN_REMAINING] = weaponImageDuration(enchant);
695                     if (strLenWithoutEscapes(theItemName) <= 8) {
696                         sprintf(newMonst->info.monsterName, "spectral %s", theItemName);
697                     } else {
698                         switch (rogue.weapon->kind) {
699                             case BROADSWORD:
700                                 strcpy(newMonst->info.monsterName, "spectral sword");
701                                 break;
702                             case HAMMER:
703                                 strcpy(newMonst->info.monsterName, "spectral hammer");
704                                 break;
705                             case PIKE:
706                                 strcpy(newMonst->info.monsterName, "spectral pike");
707                                 break;
708                             case WAR_AXE:
709                                 strcpy(newMonst->info.monsterName, "spectral axe");
710                                 break;
711                             default:
712                                 strcpy(newMonst->info.monsterName, "spectral weapon");
713                                 break;
714                         }
715                     }
716                     pmap[newMonst->xLoc][newMonst->yLoc].flags |= HAS_MONSTER;
717                     fadeInMonster(newMonst);
718                 }
719                 updateVision(true);
720 
721                 message(buf, 0);
722                 autoID = true;
723                 break;
724             case W_SLOWING:
725                 slow(defender, weaponSlowDuration(enchant));
726                 if (canDirectlySeeMonster(defender)) {
727                     sprintf(buf, "%s slows down", monstName);
728                     buf[DCOLS] = '\0';
729                     combatMessage(buf, messageColorFromVictim(defender));
730                     autoID = true;
731                 }
732                 break;
733             case W_CONFUSION:
734                 defender->status[STATUS_CONFUSED] = max(defender->status[STATUS_CONFUSED], weaponConfusionDuration(enchant));
735                 defender->maxStatus[STATUS_CONFUSED] = defender->status[STATUS_CONFUSED];
736                 if (canDirectlySeeMonster(defender)) {
737                     sprintf(buf, "%s looks very confused", monstName);
738                     buf[DCOLS] = '\0';
739                     combatMessage(buf, messageColorFromVictim(defender));
740                     autoID = true;
741                 }
742                 break;
743             case W_FORCE:
744                 autoID = forceWeaponHit(defender, theItem);
745                 break;
746             case W_MERCY:
747                 heal(defender, 50, false);
748                 if (canSeeMonster(defender)) {
749                     autoID = true;
750                 }
751                 break;
752             case W_PLENTY:
753                 newMonst = cloneMonster(defender, true, true);
754                 if (newMonst) {
755                     flashMonster(newMonst, effectColors[enchantType], 100);
756                     if (canSeeMonster(newMonst)) {
757                         autoID = true;
758                     }
759                 }
760                 break;
761             default:
762                 break;
763         }
764     }
765     if (autoID) {
766         autoIdentify(theItem);
767     }
768 }
769 
attackVerb(char returnString[DCOLS],creature * attacker,short hitPercentile)770 void attackVerb(char returnString[DCOLS], creature *attacker, short hitPercentile) {
771     short verbCount, increment;
772 
773     if (attacker != &player && (player.status[STATUS_HALLUCINATING] || !canSeeMonster(attacker))) {
774         strcpy(returnString, "hits");
775         return;
776     }
777 
778     if (attacker == &player && !rogue.weapon) {
779         strcpy(returnString, "punch");
780         return;
781     }
782 
783     for (verbCount = 0; verbCount < 4 && monsterText[attacker->info.monsterID].attack[verbCount + 1][0] != '\0'; verbCount++);
784     increment = (100 / (verbCount + 1));
785     hitPercentile = max(0, min(hitPercentile, increment * (verbCount + 1) - 1));
786     strcpy(returnString, monsterText[attacker->info.monsterID].attack[hitPercentile / increment]);
787     resolvePronounEscapes(returnString, attacker);
788 }
789 
applyArmorRunicEffect(char returnString[DCOLS],creature * attacker,short * damage,boolean melee)790 void applyArmorRunicEffect(char returnString[DCOLS], creature *attacker, short *damage, boolean melee) {
791     char armorName[DCOLS], attackerName[DCOLS], monstName[DCOLS], buf[DCOLS * 3];
792     boolean runicKnown;
793     boolean runicDiscovered;
794     short newDamage, dir, newX, newY, count, i;
795     fixpt enchant;
796     creature *monst, *hitList[8];
797 
798     returnString[0] = '\0';
799 
800     if (!(rogue.armor && rogue.armor->flags & ITEM_RUNIC)) {
801         return; // just in case
802     }
803 
804     enchant = netEnchant(rogue.armor);
805 
806     runicKnown = rogue.armor->flags & ITEM_RUNIC_IDENTIFIED;
807     runicDiscovered = false;
808 
809     itemName(rogue.armor, armorName, false, false, NULL);
810 
811     monsterName(attackerName, attacker, true);
812 
813     switch (rogue.armor->enchant2) {
814         case A_MULTIPLICITY:
815             if (melee && !(attacker->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE)) && rand_percent(33)) {
816                 for (i = 0; i < armorImageCount(enchant); i++) {
817                     monst = cloneMonster(attacker, false, true);
818                     monst->bookkeepingFlags |= (MB_FOLLOWER | MB_BOUND_TO_LEADER | MB_DOES_NOT_TRACK_LEADER | MB_TELEPATHICALLY_REVEALED);
819                     monst->info.flags |= MONST_DIES_IF_NEGATED;
820                     monst->bookkeepingFlags &= ~(MB_JUST_SUMMONED | MB_SEIZED | MB_SEIZING);
821                     monst->info.abilityFlags &= ~(MA_CAST_SUMMON | MA_DF_ON_DEATH); // No summoning by spectral images. Gotta draw the line!
822                                                                                     // Also no exploding or infecting by spectral clones.
823                     monst->leader = &player;
824                     monst->creatureState = MONSTER_ALLY;
825                     monst->status[STATUS_DISCORDANT] = 0; // Otherwise things can get out of control...
826                     monst->ticksUntilTurn = 100;
827                     monst->info.monsterID = MK_SPECTRAL_IMAGE;
828                     if (monst->carriedMonster) {
829                         creature *carried = monst->carriedMonster;
830                         monst->carriedMonster = NULL;
831                         killCreature(carried, true); // Otherwise you can get infinite phoenices from a discordant phoenix.
832                     }
833 
834                     // Give it the glowy red light and color.
835                     monst->info.intrinsicLightType = SPECTRAL_IMAGE_LIGHT;
836                     monst->info.foreColor = &spectralImageColor;
837 
838                     // Temporary guest!
839                     monst->status[STATUS_LIFESPAN_REMAINING] = monst->maxStatus[STATUS_LIFESPAN_REMAINING] = 3;
840                     monst->currentHP = monst->info.maxHP = 1;
841                     monst->info.defense = 0;
842 
843                     if (strLenWithoutEscapes(attacker->info.monsterName) <= 6) {
844                         sprintf(monst->info.monsterName, "spectral %s", attacker->info.monsterName);
845                     } else {
846                         strcpy(monst->info.monsterName, "spectral clone");
847                     }
848                     fadeInMonster(monst);
849                 }
850                 updateVision(true);
851 
852                 runicDiscovered = true;
853                 sprintf(returnString, "Your %s flashes, and spectral images of %s appear!", armorName, attackerName);
854             }
855             break;
856         case A_MUTUALITY:
857             if (*damage > 0) {
858                 count = 0;
859                 for (i=0; i<8; i++) {
860                     hitList[i] = NULL;
861                     dir = i % 8;
862                     newX = player.xLoc + nbDirs[dir][0];
863                     newY = player.yLoc + nbDirs[dir][1];
864                     if (coordinatesAreInMap(newX, newY) && (pmap[newX][newY].flags & HAS_MONSTER)) {
865                         monst = monsterAtLoc(newX, newY);
866                         if (monst
867                             && monst != attacker
868                             && monstersAreEnemies(&player, monst)
869                             && !(monst->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE))
870                             && !(monst->bookkeepingFlags & MB_IS_DYING)) {
871 
872                             hitList[i] = monst;
873                             count++;
874                         }
875                     }
876                 }
877                 if (count) {
878                     for (i=0; i<8; i++) {
879                         if (hitList[i] && !(hitList[i]->bookkeepingFlags & MB_IS_DYING)) {
880                             monsterName(monstName, hitList[i], true);
881                             if (inflictDamage(&player, hitList[i], (*damage + count) / (count + 1), &blue, true)
882                                 && canSeeMonster(hitList[i])) {
883 
884                                 sprintf(buf, "%s %s", monstName, ((hitList[i]->info.flags & MONST_INANIMATE) ? "is destroyed" : "dies"));
885                                 combatMessage(buf, messageColorFromVictim(hitList[i]));
886                             }
887                         }
888                     }
889                     runicDiscovered = true;
890                     if (!runicKnown) {
891                         sprintf(returnString, "Your %s pulses, and the damage is shared with %s!",
892                                 armorName,
893                                 (count == 1 ? monstName : "the other adjacent enemies"));
894                     }
895                     *damage = (*damage + count) / (count + 1);
896                 }
897             }
898             break;
899         case A_ABSORPTION:
900             *damage -= rand_range(0, armorAbsorptionMax(enchant));
901             if (*damage <= 0) {
902                 *damage = 0;
903                 runicDiscovered = true;
904                 if (!runicKnown) {
905                     sprintf(returnString, "your %s pulses and absorbs the blow!", armorName);
906                 }
907             }
908             break;
909         case A_REPRISAL:
910             if (melee && !(attacker->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
911                 newDamage = max(1, armorReprisalPercent(enchant) * (*damage) / 100); // 5% reprisal per armor level
912                 if (inflictDamage(&player, attacker, newDamage, &blue, true)) {
913                     if (canSeeMonster(attacker)) {
914                         sprintf(returnString, "your %s pulses and %s drops dead!", armorName, attackerName);
915                         runicDiscovered = true;
916                     }
917                 } else if (!runicKnown) {
918                     if (canSeeMonster(attacker)) {
919                         sprintf(returnString, "your %s pulses and %s shudders in pain!", armorName, attackerName);
920                         runicDiscovered = true;
921                     }
922                 }
923             }
924             break;
925         case A_IMMUNITY:
926             if (monsterIsInClass(attacker, rogue.armor->vorpalEnemy)) {
927                 *damage = 0;
928                 runicDiscovered = true;
929             }
930             break;
931         case A_BURDEN:
932             if (rand_percent(10)) {
933                 rogue.armor->strengthRequired++;
934                 sprintf(returnString, "your %s suddenly feels heavier!", armorName);
935                 equipItem(rogue.armor, true, NULL);
936                 runicDiscovered = true;
937             }
938             break;
939         case A_VULNERABILITY:
940             *damage *= 2;
941             if (!runicKnown) {
942                 sprintf(returnString, "your %s pulses and you are wracked with pain!", armorName);
943                 runicDiscovered = true;
944             }
945             break;
946         case A_IMMOLATION:
947             if (rand_percent(10)) {
948                 sprintf(returnString, "flames suddenly explode out of your %s!", armorName);
949                 message(returnString, runicKnown ? 0 : REQUIRE_ACKNOWLEDGMENT);
950                 returnString[0] = '\0';
951                 spawnDungeonFeature(player.xLoc, player.yLoc, &(dungeonFeatureCatalog[DF_ARMOR_IMMOLATION]), true, false);
952                 runicDiscovered = true;
953             }
954         default:
955             break;
956     }
957 
958     if (runicDiscovered && !runicKnown) {
959         autoIdentify(rogue.armor);
960     }
961 }
962 
decrementWeaponAutoIDTimer()963 void decrementWeaponAutoIDTimer() {
964     char buf[COLS*3], buf2[COLS*3];
965 
966     if (rogue.weapon
967         && !(rogue.weapon->flags & ITEM_IDENTIFIED)
968         && !--rogue.weapon->charges) {
969 
970         rogue.weapon->flags |= ITEM_IDENTIFIED;
971         updateIdentifiableItems();
972         messageWithColor("you are now familiar enough with your weapon to identify it.", &itemMessageColor, 0);
973         itemName(rogue.weapon, buf2, true, true, NULL);
974         sprintf(buf, "%s %s.", (rogue.weapon->quantity > 1 ? "they are" : "it is"), buf2);
975         messageWithColor(buf, &itemMessageColor, 0);
976     }
977 }
978 
processStaggerHit(creature * attacker,creature * defender)979 void processStaggerHit(creature *attacker, creature *defender) {
980     if ((defender->info.flags & (MONST_INVULNERABLE | MONST_IMMOBILE | MONST_INANIMATE))
981         || (defender->bookkeepingFlags & MB_CAPTIVE)
982         || cellHasTerrainFlag(defender->xLoc, defender->yLoc, T_OBSTRUCTS_PASSABILITY)) {
983 
984         return;
985     }
986     short newX = clamp(defender->xLoc - attacker->xLoc, -1, 1) + defender->xLoc;
987     short newY = clamp(defender->yLoc - attacker->yLoc, -1, 1) + defender->yLoc;
988     if (coordinatesAreInMap(newX, newY)
989         && !cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_PASSABILITY)
990         && !(pmap[newX][newY].flags & (HAS_MONSTER | HAS_PLAYER))) {
991 
992         setMonsterLocation(defender, newX, newY);
993     }
994 }
995 
996 // returns whether the attack hit
attack(creature * attacker,creature * defender,boolean lungeAttack)997 boolean attack(creature *attacker, creature *defender, boolean lungeAttack) {
998     short damage, specialDamage, poisonDamage;
999     char buf[COLS*2], buf2[COLS*2], attackerName[COLS], defenderName[COLS], verb[DCOLS], explicationClause[DCOLS] = "", armorRunicString[DCOLS*3];
1000     boolean sneakAttack, defenderWasAsleep, defenderWasParalyzed, degradesAttackerWeapon, sightUnseen;
1001 
1002     if (attacker == &player && canSeeMonster(defender)) {
1003         rogue.featRecord[FEAT_PURE_MAGE] = false;
1004     }
1005 
1006     if (attacker->info.abilityFlags & MA_KAMIKAZE) {
1007         killCreature(attacker, false);
1008         return true;
1009     }
1010 
1011     armorRunicString[0] = '\0';
1012 
1013     poisonDamage = 0;
1014 
1015     degradesAttackerWeapon = (defender->info.flags & MONST_DEFEND_DEGRADE_WEAPON ? true : false);
1016 
1017     sightUnseen = !canSeeMonster(attacker) && !canSeeMonster(defender);
1018 
1019     if (defender->status[STATUS_LEVITATING] && (attacker->info.flags & MONST_RESTRICTED_TO_LIQUID)) {
1020         return false; // aquatic or other liquid-bound monsters cannot attack flying opponents
1021     }
1022 
1023     if ((attacker == &player || defender == &player) && !rogue.blockCombatText) {
1024         rogue.disturbed = true;
1025     }
1026 
1027     defender->status[STATUS_ENTRANCED] = 0;
1028     if (defender->status[STATUS_MAGICAL_FEAR]) {
1029         defender->status[STATUS_MAGICAL_FEAR] = 1;
1030     }
1031 
1032     if (attacker == &player
1033         && defender->creatureState != MONSTER_TRACKING_SCENT) {
1034 
1035         rogue.featRecord[FEAT_PALADIN] = false;
1036     }
1037 
1038     if (attacker != &player && defender == &player && attacker->creatureState == MONSTER_WANDERING) {
1039         attacker->creatureState = MONSTER_TRACKING_SCENT;
1040     }
1041 
1042     if (defender->info.flags & MONST_INANIMATE) {
1043         sneakAttack = false;
1044         defenderWasAsleep = false;
1045         defenderWasParalyzed = false;
1046     } else {
1047         sneakAttack = (defender != &player && attacker == &player && (defender->creatureState == MONSTER_WANDERING) ? true : false);
1048         defenderWasAsleep = (defender != &player && (defender->creatureState == MONSTER_SLEEPING) ? true : false);
1049         defenderWasParalyzed = defender->status[STATUS_PARALYZED] > 0;
1050     }
1051 
1052     monsterName(attackerName, attacker, true);
1053     monsterName(defenderName, defender, true);
1054 
1055     if ((attacker->info.abilityFlags & MA_SEIZES)
1056         && (!(attacker->bookkeepingFlags & MB_SEIZING) || !(defender->bookkeepingFlags & MB_SEIZED))
1057         && (distanceBetween(attacker->xLoc, attacker->yLoc, defender->xLoc, defender->yLoc) == 1
1058             && !diagonalBlocked(attacker->xLoc, attacker->yLoc, defender->xLoc, defender->yLoc, false))) {
1059 
1060         attacker->bookkeepingFlags |= MB_SEIZING;
1061         defender->bookkeepingFlags |= MB_SEIZED;
1062         if (canSeeMonster(attacker) || canSeeMonster(defender)) {
1063             sprintf(buf, "%s seizes %s!", attackerName, (defender == &player ? "your legs" : defenderName));
1064             messageWithColor(buf, &white, 0);
1065         }
1066         return false;
1067     }
1068 
1069     if (sneakAttack || defenderWasAsleep || defenderWasParalyzed || lungeAttack || attackHit(attacker, defender)) {
1070         // If the attack hit:
1071         damage = (defender->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE)
1072                   ? 0 : randClump(attacker->info.damage) * monsterDamageAdjustmentAmount(attacker) / FP_FACTOR);
1073 
1074         if (sneakAttack || defenderWasAsleep || defenderWasParalyzed) {
1075             if (defender != &player) {
1076                 // The non-player defender doesn't hit back this turn because it's still flat-footed.
1077                 defender->ticksUntilTurn += max(defender->movementSpeed, defender->attackSpeed);
1078                 if (defender->creatureState != MONSTER_ALLY) {
1079                     defender->creatureState = MONSTER_TRACKING_SCENT; // Wake up!
1080                 }
1081             }
1082         }
1083         if (sneakAttack || defenderWasAsleep || defenderWasParalyzed || lungeAttack) {
1084             if (attacker == &player
1085                 && rogue.weapon
1086                 && (rogue.weapon->flags & ITEM_SNEAK_ATTACK_BONUS)) {
1087 
1088                 damage *= 5; // 5x damage for dagger sneak attacks.
1089             } else {
1090                 damage *= 3; // Treble damage for general sneak attacks.
1091             }
1092         }
1093 
1094         if (defender == &player && rogue.armor && (rogue.armor->flags & ITEM_RUNIC)) {
1095             applyArmorRunicEffect(armorRunicString, attacker, &damage, true);
1096         }
1097 
1098         if (attacker == &player
1099             && rogue.reaping
1100             && !(defender->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
1101 
1102             specialDamage = min(damage, defender->currentHP) * rogue.reaping; // Maximum reaped damage can't exceed the victim's remaining health.
1103             if (rogue.reaping > 0) {
1104                 specialDamage = rand_range(0, specialDamage);
1105             } else {
1106                 specialDamage = rand_range(specialDamage, 0);
1107             }
1108             if (specialDamage) {
1109                 rechargeItemsIncrementally(specialDamage);
1110             }
1111         }
1112 
1113         if (damage == 0) {
1114             sprintf(explicationClause, " but %s no damage", (attacker == &player ? "do" : "does"));
1115             if (attacker == &player) {
1116                 rogue.disturbed = true;
1117             }
1118         } else if (lungeAttack) {
1119             strcpy(explicationClause, " with a vicious lunge attack");
1120         } else if (defenderWasParalyzed) {
1121             sprintf(explicationClause, " while $HESHE %s paralyzed", (defender == &player ? "are" : "is"));
1122         } else if (defenderWasAsleep) {
1123             strcpy(explicationClause, " in $HISHER sleep");
1124         } else if (sneakAttack) {
1125             strcpy(explicationClause, ", catching $HIMHER unaware");
1126         } else if (defender->status[STATUS_STUCK] || defender->bookkeepingFlags & MB_CAPTIVE) {
1127             sprintf(explicationClause, " while %s dangle%s helplessly",
1128                     (canSeeMonster(defender) ? "$HESHE" : "it"),
1129                     (defender == &player ? "" : "s"));
1130         }
1131         resolvePronounEscapes(explicationClause, defender);
1132 
1133         if ((attacker->info.abilityFlags & MA_POISONS) && damage > 0) {
1134             poisonDamage = damage;
1135             damage = 1;
1136         }
1137 
1138         if (inflictDamage(attacker, defender, damage, &red, false)) { // if the attack killed the defender
1139             if (defenderWasAsleep || sneakAttack || defenderWasParalyzed || lungeAttack) {
1140                 sprintf(buf, "%s %s %s%s", attackerName,
1141                         ((defender->info.flags & MONST_INANIMATE) ? "destroyed" : "dispatched"),
1142                         defenderName,
1143                         explicationClause);
1144             } else {
1145                 sprintf(buf, "%s %s %s%s",
1146                         attackerName,
1147                         ((defender->info.flags & MONST_INANIMATE) ? "destroyed" : "defeated"),
1148                         defenderName,
1149                         explicationClause);
1150             }
1151             if (sightUnseen) {
1152                 if (defender->info.flags & MONST_INANIMATE) {
1153                     combatMessage("you hear something get destroyed in combat", 0);
1154                 } else {
1155                     combatMessage("you hear something die in combat", 0);
1156                 }
1157             } else {
1158                 combatMessage(buf, (damage > 0 ? messageColorFromVictim(defender) : &white));
1159             }
1160             if (&player == defender) {
1161                 gameOver(attacker->info.monsterName, false);
1162                 return true;
1163             } else if (&player == attacker
1164                        && defender->info.monsterID == MK_DRAGON) {
1165 
1166                 rogue.featRecord[FEAT_DRAGONSLAYER] = true;
1167             }
1168         } else { // if the defender survived
1169             if (!rogue.blockCombatText && (canSeeMonster(attacker) || canSeeMonster(defender))) {
1170                 attackVerb(verb, attacker, max(damage - (attacker->info.damage.lowerBound * monsterDamageAdjustmentAmount(attacker) / FP_FACTOR), 0) * 100
1171                            / max(1, (attacker->info.damage.upperBound - attacker->info.damage.lowerBound) * monsterDamageAdjustmentAmount(attacker) / FP_FACTOR));
1172                 sprintf(buf, "%s %s %s%s", attackerName, verb, defenderName, explicationClause);
1173                 if (sightUnseen) {
1174                     if (!rogue.heardCombatThisTurn) {
1175                         rogue.heardCombatThisTurn = true;
1176                         combatMessage("you hear combat in the distance", 0);
1177                     }
1178                 } else {
1179                     combatMessage(buf, messageColorFromVictim(defender));
1180                 }
1181             }
1182             if (attacker == &player && rogue.weapon && (rogue.weapon->flags & ITEM_ATTACKS_STAGGER)) {
1183                 processStaggerHit(attacker, defender);
1184             }
1185             if (attacker->info.abilityFlags & SPECIAL_HIT) {
1186                 specialHit(attacker, defender, (attacker->info.abilityFlags & MA_POISONS) ? poisonDamage : damage);
1187             }
1188             if (armorRunicString[0]) {
1189                 message(armorRunicString, 0);
1190                 if (rogue.armor && (rogue.armor->flags & ITEM_RUNIC) && rogue.armor->enchant2 == A_BURDEN) {
1191                     strengthCheck(rogue.armor, true);
1192                 }
1193             }
1194         }
1195 
1196         moralAttack(attacker, defender);
1197 
1198         if (attacker == &player && rogue.weapon && (rogue.weapon->flags & ITEM_RUNIC)) {
1199             magicWeaponHit(defender, rogue.weapon, sneakAttack || defenderWasAsleep || defenderWasParalyzed);
1200         }
1201 
1202         if (attacker == &player
1203             && (defender->bookkeepingFlags & MB_IS_DYING)
1204             && (defender->bookkeepingFlags & MB_HAS_SOUL)) {
1205 
1206             decrementWeaponAutoIDTimer();
1207         }
1208 
1209         if (degradesAttackerWeapon
1210             && attacker == &player
1211             && rogue.weapon
1212             && !(rogue.weapon->flags & ITEM_PROTECTED)
1213                 // Can't damage a Weapon of Acid Mound Slaying by attacking an acid mound... just ain't right!
1214             && !((rogue.weapon->flags & ITEM_RUNIC) && rogue.weapon->enchant2 == W_SLAYING && monsterIsInClass(defender, rogue.weapon->vorpalEnemy))
1215             && rogue.weapon->enchant1 >= -10) {
1216 
1217             rogue.weapon->enchant1--;
1218             if (rogue.weapon->quiverNumber) {
1219                 rogue.weapon->quiverNumber = rand_range(1, 60000);
1220             }
1221             equipItem(rogue.weapon, true, NULL);
1222             itemName(rogue.weapon, buf2, false, false, NULL);
1223             sprintf(buf, "your %s weakens!", buf2);
1224             messageWithColor(buf, &itemMessageColor, 0);
1225             checkForDisenchantment(rogue.weapon);
1226         }
1227 
1228         return true;
1229     } else { // if the attack missed
1230         if (!rogue.blockCombatText) {
1231             if (sightUnseen) {
1232                 if (!rogue.heardCombatThisTurn) {
1233                     rogue.heardCombatThisTurn = true;
1234                     combatMessage("you hear combat in the distance", 0);
1235                 }
1236             } else {
1237                 sprintf(buf, "%s missed %s", attackerName, defenderName);
1238                 combatMessage(buf, 0);
1239             }
1240         }
1241         return false;
1242     }
1243 }
1244 
1245 // Gets the length of a string without the four-character color escape sequences, since those aren't displayed.
strLenWithoutEscapes(const char * str)1246 short strLenWithoutEscapes(const char *str) {
1247     short i, count;
1248 
1249     count = 0;
1250     for (i=0; str[i];) {
1251         if (str[i] == COLOR_ESCAPE) {
1252             i += 4;
1253             continue;
1254         }
1255         count++;
1256         i++;
1257     }
1258     return count;
1259 }
1260 
1261 // Buffer messages generated by combat until flushed by displayCombatText().
1262 // Messages in the buffer are delimited by newlines.
combatMessage(char * theMsg,color * theColor)1263 void combatMessage(char *theMsg, color *theColor) {
1264     short length;
1265     char newMsg[COLS * 2];
1266 
1267     if (theColor == 0) {
1268         theColor = &white;
1269     }
1270 
1271     newMsg[0] = '\0';
1272     encodeMessageColor(newMsg, 0, theColor);
1273     strcat(newMsg, theMsg);
1274 
1275     length = strlen(combatText);
1276 
1277     // Buffer combat messages here just for timing; otherwise player combat
1278     // messages appear after monsters, rather than before.  The -2 is for the
1279     // newline and terminator.
1280     if (length + strlen(newMsg) > COLS * 2 - 2) {
1281         displayCombatText();
1282     }
1283 
1284     if (combatText[0]) {
1285         snprintf(&combatText[length], COLS * 2 - length, "\n%s", newMsg);
1286     } else {
1287         strcpy(combatText, newMsg);
1288     }
1289 }
1290 
1291 // Flush any buffered, newline-delimited combat messages, passing each to
1292 // message().  These messages are "foldable", meaning that if space permits
1293 // they may be joined together by semi-colons.  Notice that combat messages may
1294 // be flushed by a number of different callers.  One is message() itself
1295 // creating a recursion, which this function is responsible for terminating.
displayCombatText()1296 void displayCombatText() {
1297     char buf[COLS * 2];
1298     char *start, *end;
1299 
1300     // message itself will call displayCombatText.  For this guard to terminate
1301     // the recursion, we need to copy combatText out and empty it before
1302     // calling message.
1303     if (combatText[0] == '\0') {
1304         return;
1305     }
1306 
1307     strcpy(buf, combatText);
1308     combatText[0] = '\0';
1309 
1310     start = buf;
1311     for (end = start; *end != '\0'; end++) {
1312         if (*end == '\n') {
1313             *end = '\0';
1314             message(start, FOLDABLE | (rogue.cautiousMode ? REQUIRE_ACKNOWLEDGMENT : 0));
1315             start = end + 1;
1316         }
1317     }
1318 
1319     message(start, FOLDABLE | (rogue.cautiousMode ? REQUIRE_ACKNOWLEDGMENT : 0));
1320 
1321     rogue.cautiousMode = false;
1322 }
1323 
flashMonster(creature * monst,const color * theColor,short strength)1324 void flashMonster(creature *monst, const color *theColor, short strength) {
1325     if (!theColor) {
1326         return;
1327     }
1328     if (!(monst->bookkeepingFlags & MB_WILL_FLASH) || monst->flashStrength < strength) {
1329         monst->bookkeepingFlags |= MB_WILL_FLASH;
1330         monst->flashStrength = strength;
1331         monst->flashColor = *theColor;
1332         rogue.creaturesWillFlashThisTurn = true;
1333     }
1334 }
1335 
canAbsorb(creature * ally,boolean ourBolts[NUMBER_BOLT_KINDS],creature * prey,short ** grid)1336 boolean canAbsorb(creature *ally, boolean ourBolts[NUMBER_BOLT_KINDS], creature *prey, short **grid) {
1337     short i;
1338 
1339     if (ally->creatureState == MONSTER_ALLY
1340         && ally->newPowerCount > 0
1341         && (ally->targetCorpseLoc[0] <= 0)
1342         && !((ally->info.flags | prey->info.flags) & (MONST_INANIMATE | MONST_IMMOBILE))
1343         && !monsterAvoids(ally, prey->xLoc, prey->yLoc)
1344         && grid[ally->xLoc][ally->yLoc] <= 10) {
1345 
1346         if (~(ally->info.abilityFlags) & prey->info.abilityFlags & LEARNABLE_ABILITIES) {
1347             return true;
1348         } else if (~(ally->info.flags) & prey->info.flags & LEARNABLE_BEHAVIORS) {
1349             return true;
1350         } else {
1351             for (i = 0; i < NUMBER_BOLT_KINDS; i++) {
1352                 ourBolts[i] = false;
1353             }
1354             for (i = 0; ally->info.bolts[i] != BOLT_NONE; i++) {
1355                 ourBolts[ally->info.bolts[i]] = true;
1356             }
1357 
1358             for (i=0; prey->info.bolts[i] != BOLT_NONE; i++) {
1359                 if (!(boltCatalog[prey->info.bolts[i]].flags & BF_NOT_LEARNABLE)
1360                     && !ourBolts[prey->info.bolts[i]]) {
1361 
1362                     return true;
1363                 }
1364             }
1365         }
1366     }
1367     return false;
1368 }
1369 
anyoneWantABite(creature * decedent)1370 boolean anyoneWantABite(creature *decedent) {
1371     short candidates, randIndex, i;
1372     short **grid;
1373     boolean success = false;
1374     boolean ourBolts[NUMBER_BOLT_KINDS] = {false};
1375 
1376     candidates = 0;
1377     if ((!(decedent->info.abilityFlags & LEARNABLE_ABILITIES)
1378          && !(decedent->info.flags & LEARNABLE_BEHAVIORS)
1379          && decedent->info.bolts[0] == BOLT_NONE)
1380         || (cellHasTerrainFlag(decedent->xLoc, decedent->yLoc, T_PATHING_BLOCKER))
1381         || decedent->info.monsterID == MK_SPECTRAL_IMAGE
1382         || (decedent->info.flags & (MONST_INANIMATE | MONST_IMMOBILE))) {
1383 
1384         return false;
1385     }
1386 
1387     grid = allocGrid();
1388     fillGrid(grid, 0);
1389     calculateDistances(grid, decedent->xLoc, decedent->yLoc, T_PATHING_BLOCKER, NULL, true, true);
1390     for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
1391         creature *ally = nextCreature(&it);
1392         if (canAbsorb(ally, ourBolts, decedent, grid)) {
1393             candidates++;
1394         }
1395     }
1396     if (candidates > 0) {
1397         randIndex = rand_range(1, candidates);
1398         creature *firstAlly = NULL;
1399         for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) {
1400             creature *ally = nextCreature(&it);
1401             // CanAbsorb() populates ourBolts if it returns true and there are no learnable behaviors or flags:
1402             if (canAbsorb(ally, ourBolts, decedent, grid) && !--randIndex) {
1403                 firstAlly = ally;
1404                 break;
1405             }
1406         }
1407         if (firstAlly) {
1408             firstAlly->targetCorpseLoc[0] = decedent->xLoc;
1409             firstAlly->targetCorpseLoc[1] = decedent->yLoc;
1410             strcpy(firstAlly->targetCorpseName, decedent->info.monsterName);
1411             firstAlly->corpseAbsorptionCounter = 20; // 20 turns to get there and start eating before he loses interest
1412 
1413             // Choose a superpower.
1414             // First, select from among learnable ability or behavior flags, if one is available.
1415             candidates = 0;
1416             for (i=0; i<32; i++) {
1417                 if (Fl(i) & ~(firstAlly->info.abilityFlags) & decedent->info.abilityFlags & LEARNABLE_ABILITIES) {
1418                     candidates++;
1419                 }
1420             }
1421             for (i=0; i<32; i++) {
1422                 if (Fl(i) & ~(firstAlly->info.flags) & decedent->info.flags & LEARNABLE_BEHAVIORS) {
1423                     candidates++;
1424                 }
1425             }
1426             if (candidates > 0) {
1427                 randIndex = rand_range(1, candidates);
1428                 for (i=0; i<32; i++) {
1429                     if ((Fl(i) & ~(firstAlly->info.abilityFlags) & decedent->info.abilityFlags & LEARNABLE_ABILITIES)
1430                         && !--randIndex) {
1431 
1432                         firstAlly->absorptionFlags = Fl(i);
1433                         firstAlly->absorbBehavior = false;
1434                         success = true;
1435                         break;
1436                     }
1437                 }
1438                 for (i=0; i<32 && !success; i++) {
1439                     if ((Fl(i) & ~(firstAlly->info.flags) & decedent->info.flags & LEARNABLE_BEHAVIORS)
1440                         && !--randIndex) {
1441 
1442                         firstAlly->absorptionFlags = Fl(i);
1443                         firstAlly->absorbBehavior = true;
1444                         success = true;
1445                         break;
1446                     }
1447                 }
1448             } else if (decedent->info.bolts[0] != BOLT_NONE) {
1449                 // If there are no learnable ability or behavior flags, pick a learnable bolt.
1450                 candidates = 0;
1451                 for (i=0; decedent->info.bolts[i] != BOLT_NONE; i++) {
1452                     if (!(boltCatalog[decedent->info.bolts[i]].flags & BF_NOT_LEARNABLE)
1453                         && !ourBolts[decedent->info.bolts[i]]) {
1454 
1455                         candidates++;
1456                     }
1457                 }
1458                 if (candidates > 0) {
1459                     randIndex = rand_range(1, candidates);
1460                     for (i=0; decedent->info.bolts[i] != BOLT_NONE; i++) {
1461                         if (!(boltCatalog[decedent->info.bolts[i]].flags & BF_NOT_LEARNABLE)
1462                             && !ourBolts[decedent->info.bolts[i]]
1463                             && !--randIndex) {
1464 
1465                             firstAlly->absorptionBolt = decedent->info.bolts[i];
1466                             success = true;
1467                             break;
1468                         }
1469                     }
1470                 }
1471             }
1472         }
1473     }
1474     freeGrid(grid);
1475     return success;
1476 }
1477 
1478 #define MIN_FLASH_STRENGTH  50
1479 
inflictLethalDamage(creature * attacker,creature * defender)1480 void inflictLethalDamage(creature *attacker, creature *defender) {
1481     inflictDamage(attacker, defender, defender->currentHP, NULL, true);
1482 }
1483 
1484 // returns true if this was a killing stroke; does NOT free the pointer, but DOES remove it from the monster chain
1485 // flashColor indicates the color that the damage will cause the creature to flash
inflictDamage(creature * attacker,creature * defender,short damage,const color * flashColor,boolean ignoresProtectionShield)1486 boolean inflictDamage(creature *attacker, creature *defender,
1487                       short damage, const color *flashColor, boolean ignoresProtectionShield) {
1488 
1489     boolean killed = false;
1490     dungeonFeature theBlood;
1491     short transferenceAmount;
1492 
1493     if (damage == 0
1494         || (defender->info.flags & MONST_INVULNERABLE)) {
1495 
1496         return false;
1497     }
1498 
1499     if (!ignoresProtectionShield
1500         && defender->status[STATUS_SHIELDED]) {
1501 
1502         if (defender->status[STATUS_SHIELDED] > damage * 10) {
1503             defender->status[STATUS_SHIELDED] -= damage * 10;
1504             damage = 0;
1505         } else {
1506             damage -= (defender->status[STATUS_SHIELDED] + 9) / 10;
1507             defender->status[STATUS_SHIELDED] = defender->maxStatus[STATUS_SHIELDED] = 0;
1508         }
1509     }
1510 
1511     defender->bookkeepingFlags &= ~MB_ABSORBING; // Stop eating a corpse if you are getting hurt.
1512 
1513     // bleed all over the place, proportionately to damage inflicted:
1514     if (damage > 0 && defender->info.bloodType) {
1515         theBlood = dungeonFeatureCatalog[defender->info.bloodType];
1516         theBlood.startProbability = (theBlood.startProbability * (15 + min(damage, defender->currentHP) * 3 / 2) / 100);
1517         if (theBlood.layer == GAS) {
1518             theBlood.startProbability *= 100;
1519         }
1520         spawnDungeonFeature(defender->xLoc, defender->yLoc, &theBlood, true, false);
1521     }
1522 
1523     if (defender != &player && defender->creatureState == MONSTER_SLEEPING) {
1524         wakeUp(defender);
1525     }
1526 
1527     if (defender == &player
1528         && rogue.easyMode
1529         && damage > 0) {
1530         damage = max(1, damage/5);
1531     }
1532 
1533     if (((attacker == &player && rogue.transference) || (attacker && attacker != &player && (attacker->info.abilityFlags & MA_TRANSFERENCE)))
1534         && !(defender->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) {
1535 
1536         transferenceAmount = min(damage, defender->currentHP); // Maximum transferred damage can't exceed the victim's remaining health.
1537 
1538         if (attacker == &player) {
1539             transferenceAmount = transferenceAmount * rogue.transference / 20;
1540             if (transferenceAmount == 0) {
1541                 transferenceAmount = ((rogue.transference > 0) ? 1 : -1);
1542             }
1543         } else if (attacker->creatureState == MONSTER_ALLY) {
1544             transferenceAmount = transferenceAmount * 4 / 10; // allies get 40% recovery rate
1545         } else {
1546             transferenceAmount = transferenceAmount * 9 / 10; // enemies get 90% recovery rate, deal with it
1547         }
1548 
1549         attacker->currentHP += transferenceAmount;
1550 
1551         if (attacker == &player && player.currentHP <= 0) {
1552             gameOver("Drained by a cursed ring", true);
1553             return false;
1554         }
1555     }
1556 
1557     if (defender->currentHP <= damage) { // killed
1558         killCreature(defender, false);
1559         anyoneWantABite(defender);
1560         killed = true;
1561     } else { // survived
1562         if (damage < 0 && defender->currentHP - damage > defender->info.maxHP) {
1563             defender->currentHP = max(defender->currentHP, defender->info.maxHP);
1564         } else {
1565             defender->currentHP -= damage; // inflict the damage!
1566             if (defender == &player && damage > 0) {
1567                 rogue.featRecord[FEAT_INDOMITABLE] = false;
1568             }
1569         }
1570 
1571         if (defender != &player && defender->creatureState != MONSTER_ALLY
1572             && defender->info.flags & MONST_FLEES_NEAR_DEATH
1573             && defender->info.maxHP / 4 >= defender->currentHP) {
1574 
1575             defender->creatureState = MONSTER_FLEEING;
1576         }
1577         if (flashColor && damage > 0) {
1578             flashMonster(defender, flashColor, MIN_FLASH_STRENGTH + (100 - MIN_FLASH_STRENGTH) * damage / defender->info.maxHP);
1579         }
1580     }
1581 
1582     refreshSideBar(-1, -1, false);
1583     return killed;
1584 }
1585 
addPoison(creature * monst,short durationIncrement,short concentrationIncrement)1586 void addPoison(creature *monst, short durationIncrement, short concentrationIncrement) {
1587     extern const color poisonColor;
1588     if (durationIncrement > 0) {
1589         if (monst == &player && !player.status[STATUS_POISONED]) {
1590             combatMessage("scalding poison fills your veins", &badMessageColor);
1591         }
1592         if (!monst->status[STATUS_POISONED]) {
1593             monst->maxStatus[STATUS_POISONED] = 0;
1594         }
1595         monst->poisonAmount += concentrationIncrement;
1596         if (monst->poisonAmount == 0) {
1597             monst->poisonAmount = 1;
1598         }
1599         monst->status[STATUS_POISONED] += durationIncrement;
1600         monst->maxStatus[STATUS_POISONED] = monst->info.maxHP / monst->poisonAmount;
1601 
1602         if (canSeeMonster(monst)) {
1603             flashMonster(monst, &poisonColor, 100);
1604         }
1605     }
1606 }
1607 
1608 
1609 // Removes the decedent from the screen and from the monster chain; inserts it into the graveyard chain; does NOT free the memory.
1610 // Or, if the decedent is a player ally at the moment of death, insert it into the purgatory chain for possible future resurrection.
1611 // Use "administrativeDeath" if the monster is being deleted for administrative purposes, as opposed to dying as a result of physical actions.
1612 // AdministrativeDeath means the monster simply disappears, with no messages, dropped item, DFs or other effect.
killCreature(creature * decedent,boolean administrativeDeath)1613 void killCreature(creature *decedent, boolean administrativeDeath) {
1614     short x, y;
1615     char monstName[DCOLS], buf[DCOLS * 3];
1616 
1617     if (decedent->bookkeepingFlags & MB_IS_DYING) {
1618         // monster has already been killed; let's avoid overkill
1619         return;
1620     }
1621 
1622     if (decedent != &player) {
1623         decedent->bookkeepingFlags |= MB_IS_DYING;
1624     }
1625 
1626     if (rogue.lastTarget == decedent) {
1627         rogue.lastTarget = NULL;
1628     }
1629     if (rogue.yendorWarden == decedent) {
1630         rogue.yendorWarden = NULL;
1631     }
1632 
1633     if (decedent->carriedItem) {
1634         if (administrativeDeath) {
1635             deleteItem(decedent->carriedItem);
1636             decedent->carriedItem = NULL;
1637         } else {
1638             makeMonsterDropItem(decedent);
1639         }
1640     }
1641 
1642     if (!administrativeDeath && (decedent->info.abilityFlags & MA_DF_ON_DEATH)
1643         && !(decedent->bookkeepingFlags & MB_IS_FALLING)) {
1644         spawnDungeonFeature(decedent->xLoc, decedent->yLoc, &dungeonFeatureCatalog[decedent->info.DFType], true, false);
1645 
1646         if (monsterText[decedent->info.monsterID].DFMessage[0] && canSeeMonster(decedent)) {
1647             monsterName(monstName, decedent, true);
1648             snprintf(buf, DCOLS * 3, "%s %s", monstName, monsterText[decedent->info.monsterID].DFMessage);
1649             resolvePronounEscapes(buf, decedent);
1650             message(buf, 0);
1651         }
1652     }
1653 
1654     if (decedent == &player) { // the player died
1655         // game over handled elsewhere
1656     } else {
1657         if (!administrativeDeath
1658             && decedent->creatureState == MONSTER_ALLY
1659             && !canSeeMonster(decedent)
1660             && !(decedent->info.flags & MONST_INANIMATE)
1661             && !(decedent->bookkeepingFlags & MB_BOUND_TO_LEADER)
1662             && !decedent->carriedMonster) {
1663 
1664             messageWithColor("you feel a sense of loss.", &badMessageColor, 0);
1665         }
1666         x = decedent->xLoc;
1667         y = decedent->yLoc;
1668         if (decedent->bookkeepingFlags & MB_IS_DORMANT) {
1669             pmap[x][y].flags &= ~HAS_DORMANT_MONSTER;
1670         } else {
1671             pmap[x][y].flags &= ~HAS_MONSTER;
1672         }
1673         removeCreature(dormantMonsters, decedent);
1674         removeCreature(monsters, decedent);
1675 
1676         if (decedent->leader == &player
1677             && !(decedent->info.flags & MONST_INANIMATE)
1678             && (decedent->bookkeepingFlags & MB_HAS_SOUL)
1679             && !administrativeDeath) {
1680             prependCreature(&purgatory, decedent);
1681         } else {
1682             prependCreature(&graveyard, decedent);
1683 
1684         }
1685 
1686         if (!administrativeDeath && !(decedent->bookkeepingFlags & MB_IS_DORMANT)) {
1687             // Was there another monster inside?
1688             if (decedent->carriedMonster) {
1689                 // Insert it into the chain.
1690                 creature *carriedMonster = decedent->carriedMonster;
1691                 decedent->carriedMonster = NULL;
1692                 prependCreature(monsters, carriedMonster);
1693 
1694                 carriedMonster->xLoc = x;
1695                 carriedMonster->yLoc = y;
1696                 carriedMonster->ticksUntilTurn = 200;
1697                 pmap[x][y].flags |= HAS_MONSTER;
1698                 fadeInMonster(carriedMonster);
1699 
1700                 if (canSeeMonster(carriedMonster)) {
1701                     monsterName(monstName, carriedMonster, true);
1702                     sprintf(buf, "%s appears", monstName);
1703                     combatMessage(buf, NULL);
1704                 }
1705 
1706                 applyInstantTileEffectsToCreature(carriedMonster);
1707             }
1708             refreshDungeonCell(x, y);
1709         }
1710     }
1711     decedent->currentHP = 0;
1712     demoteMonsterFromLeadership(decedent);
1713     if (decedent->leader) {
1714         checkForContinuedLeadership(decedent->leader);
1715     }
1716 }
1717 
buildHitList(creature ** hitList,const creature * attacker,creature * defender,const boolean sweep)1718 void buildHitList(creature **hitList, const creature *attacker, creature *defender, const boolean sweep) {
1719     short i, x, y, newX, newY, newestX, newestY;
1720     enum directions dir, newDir;
1721 
1722     x = attacker->xLoc;
1723     y = attacker->yLoc;
1724     newX = defender->xLoc;
1725     newY = defender->yLoc;
1726 
1727     dir = NO_DIRECTION;
1728     for (i = 0; i < DIRECTION_COUNT; i++) {
1729         if (nbDirs[i][0] == newX - x
1730             && nbDirs[i][1] == newY - y) {
1731 
1732             dir = i;
1733             break;
1734         }
1735     }
1736 
1737     if (sweep) {
1738         if (dir == NO_DIRECTION) {
1739             dir = UP; // Just pick one.
1740         }
1741         for (i=0; i<8; i++) {
1742             newDir = (dir + i) % DIRECTION_COUNT;
1743             newestX = x + cDirs[newDir][0];
1744             newestY = y + cDirs[newDir][1];
1745             if (coordinatesAreInMap(newestX, newestY) && (pmap[newestX][newestY].flags & (HAS_MONSTER | HAS_PLAYER))) {
1746                 defender = monsterAtLoc(newestX, newestY);
1747                 if (defender
1748                     && monsterWillAttackTarget(attacker, defender)
1749                     && (!cellHasTerrainFlag(defender->xLoc, defender->yLoc, T_OBSTRUCTS_PASSABILITY) || (defender->info.flags & MONST_ATTACKABLE_THRU_WALLS))) {
1750 
1751                     hitList[i] = defender;
1752                 }
1753             }
1754         }
1755     } else {
1756         hitList[0] = defender;
1757     }
1758 }
1759