File: [contributed] / brogue-ce / src / brogue / Combat.c (download)
Revision 1.1.1.1 (vendor branch), Thu May 27 20:31:37 2021 UTC (3 years, 3 months ago) by rubenllorente
Branch: tmewett, MAIN
CVS Tags: start, HEAD Changes since 1.1: +0 -0 lines
Initial Import from release 1.9.3
|
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#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; i<DCOLS; i++) {
for (j=0; j<DROWS; j++) {
if (monsterGrid[i][j]) {
for (dir=0; dir<4; dir++) {
newX = i + nbDirs[dir][0];
newY = j + nbDirs[dir][1];
if (coordinatesAreInMap(newX, newY)
&& !eligibleGrid[newX][newY]
&& !monsterGrid[newX][newY]
&& !(pmap[newX][newY].flags & (HAS_PLAYER | HAS_MONSTER))
&& !monsterAvoids(monst, newX, newY)) {
eligibleGrid[newX][newY] = true;
eligibleLocationCount++;
}
}
}
}
}
// DEBUG {
// hiliteCharGrid(eligibleGrid, &green, 75);
// hiliteCharGrid(monsterGrid, &blue, 75);
// temporaryMessage("Jelly spawn possibilities (green = eligible, blue = monster):", true);
// displayLevel();
// }
// Pick a random location on the eligibleGrid and add the clone there.
if (eligibleLocationCount) {
randIndex = rand_range(1, eligibleLocationCount);
for (i=0; i<DCOLS; i++) {
for (j=0; j<DROWS; j++) {
if (eligibleGrid[i][j] && !--randIndex) {
// Found the spot!
monsterName(monstName, monst, true);
monst->currentHP = (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;
}
}