/* * Combat.c * Brogue * * Created by Brian Walker on 6/11/09. * Copyright 2012. All rights reserved. * * This file is part of Brogue. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include "Rogue.h" #include "IncludeGlobals.h" /* Combat rules: * Each combatant has an accuracy rating. This is the percentage of their attacks that will ordinarily hit; * higher numbers are better for them. Numbers over 100 are permitted. * * Each combatant also has a defense rating. The "hit probability" is calculated as given by this formula: * * hit probability = (accuracy) * 0.987 ^ (defense) * * when hit determinations are made. Negative numbers and numbers over 100 are permitted. * The hit is then randomly determined according to this final percentage. * * Some environmental factors can modify these numbers. An unaware, sleeping, stuck or paralyzed * combatant is always hit. An unaware, sleeping or paralyzed combatant also takes treble damage. * * If the hit lands, damage is calculated in the range provided. However, the clumping factor affects the * probability distribution. If the range is 0-10 with a clumping factor of 1, it's a uniform distribution. * With a clumping factor of 2, it's calculated as 2d5 (with d5 meaing a die numbered from 0 through 5). * With 3, it's 3d3, and so on. Note that a range not divisible by the clumping factor is defective, * as it will never be resolved in the top few numbers of the range. In fact, the top * (rangeWidth % clumpingFactor) will never succeed. Thus we increment the maximum of the first * (rangeWidth % clumpingFactor) die by 1, so that in fact 0-10 with a CF of 3 would be 1d4 + 2d3. Similarly, * 0-10 with CF 4 would be 2d3 + 2d2. By playing with the numbers, one can approximate a gaussian * distribution of any mean and standard deviation. * * Player combatants take their base defense value of their actual armor. Their accuracy is a combination of weapon, armor * and strength. * * Players have a base accuracy value of 100 throughout the game. Each point of weapon enchantment (net of * strength penalty/benefit) increases */ fixpt strengthModifier(item *theItem) { int difference = (rogue.strength - player.weaknessAmount) - theItem->strengthRequired; if (difference > 0) { return difference * FP_FACTOR / 4; // 0.25x } else { return difference * FP_FACTOR * 5/2; // 2.5x } } fixpt netEnchant(item *theItem) { fixpt retval = theItem->enchant1 * FP_FACTOR; if (theItem->category & (WEAPON | ARMOR)) { retval += strengthModifier(theItem); } // Clamp all net enchantment values to [-20, 50]. return clamp(retval, -20 * FP_FACTOR, 50 * FP_FACTOR); } fixpt monsterDamageAdjustmentAmount(const creature *monst) { if (monst == &player) { // Handled through player strength routines elsewhere. return FP_FACTOR; } else { return damageFraction(monst->weaknessAmount * FP_FACTOR * -3/2); } } short monsterDefenseAdjusted(const creature *monst) { short retval; if (monst == &player) { // Weakness is already taken into account in recalculateEquipmentBonuses() for the player. retval = monst->info.defense; } else { retval = monst->info.defense - 25 * monst->weaknessAmount; } return max(retval, 0); } short monsterAccuracyAdjusted(const creature *monst) { short retval = monst->info.accuracy * accuracyFraction(monst->weaknessAmount * FP_FACTOR * -3/2) / FP_FACTOR; return max(retval, 0); } // does NOT account for auto-hit from sleeping or unaware defenders; does account for auto-hit from // stuck or captive defenders and from weapons of slaying. short hitProbability(creature *attacker, creature *defender) { short accuracy = monsterAccuracyAdjusted(attacker); short defense = monsterDefenseAdjusted(defender); short hitProbability; if (defender->status[STATUS_STUCK] || (defender->bookkeepingFlags & MB_CAPTIVE)) { return 100; } if ((defender->bookkeepingFlags & MB_SEIZED) && (attacker->bookkeepingFlags & MB_SEIZING)) { return 100; } if (attacker == &player && rogue.weapon) { if ((rogue.weapon->flags & ITEM_RUNIC) && rogue.weapon->enchant2 == W_SLAYING && monsterIsInClass(defender, rogue.weapon->vorpalEnemy)) { return 100; } accuracy = player.info.accuracy * accuracyFraction(netEnchant(rogue.weapon)) / FP_FACTOR; } hitProbability = accuracy * defenseFraction(defense * FP_FACTOR) / FP_FACTOR; if (hitProbability > 100) { hitProbability = 100; } else if (hitProbability < 0) { hitProbability = 0; } return hitProbability; } boolean attackHit(creature *attacker, creature *defender) { // automatically hit if the monster is sleeping or captive or stuck in a web if (defender->status[STATUS_STUCK] || defender->status[STATUS_PARALYZED] || (defender->bookkeepingFlags & MB_CAPTIVE)) { return true; } return rand_percent(hitProbability(attacker, defender)); } void addMonsterToContiguousMonsterGrid(short x, short y, creature *monst, char grid[DCOLS][DROWS]) { short newX, newY; enum directions dir; creature *tempMonst; grid[x][y] = true; for (dir=0; dir<4; dir++) { newX = x + nbDirs[dir][0]; newY = y + nbDirs[dir][1]; if (coordinatesAreInMap(newX, newY) && !grid[newX][newY]) { tempMonst = monsterAtLoc(newX, newY); if (tempMonst && monstersAreTeammates(monst, tempMonst)) { addMonsterToContiguousMonsterGrid(newX, newY, monst, grid); } } } } // Splits a monster in half. // The split occurs only if there is a spot adjacent to the contiguous // group of monsters that the monster would not avoid. // The contiguous group is supplemented with the given (x, y) coordinates, if any; // this is so that jellies et al. can spawn behind the player in a hallway. void splitMonster(creature *monst, short x, short y) { short i, j, b, dir, newX, newY, eligibleLocationCount, randIndex; char buf[DCOLS * 3]; char monstName[DCOLS]; char monsterGrid[DCOLS][DROWS], eligibleGrid[DCOLS][DROWS]; creature *clone; zeroOutGrid(monsterGrid); zeroOutGrid(eligibleGrid); eligibleLocationCount = 0; // Add the (x, y) location to the contiguous group, if any. if (x > 0 && y > 0) { monsterGrid[x][y] = true; } // Find the contiguous group of monsters. addMonsterToContiguousMonsterGrid(monst->xLoc, monst->yLoc, monst, monsterGrid); // Find the eligible edges around the group of monsters. for (i=0; icurrentHP = (monst->currentHP + 1) / 2; clone = cloneMonster(monst, false, false); // Split monsters don't inherit the learnings of their parents. // Sorry, but self-healing jelly armies are too much. // Mutation effects can be inherited, however; they're not learned abilities. if (monst->mutationIndex >= 0) { clone->info.flags &= (monsterCatalog[clone->info.monsterID].flags | mutationCatalog[monst->mutationIndex].monsterFlags); clone->info.abilityFlags &= (monsterCatalog[clone->info.monsterID].abilityFlags | mutationCatalog[monst->mutationIndex].monsterAbilityFlags); } else { clone->info.flags &= monsterCatalog[clone->info.monsterID].flags; clone->info.abilityFlags &= monsterCatalog[clone->info.monsterID].abilityFlags; } for (b = 0; b < 20; b++) { clone->info.bolts[b] = monsterCatalog[clone->info.monsterID].bolts[b]; } if (!(clone->info.flags & MONST_FLIES) && clone->status[STATUS_LEVITATING] == 1000) { clone->status[STATUS_LEVITATING] = 0; } clone->xLoc = i; clone->yLoc = j; pmap[i][j].flags |= HAS_MONSTER; clone->ticksUntilTurn = max(clone->ticksUntilTurn, 101); fadeInMonster(clone); refreshSideBar(-1, -1, false); if (canDirectlySeeMonster(monst)) { sprintf(buf, "%s splits in two!", monstName); message(buf, false); } return; } } } } } short alliedCloneCount(creature *monst) { short count; creature *temp; count = 0; for (temp = monsters->nextCreature; temp != NULL; temp = temp->nextCreature) { if (temp != monst && temp->info.monsterID == monst->info.monsterID && monstersAreTeammates(temp, monst)) { count++; } } if (rogue.depthLevel > 1) { for (temp = levels[rogue.depthLevel - 2].monsters; temp != NULL; temp = temp->nextCreature) { if (temp != monst && temp->info.monsterID == monst->info.monsterID && monstersAreTeammates(temp, monst)) { count++; } } } if (rogue.depthLevel < DEEPEST_LEVEL) { for (temp = levels[rogue.depthLevel].monsters; temp != NULL; temp = temp->nextCreature) { if (temp != monst && temp->info.monsterID == monst->info.monsterID && monstersAreTeammates(temp, monst)) { count++; } } } return count; } // This function is called whenever one creature acts aggressively against another in a way that directly causes damage. // This can be things like melee attacks, fire/lightning attacks or throwing a weapon. void moralAttack(creature *attacker, creature *defender) { if (attacker == &player && canSeeMonster(defender)) { rogue.featRecord[FEAT_PACIFIST] = false; if (defender->creatureState != MONSTER_TRACKING_SCENT) { rogue.featRecord[FEAT_PALADIN] = false; } } if (defender->currentHP > 0 && !(defender->bookkeepingFlags & MB_IS_DYING)) { if (defender->status[STATUS_PARALYZED]) { defender->status[STATUS_PARALYZED] = 0; // Paralyzed creature gets a turn to react before the attacker moves again. defender->ticksUntilTurn = min(attacker->attackSpeed, 100) - 1; } if (defender->status[STATUS_MAGICAL_FEAR]) { defender->status[STATUS_MAGICAL_FEAR] = 1; } defender->status[STATUS_ENTRANCED] = 0; if (attacker == &player && defender->creatureState == MONSTER_ALLY && !defender->status[STATUS_DISCORDANT] && !attacker->status[STATUS_CONFUSED] && !(attacker->bookkeepingFlags & MB_IS_DYING)) { unAlly(defender); } if ((attacker == &player || attacker->creatureState == MONSTER_ALLY) && defender != &player && defender->creatureState != MONSTER_ALLY) { alertMonster(defender); // this alerts the monster that you're nearby } if ((defender->info.abilityFlags & MA_CLONE_SELF_ON_DEFEND) && alliedCloneCount(defender) < 100) { if (distanceBetween(defender->xLoc, defender->yLoc, attacker->xLoc, attacker->yLoc) <= 1) { splitMonster(defender, attacker->xLoc, attacker->yLoc); } else { splitMonster(defender, 0, 0); } } } } boolean playerImmuneToMonster(creature *monst) { if (monst != &player && rogue.armor && (rogue.armor->flags & ITEM_RUNIC) && (rogue.armor->enchant2 == A_IMMUNITY) && monsterIsInClass(monst, rogue.armor->vorpalEnemy)) { return true; } else { return false; } } void specialHit(creature *attacker, creature *defender, short damage) { short itemCandidates, randItemIndex, stolenQuantity; item *theItem = NULL, *itemFromTopOfStack; char buf[COLS], buf2[COLS], buf3[COLS]; if (!(attacker->info.abilityFlags & SPECIAL_HIT)) { return; } // Special hits that can affect only the player: if (defender == &player) { if (playerImmuneToMonster(attacker)) { return; } if (attacker->info.abilityFlags & MA_HIT_DEGRADE_ARMOR && defender == &player && rogue.armor && !(rogue.armor->flags & ITEM_PROTECTED) && (rogue.armor->enchant1 + rogue.armor->armor/10 > -10)) { rogue.armor->enchant1--; equipItem(rogue.armor, true); itemName(rogue.armor, buf2, false, false, NULL); sprintf(buf, "your %s weakens!", buf2); messageWithColor(buf, &itemMessageColor, false); checkForDisenchantment(rogue.armor); } if (attacker->info.abilityFlags & MA_HIT_HALLUCINATE) { if (!player.status[STATUS_HALLUCINATING]) { combatMessage("you begin to hallucinate", 0); } if (!player.status[STATUS_HALLUCINATING]) { player.maxStatus[STATUS_HALLUCINATING] = 0; } player.status[STATUS_HALLUCINATING] += 20; player.maxStatus[STATUS_HALLUCINATING] = max(player.maxStatus[STATUS_HALLUCINATING], player.status[STATUS_HALLUCINATING]); } if (attacker->info.abilityFlags & MA_HIT_BURN && !defender->status[STATUS_IMMUNE_TO_FIRE]) { exposeCreatureToFire(defender); } if (attacker->info.abilityFlags & MA_HIT_STEAL_FLEE && !(attacker->carriedItem) && (packItems->nextItem) && attacker->currentHP > 0 && !attacker->status[STATUS_CONFUSED] // No stealing from the player if you bump him while confused. && attackHit(attacker, defender)) { itemCandidates = numberOfMatchingPackItems(ALL_ITEMS, 0, (ITEM_EQUIPPED), false); if (itemCandidates) { randItemIndex = rand_range(1, itemCandidates); for (theItem = packItems->nextItem; theItem != NULL; theItem = theItem->nextItem) { if (!(theItem->flags & (ITEM_EQUIPPED))) { if (randItemIndex == 1) { break; } else { randItemIndex--; } } } if (theItem) { if (theItem->category & WEAPON) { // Monkeys will steal half of a stack of weapons, and one of any other stack. if (theItem->quantity > 3) { stolenQuantity = (theItem->quantity + 1) / 2; } else { stolenQuantity = theItem->quantity; } } else { stolenQuantity = 1; } if (stolenQuantity < theItem->quantity) { // Peel off stolen item(s). itemFromTopOfStack = generateItem(ALL_ITEMS, -1); *itemFromTopOfStack = *theItem; // Clone the item. theItem->quantity -= stolenQuantity; itemFromTopOfStack->quantity = stolenQuantity; theItem = itemFromTopOfStack; // Redirect pointer. } else { removeItemFromChain(theItem, packItems); } theItem->flags &= ~ITEM_PLAYER_AVOIDS; // Explore will seek the item out if it ends up on the floor again. attacker->carriedItem = theItem; attacker->creatureMode = MODE_PERM_FLEEING; attacker->creatureState = MONSTER_FLEEING; monsterName(buf2, attacker, true); itemName(theItem, buf3, false, true, NULL); sprintf(buf, "%s stole %s!", buf2, buf3); messageWithColor(buf, &badMessageColor, false); } } } } if ((attacker->info.abilityFlags & MA_POISONS) && damage > 0 && !(defender->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) { addPoison(defender, damage, 1); } if ((attacker->info.abilityFlags & MA_CAUSES_WEAKNESS) && damage > 0 && !(defender->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) { weaken(defender, 300); } if (attacker->info.abilityFlags & MA_ATTACKS_STAGGER) { processStaggerHit(attacker, defender); } } boolean forceWeaponHit(creature *defender, item *theItem) { short oldLoc[2], newLoc[2], forceDamage; char buf[DCOLS*3], buf2[COLS], monstName[DCOLS]; creature *otherMonster = NULL; boolean knowFirstMonsterDied = false, autoID = false; bolt theBolt; monsterName(monstName, defender, true); oldLoc[0] = defender->xLoc; oldLoc[1] = defender->yLoc; newLoc[0] = defender->xLoc + clamp(defender->xLoc - player.xLoc, -1, 1); newLoc[1] = defender->yLoc + clamp(defender->yLoc - player.yLoc, -1, 1); if (canDirectlySeeMonster(defender) && !cellHasTerrainFlag(newLoc[0], newLoc[1], T_OBSTRUCTS_PASSABILITY | T_OBSTRUCTS_VISION) && !(pmap[newLoc[0]][newLoc[1]].flags & (HAS_MONSTER | HAS_PLAYER))) { sprintf(buf, "you launch %s backward with the force of your blow", monstName); buf[DCOLS] = '\0'; combatMessage(buf, messageColorFromVictim(defender)); autoID = true; } theBolt = boltCatalog[BOLT_BLINKING]; theBolt.magnitude = max(1, netEnchant(theItem) / FP_FACTOR); zap(oldLoc, newLoc, &theBolt, false); if (!(defender->bookkeepingFlags & MB_IS_DYING) && distanceBetween(oldLoc[0], oldLoc[1], defender->xLoc, defender->yLoc) > 0 && distanceBetween(oldLoc[0], oldLoc[1], defender->xLoc, defender->yLoc) < weaponForceDistance(netEnchant(theItem))) { if (pmap[defender->xLoc + newLoc[0] - oldLoc[0]][defender->yLoc + newLoc[1] - oldLoc[1]].flags & (HAS_MONSTER | HAS_PLAYER)) { otherMonster = monsterAtLoc(defender->xLoc + newLoc[0] - oldLoc[0], defender->yLoc + newLoc[1] - oldLoc[1]); monsterName(buf2, otherMonster, true); } else { otherMonster = NULL; 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); } forceDamage = distanceBetween(oldLoc[0], oldLoc[1], defender->xLoc, defender->yLoc); if (!(defender->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE)) && inflictDamage(NULL, defender, forceDamage, &white, false)) { if (canDirectlySeeMonster(defender)) { knowFirstMonsterDied = true; sprintf(buf, "%s %s on impact with %s", monstName, (defender->info.flags & MONST_INANIMATE) ? "is destroyed" : "dies", buf2); buf[DCOLS] = '\0'; combatMessage(buf, messageColorFromVictim(defender)); autoID = true; } } else { if (canDirectlySeeMonster(defender)) { sprintf(buf, "%s slams against %s", monstName, buf2); buf[DCOLS] = '\0'; combatMessage(buf, messageColorFromVictim(defender)); autoID = true; } } moralAttack(&player, defender); if (otherMonster && !(defender->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE))) { if (inflictDamage(NULL, otherMonster, forceDamage, &white, false)) { if (canDirectlySeeMonster(otherMonster)) { sprintf(buf, "%s %s%s when %s slams into $HIMHER", buf2, (knowFirstMonsterDied ? "also " : ""), (defender->info.flags & MONST_INANIMATE) ? "is destroyed" : "dies", monstName); resolvePronounEscapes(buf, otherMonster); buf[DCOLS] = '\0'; combatMessage(buf, messageColorFromVictim(otherMonster)); autoID = true; } } if (otherMonster->creatureState != MONSTER_ALLY) { // Allies won't defect if you throw another monster at them, even though it hurts. moralAttack(&player, otherMonster); } } } return autoID; } void magicWeaponHit(creature *defender, item *theItem, boolean backstabbed) { char buf[DCOLS*3], monstName[DCOLS], theItemName[DCOLS]; color *effectColors[NUMBER_WEAPON_RUNIC_KINDS] = {&white, &black, &yellow, &pink, &green, &confusionGasColor, NULL, NULL, &darkRed, &rainbow}; // W_SPEED, W_QUIETUS, W_PARALYSIS, W_MULTIPLICITY, W_SLOWING, W_CONFUSION, W_FORCE, W_SLAYING, W_MERCY, W_PLENTY short chance, i; fixpt enchant; enum weaponEnchants enchantType = theItem->enchant2; creature *newMonst; boolean autoID = false; // If the defender is already dead, proceed only if the runic is speed or multiplicity. // (Everything else acts on the victim, which would literally be overkill.) if ((defender->bookkeepingFlags & MB_IS_DYING) && theItem->enchant2 != W_SPEED && theItem->enchant2 != W_MULTIPLICITY) { return; } enchant = netEnchant(theItem); if (theItem->enchant2 == W_SLAYING) { chance = (monsterIsInClass(defender, theItem->vorpalEnemy) ? 100 : 0); } else if (defender->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE)) { chance = 0; } else { chance = runicWeaponChance(theItem, false, 0); if (backstabbed && chance < 100) { chance = min(chance * 2, (chance + 100) / 2); } } if (chance > 0 && rand_percent(chance)) { if (!(defender->bookkeepingFlags & MB_SUBMERGED)) { switch (enchantType) { case W_SPEED: createFlare(player.xLoc, player.yLoc, SCROLL_ENCHANTMENT_LIGHT); break; case W_QUIETUS: createFlare(defender->xLoc, defender->yLoc, QUIETUS_FLARE_LIGHT); break; case W_SLAYING: createFlare(defender->xLoc, defender->yLoc, SLAYING_FLARE_LIGHT); break; default: flashMonster(defender, effectColors[enchantType], 100); break; } autoID = true; } rogue.disturbed = true; monsterName(monstName, defender, true); itemName(theItem, theItemName, false, false, NULL); switch (enchantType) { case W_SPEED: if (player.ticksUntilTurn != -1) { sprintf(buf, "your %s trembles and time freezes for a moment", theItemName); buf[DCOLS] = '\0'; combatMessage(buf, 0); player.ticksUntilTurn = -1; // free turn! autoID = true; } break; case W_SLAYING: case W_QUIETUS: inflictLethalDamage(&player, defender); sprintf(buf, "%s suddenly %s", monstName, (defender->info.flags & MONST_INANIMATE) ? "shatters" : "dies"); buf[DCOLS] = '\0'; combatMessage(buf, messageColorFromVictim(defender)); autoID = true; break; case W_PARALYSIS: defender->status[STATUS_PARALYZED] = max(defender->status[STATUS_PARALYZED], weaponParalysisDuration(enchant)); defender->maxStatus[STATUS_PARALYZED] = defender->status[STATUS_PARALYZED]; if (canDirectlySeeMonster(defender)) { sprintf(buf, "%s is frozen in place", monstName); buf[DCOLS] = '\0'; combatMessage(buf, messageColorFromVictim(defender)); autoID = true; } break; case W_MULTIPLICITY: sprintf(buf, "Your %s emits a flash of light, and %sspectral duplicate%s appear%s!", theItemName, (weaponImageCount(enchant) == 1 ? "a " : ""), (weaponImageCount(enchant) == 1 ? "" : "s"), (weaponImageCount(enchant) == 1 ? "s" : "")); buf[DCOLS] = '\0'; for (i = 0; i < (weaponImageCount(enchant)); i++) { newMonst = generateMonster(MK_SPECTRAL_IMAGE, true, false); getQualifyingPathLocNear(&(newMonst->xLoc), &(newMonst->yLoc), defender->xLoc, defender->yLoc, true, T_DIVIDES_LEVEL & avoidedFlagsForMonster(&(newMonst->info)), HAS_PLAYER, avoidedFlagsForMonster(&(newMonst->info)), (HAS_PLAYER | HAS_MONSTER | HAS_STAIRS), false); newMonst->bookkeepingFlags |= (MB_FOLLOWER | MB_BOUND_TO_LEADER | MB_DOES_NOT_TRACK_LEADER | MB_TELEPATHICALLY_REVEALED); newMonst->bookkeepingFlags &= ~MB_JUST_SUMMONED; newMonst->leader = &player; newMonst->creatureState = MONSTER_ALLY; if (theItem->flags & ITEM_ATTACKS_STAGGER) { newMonst->info.attackSpeed *= 2; newMonst->info.abilityFlags |= MA_ATTACKS_STAGGER; } if (theItem->flags & ITEM_ATTACKS_QUICKLY) { newMonst->info.attackSpeed /= 2; } if (theItem->flags & ITEM_ATTACKS_PENETRATE) { newMonst->info.abilityFlags |= MA_ATTACKS_PENETRATE; } if (theItem->flags & ITEM_ATTACKS_ALL_ADJACENT) { newMonst->info.abilityFlags |= MA_ATTACKS_ALL_ADJACENT; } if (theItem->flags & ITEM_ATTACKS_EXTEND) { newMonst->info.abilityFlags |= MA_ATTACKS_EXTEND; } newMonst->ticksUntilTurn = 100; newMonst->info.accuracy = player.info.accuracy + (5 * netEnchant(theItem) / FP_FACTOR); newMonst->info.damage = player.info.damage; newMonst->status[STATUS_LIFESPAN_REMAINING] = newMonst->maxStatus[STATUS_LIFESPAN_REMAINING] = weaponImageDuration(enchant); if (strLenWithoutEscapes(theItemName) <= 8) { sprintf(newMonst->info.monsterName, "spectral %s", theItemName); } else { switch (rogue.weapon->kind) { case BROADSWORD: strcpy(newMonst->info.monsterName, "spectral sword"); break; case HAMMER: strcpy(newMonst->info.monsterName, "spectral hammer"); break; case PIKE: strcpy(newMonst->info.monsterName, "spectral pike"); break; case WAR_AXE: strcpy(newMonst->info.monsterName, "spectral axe"); break; default: strcpy(newMonst->info.monsterName, "spectral weapon"); break; } } pmap[newMonst->xLoc][newMonst->yLoc].flags |= HAS_MONSTER; fadeInMonster(newMonst); } updateVision(true); message(buf, false); autoID = true; break; case W_SLOWING: slow(defender, weaponSlowDuration(enchant)); if (canDirectlySeeMonster(defender)) { sprintf(buf, "%s slows down", monstName); buf[DCOLS] = '\0'; combatMessage(buf, messageColorFromVictim(defender)); autoID = true; } break; case W_CONFUSION: defender->status[STATUS_CONFUSED] = max(defender->status[STATUS_CONFUSED], weaponConfusionDuration(enchant)); defender->maxStatus[STATUS_CONFUSED] = defender->status[STATUS_CONFUSED]; if (canDirectlySeeMonster(defender)) { sprintf(buf, "%s looks very confused", monstName); buf[DCOLS] = '\0'; combatMessage(buf, messageColorFromVictim(defender)); autoID = true; } break; case W_FORCE: autoID = forceWeaponHit(defender, theItem); break; case W_MERCY: heal(defender, 50, false); if (canSeeMonster(defender)) { autoID = true; } break; case W_PLENTY: newMonst = cloneMonster(defender, true, true); if (newMonst) { flashMonster(newMonst, effectColors[enchantType], 100); if (canSeeMonster(newMonst)) { autoID = true; } } break; default: break; } } if (autoID) { autoIdentify(theItem); } } void attackVerb(char returnString[DCOLS], creature *attacker, short hitPercentile) { short verbCount, increment; if (attacker != &player && (player.status[STATUS_HALLUCINATING] || !canSeeMonster(attacker))) { strcpy(returnString, "hits"); return; } if (attacker == &player && !rogue.weapon) { strcpy(returnString, "punch"); return; } for (verbCount = 0; verbCount < 4 && monsterText[attacker->info.monsterID].attack[verbCount + 1][0] != '\0'; verbCount++); increment = (100 / (verbCount + 1)); hitPercentile = max(0, min(hitPercentile, increment * (verbCount + 1) - 1)); strcpy(returnString, monsterText[attacker->info.monsterID].attack[hitPercentile / increment]); resolvePronounEscapes(returnString, attacker); } void applyArmorRunicEffect(char returnString[DCOLS], creature *attacker, short *damage, boolean melee) { char armorName[DCOLS], attackerName[DCOLS], monstName[DCOLS], buf[DCOLS * 3]; boolean runicKnown; boolean runicDiscovered; short newDamage, dir, newX, newY, count, i; fixpt enchant; creature *monst, *hitList[8]; returnString[0] = '\0'; if (!(rogue.armor && rogue.armor->flags & ITEM_RUNIC)) { return; // just in case } enchant = netEnchant(rogue.armor); runicKnown = rogue.armor->flags & ITEM_RUNIC_IDENTIFIED; runicDiscovered = false; itemName(rogue.armor, armorName, false, false, NULL); monsterName(attackerName, attacker, true); switch (rogue.armor->enchant2) { case A_MULTIPLICITY: if (melee && !(attacker->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE)) && rand_percent(33)) { for (i = 0; i < armorImageCount(enchant); i++) { monst = cloneMonster(attacker, false, true); monst->bookkeepingFlags |= (MB_FOLLOWER | MB_BOUND_TO_LEADER | MB_DOES_NOT_TRACK_LEADER | MB_TELEPATHICALLY_REVEALED); monst->info.flags |= MONST_DIES_IF_NEGATED; monst->bookkeepingFlags &= ~(MB_JUST_SUMMONED | MB_SEIZED | MB_SEIZING); monst->info.abilityFlags &= ~(MA_CAST_SUMMON | MA_DF_ON_DEATH); // No summoning by spectral images. Gotta draw the line! // Also no exploding or infecting by spectral clones. monst->leader = &player; monst->creatureState = MONSTER_ALLY; monst->status[STATUS_DISCORDANT] = 0; // Otherwise things can get out of control... monst->ticksUntilTurn = 100; monst->info.monsterID = MK_SPECTRAL_IMAGE; if (monst->carriedMonster) { killCreature(monst->carriedMonster, true); // Otherwise you can get infinite phoenices from a discordant phoenix. monst->carriedMonster = NULL; } // Give it the glowy red light and color. monst->info.intrinsicLightType = SPECTRAL_IMAGE_LIGHT; monst->info.foreColor = &spectralImageColor; // Temporary guest! monst->status[STATUS_LIFESPAN_REMAINING] = monst->maxStatus[STATUS_LIFESPAN_REMAINING] = 3; monst->currentHP = monst->info.maxHP = 1; monst->info.defense = 0; if (strLenWithoutEscapes(attacker->info.monsterName) <= 6) { sprintf(monst->info.monsterName, "spectral %s", attacker->info.monsterName); } else { strcpy(monst->info.monsterName, "spectral clone"); } fadeInMonster(monst); } updateVision(true); runicDiscovered = true; sprintf(returnString, "Your %s flashes, and spectral images of %s appear!", armorName, attackerName); } break; case A_MUTUALITY: if (*damage > 0) { count = 0; for (i=0; i<8; i++) { hitList[i] = NULL; dir = i % 8; newX = player.xLoc + nbDirs[dir][0]; newY = player.yLoc + nbDirs[dir][1]; if (coordinatesAreInMap(newX, newY) && (pmap[newX][newY].flags & HAS_MONSTER)) { monst = monsterAtLoc(newX, newY); if (monst && monst != attacker && monstersAreEnemies(&player, monst) && !(monst->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE)) && !(monst->bookkeepingFlags & MB_IS_DYING)) { hitList[i] = monst; count++; } } } if (count) { for (i=0; i<8; i++) { if (hitList[i] && !(hitList[i]->bookkeepingFlags & MB_IS_DYING)) { monsterName(monstName, hitList[i], true); if (inflictDamage(&player, hitList[i], (*damage + count) / (count + 1), &blue, true) && canSeeMonster(hitList[i])) { sprintf(buf, "%s %s", monstName, ((hitList[i]->info.flags & MONST_INANIMATE) ? "is destroyed" : "dies")); combatMessage(buf, messageColorFromVictim(hitList[i])); } } } runicDiscovered = true; if (!runicKnown) { sprintf(returnString, "Your %s pulses, and the damage is shared with %s!", armorName, (count == 1 ? monstName : "the other adjacent enemies")); } *damage = (*damage + count) / (count + 1); } } break; case A_ABSORPTION: *damage -= rand_range(0, armorAbsorptionMax(enchant)); if (*damage <= 0) { *damage = 0; runicDiscovered = true; if (!runicKnown) { sprintf(returnString, "your %s pulses and absorbs the blow!", armorName); } } break; case A_REPRISAL: if (melee && !(attacker->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) { newDamage = max(1, armorReprisalPercent(enchant) * (*damage) / 100); // 5% reprisal per armor level if (inflictDamage(&player, attacker, newDamage, &blue, true)) { if (canSeeMonster(attacker)) { sprintf(returnString, "your %s pulses and %s drops dead!", armorName, attackerName); runicDiscovered = true; } } else if (!runicKnown) { if (canSeeMonster(attacker)) { sprintf(returnString, "your %s pulses and %s shudders in pain!", armorName, attackerName); runicDiscovered = true; } } } break; case A_IMMUNITY: if (monsterIsInClass(attacker, rogue.armor->vorpalEnemy)) { *damage = 0; runicDiscovered = true; } break; case A_BURDEN: if (rand_percent(10)) { rogue.armor->strengthRequired++; sprintf(returnString, "your %s suddenly feels heavier!", armorName); equipItem(rogue.armor, true); runicDiscovered = true; } break; case A_VULNERABILITY: *damage *= 2; if (!runicKnown) { sprintf(returnString, "your %s pulses and you are wracked with pain!", armorName); runicDiscovered = true; } break; case A_IMMOLATION: if (rand_percent(10)) { sprintf(returnString, "flames suddenly explode out of your %s!", armorName); message(returnString, !runicKnown); returnString[0] = '\0'; spawnDungeonFeature(player.xLoc, player.yLoc, &(dungeonFeatureCatalog[DF_ARMOR_IMMOLATION]), true, false); runicDiscovered = true; } default: break; } if (runicDiscovered && !runicKnown) { autoIdentify(rogue.armor); } } void decrementWeaponAutoIDTimer() { char buf[COLS*3], buf2[COLS*3]; if (rogue.weapon && !(rogue.weapon->flags & ITEM_IDENTIFIED) && !--rogue.weapon->charges) { rogue.weapon->flags |= ITEM_IDENTIFIED; updateIdentifiableItems(); messageWithColor("you are now familiar enough with your weapon to identify it.", &itemMessageColor, false); itemName(rogue.weapon, buf2, true, true, NULL); sprintf(buf, "%s %s.", (rogue.weapon->quantity > 1 ? "they are" : "it is"), buf2); messageWithColor(buf, &itemMessageColor, false); } } void processStaggerHit(creature *attacker, creature *defender) { if ((defender->info.flags & (MONST_INVULNERABLE | MONST_IMMOBILE | MONST_INANIMATE)) || (defender->bookkeepingFlags & MB_CAPTIVE) || cellHasTerrainFlag(defender->xLoc, defender->yLoc, T_OBSTRUCTS_PASSABILITY)) { return; } short newX = clamp(defender->xLoc - attacker->xLoc, -1, 1) + defender->xLoc; short newY = clamp(defender->yLoc - attacker->yLoc, -1, 1) + defender->yLoc; if (coordinatesAreInMap(newX, newY) && !cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_PASSABILITY) && !(pmap[newX][newY].flags & (HAS_MONSTER | HAS_PLAYER))) { setMonsterLocation(defender, newX, newY); } } // returns whether the attack hit boolean attack(creature *attacker, creature *defender, boolean lungeAttack) { short damage, specialDamage, poisonDamage; char buf[COLS*2], buf2[COLS*2], attackerName[COLS], defenderName[COLS], verb[DCOLS], explicationClause[DCOLS] = "", armorRunicString[DCOLS*3]; boolean sneakAttack, defenderWasAsleep, defenderWasParalyzed, degradesAttackerWeapon, sightUnseen; if (attacker == &player && canSeeMonster(defender)) { rogue.featRecord[FEAT_PURE_MAGE] = false; } if (attacker->info.abilityFlags & MA_KAMIKAZE) { killCreature(attacker, false); return true; } armorRunicString[0] = '\0'; poisonDamage = 0; degradesAttackerWeapon = (defender->info.flags & MONST_DEFEND_DEGRADE_WEAPON ? true : false); sightUnseen = !canSeeMonster(attacker) && !canSeeMonster(defender); if (defender->status[STATUS_LEVITATING] && (attacker->info.flags & MONST_RESTRICTED_TO_LIQUID)) { return false; // aquatic or other liquid-bound monsters cannot attack flying opponents } if ((attacker == &player || defender == &player) && !rogue.blockCombatText) { rogue.disturbed = true; } defender->status[STATUS_ENTRANCED] = 0; if (defender->status[STATUS_MAGICAL_FEAR]) { defender->status[STATUS_MAGICAL_FEAR] = 1; } if (attacker == &player && defender->creatureState != MONSTER_TRACKING_SCENT) { rogue.featRecord[FEAT_PALADIN] = false; } if (attacker != &player && defender == &player && attacker->creatureState == MONSTER_WANDERING) { attacker->creatureState = MONSTER_TRACKING_SCENT; } if (defender->info.flags & MONST_INANIMATE) { sneakAttack = false; defenderWasAsleep = false; defenderWasParalyzed = false; } else { sneakAttack = (defender != &player && attacker == &player && (defender->creatureState == MONSTER_WANDERING) ? true : false); defenderWasAsleep = (defender != &player && (defender->creatureState == MONSTER_SLEEPING) ? true : false); defenderWasParalyzed = defender->status[STATUS_PARALYZED] > 0; } monsterName(attackerName, attacker, true); monsterName(defenderName, defender, true); if ((attacker->info.abilityFlags & MA_SEIZES) && (!(attacker->bookkeepingFlags & MB_SEIZING) || !(defender->bookkeepingFlags & MB_SEIZED)) && (rogue.patchVersion < 2 || (distanceBetween(attacker->xLoc, attacker->yLoc, defender->xLoc, defender->yLoc) == 1 && !diagonalBlocked(attacker->xLoc, attacker->yLoc, defender->xLoc, defender->yLoc, false)))) { attacker->bookkeepingFlags |= MB_SEIZING; defender->bookkeepingFlags |= MB_SEIZED; if (canSeeMonster(attacker) || canSeeMonster(defender)) { sprintf(buf, "%s seizes %s!", attackerName, (defender == &player ? "your legs" : defenderName)); messageWithColor(buf, &white, false); } return false; } if (sneakAttack || defenderWasAsleep || defenderWasParalyzed || lungeAttack || attackHit(attacker, defender)) { // If the attack hit: damage = (defender->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE) ? 0 : randClump(attacker->info.damage) * monsterDamageAdjustmentAmount(attacker) / FP_FACTOR); if (sneakAttack || defenderWasAsleep || defenderWasParalyzed) { if (defender != &player) { // The non-player defender doesn't hit back this turn because it's still flat-footed. defender->ticksUntilTurn += max(defender->movementSpeed, defender->attackSpeed); if (defender->creatureState != MONSTER_ALLY) { defender->creatureState = MONSTER_TRACKING_SCENT; // Wake up! } } } if (sneakAttack || defenderWasAsleep || defenderWasParalyzed || lungeAttack) { if (attacker == &player && rogue.weapon && (rogue.weapon->flags & ITEM_SNEAK_ATTACK_BONUS)) { damage *= 5; // 5x damage for dagger sneak attacks. } else { damage *= 3; // Treble damage for general sneak attacks. } } if (defender == &player && rogue.armor && (rogue.armor->flags & ITEM_RUNIC)) { applyArmorRunicEffect(armorRunicString, attacker, &damage, true); } if (attacker == &player && rogue.reaping && !(defender->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) { specialDamage = min(damage, defender->currentHP) * rogue.reaping; // Maximum reaped damage can't exceed the victim's remaining health. if (rogue.reaping > 0) { specialDamage = rand_range(0, specialDamage); } else { specialDamage = rand_range(specialDamage, 0); } if (specialDamage) { rechargeItemsIncrementally(specialDamage); } } if (damage == 0) { sprintf(explicationClause, " but %s no damage", (attacker == &player ? "do" : "does")); if (attacker == &player) { rogue.disturbed = true; } } else if (lungeAttack) { strcpy(explicationClause, " with a vicious lunge attack"); } else if (defenderWasParalyzed) { sprintf(explicationClause, " while $HESHE %s paralyzed", (defender == &player ? "are" : "is")); } else if (defenderWasAsleep) { strcpy(explicationClause, " in $HISHER sleep"); } else if (sneakAttack) { strcpy(explicationClause, ", catching $HIMHER unaware"); } else if (defender->status[STATUS_STUCK] || defender->bookkeepingFlags & MB_CAPTIVE) { sprintf(explicationClause, " while %s dangle%s helplessly", (canSeeMonster(defender) ? "$HESHE" : "it"), (defender == &player ? "" : "s")); } resolvePronounEscapes(explicationClause, defender); if ((attacker->info.abilityFlags & MA_POISONS) && damage > 0) { poisonDamage = damage; damage = 1; } if (inflictDamage(attacker, defender, damage, &red, false)) { // if the attack killed the defender if (defenderWasAsleep || sneakAttack || defenderWasParalyzed || lungeAttack) { sprintf(buf, "%s %s %s%s", attackerName, ((defender->info.flags & MONST_INANIMATE) ? "destroyed" : "dispatched"), defenderName, explicationClause); } else { sprintf(buf, "%s %s %s%s", attackerName, ((defender->info.flags & MONST_INANIMATE) ? "destroyed" : "defeated"), defenderName, explicationClause); } if (sightUnseen) { if (defender->info.flags & MONST_INANIMATE) { combatMessage("you hear something get destroyed in combat", 0); } else { combatMessage("you hear something die in combat", 0); } } else { combatMessage(buf, (damage > 0 ? messageColorFromVictim(defender) : &white)); } if (&player == defender) { gameOver(attacker->info.monsterName, false); return true; } else if (&player == attacker && defender->info.monsterID == MK_DRAGON) { rogue.featRecord[FEAT_DRAGONSLAYER] = true; } } else { // if the defender survived if (!rogue.blockCombatText && (canSeeMonster(attacker) || canSeeMonster(defender))) { attackVerb(verb, attacker, max(damage - (attacker->info.damage.lowerBound * monsterDamageAdjustmentAmount(attacker) / FP_FACTOR), 0) * 100 / max(1, (attacker->info.damage.upperBound - attacker->info.damage.lowerBound) * monsterDamageAdjustmentAmount(attacker) / FP_FACTOR)); sprintf(buf, "%s %s %s%s", attackerName, verb, defenderName, explicationClause); if (sightUnseen) { if (!rogue.heardCombatThisTurn) { rogue.heardCombatThisTurn = true; combatMessage("you hear combat in the distance", 0); } } else { combatMessage(buf, messageColorFromVictim(defender)); } } if (attacker == &player && rogue.weapon && (rogue.weapon->flags & ITEM_ATTACKS_STAGGER)) { processStaggerHit(attacker, defender); } if (attacker->info.abilityFlags & SPECIAL_HIT) { specialHit(attacker, defender, (attacker->info.abilityFlags & MA_POISONS) ? poisonDamage : damage); } if (armorRunicString[0]) { message(armorRunicString, false); if (rogue.armor && (rogue.armor->flags & ITEM_RUNIC) && rogue.armor->enchant2 == A_BURDEN) { strengthCheck(rogue.armor); } } } moralAttack(attacker, defender); if (attacker == &player && rogue.weapon && (rogue.weapon->flags & ITEM_RUNIC)) { magicWeaponHit(defender, rogue.weapon, sneakAttack || defenderWasAsleep || defenderWasParalyzed); } if (attacker == &player && (defender->bookkeepingFlags & MB_IS_DYING) && (defender->bookkeepingFlags & MB_HAS_SOUL)) { decrementWeaponAutoIDTimer(); } if (degradesAttackerWeapon && attacker == &player && rogue.weapon && !(rogue.weapon->flags & ITEM_PROTECTED) // Can't damage a Weapon of Acid Mound Slaying by attacking an acid mound... just ain't right! && !((rogue.weapon->flags & ITEM_RUNIC) && rogue.weapon->enchant2 == W_SLAYING && monsterIsInClass(defender, rogue.weapon->vorpalEnemy)) && rogue.weapon->enchant1 >= -10) { rogue.weapon->enchant1--; if (rogue.weapon->quiverNumber) { rogue.weapon->quiverNumber = rand_range(1, 60000); } equipItem(rogue.weapon, true); itemName(rogue.weapon, buf2, false, false, NULL); sprintf(buf, "your %s weakens!", buf2); messageWithColor(buf, &itemMessageColor, false); checkForDisenchantment(rogue.weapon); } return true; } else { // if the attack missed if (!rogue.blockCombatText) { if (sightUnseen) { if (!rogue.heardCombatThisTurn) { rogue.heardCombatThisTurn = true; combatMessage("you hear combat in the distance", 0); } } else { sprintf(buf, "%s missed %s", attackerName, defenderName); combatMessage(buf, 0); } } return false; } } // Gets the length of a string without the four-character color escape sequences, since those aren't displayed. short strLenWithoutEscapes(const char *str) { short i, count; count = 0; for (i=0; str[i];) { if (str[i] == COLOR_ESCAPE) { i += 4; continue; } count++; i++; } return count; } void combatMessage(char *theMsg, color *theColor) { char newMsg[COLS * 2]; if (theColor == 0) { theColor = &white; } newMsg[0] = '\0'; encodeMessageColor(newMsg, 0, theColor); strcat(newMsg, theMsg); if (strLenWithoutEscapes(combatText) + strLenWithoutEscapes(newMsg) + 3 > DCOLS) { // the "3" is for the semicolon, space and period that get added to conjoined combat texts. displayCombatText(); } if (combatText[0]) { strcat(combatText, "; "); strcat(combatText, newMsg); } else { strcpy(combatText, newMsg); } } void displayCombatText() { char buf[COLS]; if (combatText[0]) { sprintf(buf, "%s.", combatText); combatText[0] = '\0'; message(buf, rogue.cautiousMode); rogue.cautiousMode = false; } } void flashMonster(creature *monst, const color *theColor, short strength) { if (!theColor) { return; } if (!(monst->bookkeepingFlags & MB_WILL_FLASH) || monst->flashStrength < strength) { monst->bookkeepingFlags |= MB_WILL_FLASH; monst->flashStrength = strength; monst->flashColor = *theColor; rogue.creaturesWillFlashThisTurn = true; } } boolean canAbsorb(creature *ally, boolean ourBolts[NUMBER_BOLT_KINDS], creature *prey, short **grid) { short i; if (ally->creatureState == MONSTER_ALLY && ally->newPowerCount > 0 && (ally->targetCorpseLoc[0] <= 0) && !((ally->info.flags | prey->info.flags) & (MONST_INANIMATE | MONST_IMMOBILE)) && !monsterAvoids(ally, prey->xLoc, prey->yLoc) && grid[ally->xLoc][ally->yLoc] <= 10) { if (~(ally->info.abilityFlags) & prey->info.abilityFlags & LEARNABLE_ABILITIES) { return true; } else if (~(ally->info.flags) & prey->info.flags & LEARNABLE_BEHAVIORS) { return true; } else { for (i = 0; i < NUMBER_BOLT_KINDS; i++) { ourBolts[i] = false; } for (i = 0; ally->info.bolts[i] != BOLT_NONE; i++) { ourBolts[ally->info.bolts[i]] = true; } for (i=0; prey->info.bolts[i] != BOLT_NONE; i++) { if (!(boltCatalog[prey->info.bolts[i]].flags & BF_NOT_LEARNABLE) && !ourBolts[prey->info.bolts[i]]) { return true; } } } } return false; } boolean anyoneWantABite(creature *decedent) { short candidates, randIndex, i; short **grid; creature *ally; boolean success = false; boolean ourBolts[NUMBER_BOLT_KINDS] = {false}; candidates = 0; if ((!(decedent->info.abilityFlags & LEARNABLE_ABILITIES) && !(decedent->info.flags & LEARNABLE_BEHAVIORS) && decedent->info.bolts[0] == BOLT_NONE) || (cellHasTerrainFlag(decedent->xLoc, decedent->yLoc, T_PATHING_BLOCKER)) || decedent->info.monsterID == MK_SPECTRAL_IMAGE || (decedent->info.flags & (MONST_INANIMATE | MONST_IMMOBILE))) { return false; } grid = allocGrid(); fillGrid(grid, 0); calculateDistances(grid, decedent->xLoc, decedent->yLoc, T_PATHING_BLOCKER, NULL, true, true); for (ally = monsters->nextCreature; ally != NULL; ally = ally->nextCreature) { if (canAbsorb(ally, ourBolts, decedent, grid)) { candidates++; } } if (candidates > 0) { randIndex = rand_range(1, candidates); for (ally = monsters->nextCreature; ally != NULL; ally = ally->nextCreature) { // CanAbsorb() populates ourBolts if it returns true and there are no learnable behaviors or flags: if (canAbsorb(ally, ourBolts, decedent, grid) && !--randIndex) { break; } } if (ally) { ally->targetCorpseLoc[0] = decedent->xLoc; ally->targetCorpseLoc[1] = decedent->yLoc; strcpy(ally->targetCorpseName, decedent->info.monsterName); ally->corpseAbsorptionCounter = 20; // 20 turns to get there and start eating before he loses interest // Choose a superpower. // First, select from among learnable ability or behavior flags, if one is available. candidates = 0; for (i=0; i<32; i++) { if (Fl(i) & ~(ally->info.abilityFlags) & decedent->info.abilityFlags & LEARNABLE_ABILITIES) { candidates++; } } for (i=0; i<32; i++) { if (Fl(i) & ~(ally->info.flags) & decedent->info.flags & LEARNABLE_BEHAVIORS) { candidates++; } } if (candidates > 0) { randIndex = rand_range(1, candidates); for (i=0; i<32; i++) { if ((Fl(i) & ~(ally->info.abilityFlags) & decedent->info.abilityFlags & LEARNABLE_ABILITIES) && !--randIndex) { ally->absorptionFlags = Fl(i); ally->absorbBehavior = false; success = true; break; } } for (i=0; i<32 && !success; i++) { if ((Fl(i) & ~(ally->info.flags) & decedent->info.flags & LEARNABLE_BEHAVIORS) && !--randIndex) { ally->absorptionFlags = Fl(i); ally->absorbBehavior = true; success = true; break; } } } else if (decedent->info.bolts[0] != BOLT_NONE) { // If there are no learnable ability or behavior flags, pick a learnable bolt. candidates = 0; for (i=0; decedent->info.bolts[i] != BOLT_NONE; i++) { if (!(boltCatalog[decedent->info.bolts[i]].flags & BF_NOT_LEARNABLE) && !ourBolts[decedent->info.bolts[i]]) { candidates++; } } if (candidates > 0) { randIndex = rand_range(1, candidates); for (i=0; decedent->info.bolts[i] != BOLT_NONE; i++) { if (!(boltCatalog[decedent->info.bolts[i]].flags & BF_NOT_LEARNABLE) && !ourBolts[decedent->info.bolts[i]] && !--randIndex) { ally->absorptionBolt = decedent->info.bolts[i]; success = true; break; } } } } } } freeGrid(grid); return success; } #define MIN_FLASH_STRENGTH 50 void inflictLethalDamage(creature *attacker, creature *defender) { inflictDamage(attacker, defender, defender->currentHP, NULL, true); } // returns true if this was a killing stroke; does NOT free the pointer, but DOES remove it from the monster chain // flashColor indicates the color that the damage will cause the creature to flash boolean inflictDamage(creature *attacker, creature *defender, short damage, const color *flashColor, boolean ignoresProtectionShield) { boolean killed = false; dungeonFeature theBlood; short transferenceAmount; if (damage == 0 || (defender->info.flags & MONST_INVULNERABLE)) { return false; } if (!ignoresProtectionShield && defender->status[STATUS_SHIELDED]) { if (defender->status[STATUS_SHIELDED] > damage * 10) { defender->status[STATUS_SHIELDED] -= damage * 10; damage = 0; } else { damage -= (defender->status[STATUS_SHIELDED] + 9) / 10; defender->status[STATUS_SHIELDED] = defender->maxStatus[STATUS_SHIELDED] = 0; } } defender->bookkeepingFlags &= ~MB_ABSORBING; // Stop eating a corpse if you are getting hurt. // bleed all over the place, proportionately to damage inflicted: if (damage > 0 && defender->info.bloodType) { theBlood = dungeonFeatureCatalog[defender->info.bloodType]; theBlood.startProbability = (theBlood.startProbability * (15 + min(damage, defender->currentHP) * 3 / 2) / 100); if (theBlood.layer == GAS) { theBlood.startProbability *= 100; } spawnDungeonFeature(defender->xLoc, defender->yLoc, &theBlood, true, false); } if (defender != &player && defender->creatureState == MONSTER_SLEEPING) { wakeUp(defender); } if (defender == &player && rogue.easyMode && damage > 0) { damage = max(1, damage/5); } if (((attacker == &player && rogue.transference) || (attacker && attacker != &player && (attacker->info.abilityFlags & MA_TRANSFERENCE))) && !(defender->info.flags & (MONST_INANIMATE | MONST_INVULNERABLE))) { transferenceAmount = min(damage, defender->currentHP); // Maximum transferred damage can't exceed the victim's remaining health. if (attacker == &player) { transferenceAmount = transferenceAmount * rogue.transference / 20; if (transferenceAmount == 0) { transferenceAmount = ((rogue.transference > 0) ? 1 : -1); } } else if (attacker->creatureState == MONSTER_ALLY) { transferenceAmount = transferenceAmount * 4 / 10; // allies get 40% recovery rate } else { transferenceAmount = transferenceAmount * 9 / 10; // enemies get 90% recovery rate, deal with it } attacker->currentHP += transferenceAmount; if (attacker == &player && player.currentHP <= 0) { gameOver("Drained by a cursed ring", true); return false; } } if (defender->currentHP <= damage) { // killed killCreature(defender, false); anyoneWantABite(defender); killed = true; } else { // survived if (damage < 0 && defender->currentHP - damage > defender->info.maxHP) { defender->currentHP = max(defender->currentHP, defender->info.maxHP); } else { defender->currentHP -= damage; // inflict the damage! if (defender == &player && damage > 0) { rogue.featRecord[FEAT_INDOMITABLE] = false; } } if (defender != &player && defender->creatureState != MONSTER_ALLY && defender->info.flags & MONST_FLEES_NEAR_DEATH && defender->info.maxHP / 4 >= defender->currentHP) { defender->creatureState = MONSTER_FLEEING; } if (flashColor && damage > 0) { flashMonster(defender, flashColor, MIN_FLASH_STRENGTH + (100 - MIN_FLASH_STRENGTH) * damage / defender->info.maxHP); } } refreshSideBar(-1, -1, false); return killed; } void addPoison(creature *monst, short durationIncrement, short concentrationIncrement) { extern const color poisonColor; if (durationIncrement > 0) { if (monst == &player && !player.status[STATUS_POISONED]) { combatMessage("scalding poison fills your veins", &badMessageColor); } if (!monst->status[STATUS_POISONED]) { monst->maxStatus[STATUS_POISONED] = 0; } monst->poisonAmount += concentrationIncrement; if (monst->poisonAmount == 0) { monst->poisonAmount = 1; } monst->status[STATUS_POISONED] += durationIncrement; monst->maxStatus[STATUS_POISONED] = monst->info.maxHP / monst->poisonAmount; if (canSeeMonster(monst)) { flashMonster(monst, &poisonColor, 100); } } } // Removes the decedent from the screen and from the monster chain; inserts it into the graveyard chain; does NOT free the memory. // Or, if the decedent is a player ally at the moment of death, insert it into the purgatory chain for possible future resurrection. // Use "administrativeDeath" if the monster is being deleted for administrative purposes, as opposed to dying as a result of physical actions. // AdministrativeDeath means the monster simply disappears, with no messages, dropped item, DFs or other effect. void killCreature(creature *decedent, boolean administrativeDeath) { short x, y; char monstName[DCOLS], buf[DCOLS * 3]; if (decedent->bookkeepingFlags & MB_IS_DYING) { // monster has already been killed; let's avoid overkill return; } if (decedent != &player) { decedent->bookkeepingFlags |= MB_IS_DYING; } if (rogue.lastTarget == decedent) { rogue.lastTarget = NULL; } if (rogue.yendorWarden == decedent) { rogue.yendorWarden = NULL; } if (decedent->carriedItem) { if (administrativeDeath) { deleteItem(decedent->carriedItem); decedent->carriedItem = NULL; } else { makeMonsterDropItem(decedent); } } if (!administrativeDeath && (decedent->info.abilityFlags & MA_DF_ON_DEATH) && ((rogue.patchVersion < 3) || !(decedent->bookkeepingFlags & MB_IS_FALLING))) { spawnDungeonFeature(decedent->xLoc, decedent->yLoc, &dungeonFeatureCatalog[decedent->info.DFType], true, false); if (monsterText[decedent->info.monsterID].DFMessage[0] && canSeeMonster(decedent)) { monsterName(monstName, decedent, true); snprintf(buf, DCOLS * 3, "%s %s", monstName, monsterText[decedent->info.monsterID].DFMessage); resolvePronounEscapes(buf, decedent); message(buf, false); } } if (decedent == &player) { // the player died // game over handled elsewhere } else { if (!administrativeDeath && decedent->creatureState == MONSTER_ALLY && !canSeeMonster(decedent) && !(decedent->info.flags & MONST_INANIMATE) && !(decedent->bookkeepingFlags & MB_BOUND_TO_LEADER) && !decedent->carriedMonster) { messageWithColor("you feel a sense of loss.", &badMessageColor, false); } x = decedent->xLoc; y = decedent->yLoc; if (decedent->bookkeepingFlags & MB_IS_DORMANT) { pmap[x][y].flags &= ~HAS_DORMANT_MONSTER; } else { pmap[x][y].flags &= ~HAS_MONSTER; } removeMonsterFromChain(decedent, dormantMonsters); removeMonsterFromChain(decedent, monsters); if (decedent->leader == &player && !(decedent->info.flags & MONST_INANIMATE) && (decedent->bookkeepingFlags & MB_HAS_SOUL) && !administrativeDeath) { decedent->nextCreature = purgatory->nextCreature; purgatory->nextCreature = decedent; } else { decedent->nextCreature = graveyard->nextCreature; graveyard->nextCreature = decedent; } if (!administrativeDeath && !(decedent->bookkeepingFlags & MB_IS_DORMANT)) { // Was there another monster inside? if (decedent->carriedMonster) { // Insert it into the chain. decedent->carriedMonster->nextCreature = monsters->nextCreature; monsters->nextCreature = decedent->carriedMonster; decedent->carriedMonster->xLoc = x; decedent->carriedMonster->yLoc = y; decedent->carriedMonster->ticksUntilTurn = 200; pmap[x][y].flags |= HAS_MONSTER; fadeInMonster(decedent->carriedMonster); if (canSeeMonster(decedent->carriedMonster)) { monsterName(monstName, decedent->carriedMonster, true); sprintf(buf, "%s appears", monstName); combatMessage(buf, NULL); } applyInstantTileEffectsToCreature(decedent->carriedMonster); decedent->carriedMonster = NULL; } refreshDungeonCell(x, y); } } decedent->currentHP = 0; demoteMonsterFromLeadership(decedent); if (decedent->leader) { checkForContinuedLeadership(decedent->leader); } } void buildHitList(creature **hitList, const creature *attacker, creature *defender, const boolean sweep) { short i, x, y, newX, newY, newestX, newestY; enum directions dir, newDir; x = attacker->xLoc; y = attacker->yLoc; newX = defender->xLoc; newY = defender->yLoc; dir = NO_DIRECTION; for (i = 0; i < DIRECTION_COUNT; i++) { if (nbDirs[i][0] == newX - x && nbDirs[i][1] == newY - y) { dir = i; break; } } if (sweep) { if (dir == NO_DIRECTION) { dir = UP; // Just pick one. } for (i=0; i<8; i++) { newDir = (dir + i) % DIRECTION_COUNT; newestX = x + cDirs[newDir][0]; newestY = y + cDirs[newDir][1]; if (coordinatesAreInMap(newestX, newestY) && (pmap[newestX][newestY].flags & (HAS_MONSTER | HAS_PLAYER))) { defender = monsterAtLoc(newestX, newestY); if (defender && monsterWillAttackTarget(attacker, defender) && (!cellHasTerrainFlag(defender->xLoc, defender->yLoc, T_OBSTRUCTS_PASSABILITY) || (defender->info.flags & MONST_ATTACKABLE_THRU_WALLS))) { hitList[i] = defender; } } } } else { hitList[0] = defender; } }