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