diff --git a/src/brogue/Combat.c b/src/brogue/Combat.c index 2eaea320..be3ea907 100644 --- a/src/brogue/Combat.c +++ b/src/brogue/Combat.c @@ -1733,44 +1733,41 @@ void killCreature(creature *decedent, boolean administrativeDeath) { } } -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->loc.x; - y = attacker->loc.y; - newX = defender->loc.x; - newY = defender->loc.y; - - dir = NO_DIRECTION; - for (i = 0; i < DIRECTION_COUNT; i++) { - if (nbDirs[i][0] == newX - x - && nbDirs[i][1] == newY - y) { +enum directions posDirectionToNeighborPos( + const pos *source, + const pos *target, + enum directions otherwise ) { + + enum directions dir = otherwise; + const pos p = (pos) { + target->x - source->x, + target->y - source->y + }; + for (short i = 0; i < DIRECTION_COUNT; i++) { + if (nbDirs[i][0] == p.x && nbDirs[i][1] == p.y) { dir = i; break; } } + return dir; +} - 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((pos){ newestX, newestY }); - if (defender - && monsterWillAttackTarget(attacker, defender) - && (!cellHasTerrainFlag(defender->loc, T_OBSTRUCTS_PASSABILITY) || (defender->info.flags & MONST_ATTACKABLE_THRU_WALLS))) { - - hitList[i] = defender; - } - } - } - } else { +void buildHitList(creature **hitList, const creature *attacker, creature *defender, const boolean sweep) { + + if (!sweep) { hitList[0] = defender; + return; + } + + enum directions bumpDir = posDirectionToNeighborPos( &attacker->loc, &defender->loc, UP ); + + for (short i=0, dir=bumpDir; i < DIRECTION_COUNT; i++, dir++) { + dir %= DIRECTION_COUNT; + const pos p = posNeighborInDirection( attacker->loc, dir ); + defender = monsterAtLoc(p); + if (ableAndWillingToAttack(attacker, defender, dir == bumpDir, 1)) { + hitList[i] = defender; + } } } diff --git a/src/brogue/Items.c b/src/brogue/Items.c index 608fdbca..f0f06e08 100644 --- a/src/brogue/Items.c +++ b/src/brogue/Items.c @@ -3528,14 +3528,17 @@ void getImpactLoc(pos *returnLoc, const pos originLoc, const pos targetLoc, pos coords[DCOLS + 1]; short i, n; creature *monst; + creature *caster = monsterAtLoc(originLoc); n = getLineCoordinates(coords, originLoc, targetLoc, theBolt); n = min(n, maxDistance); for (i=0; ibookkeepingFlags & MB_SUBMERGED)) { + // Imaginary bolt hit the player or a monster. break; } diff --git a/src/brogue/Monsters.c b/src/brogue/Monsters.c index e7a055f1..6cab9fa8 100644 --- a/src/brogue/Monsters.c +++ b/src/brogue/Monsters.c @@ -307,6 +307,182 @@ static boolean attackWouldBeFutile(const creature *attacker, const creature *def return false; } +// ------------------------------------------------------------------------ +const short notSeeInvis = 0; +const short yesSeeInvis = 1; +const short isBumping = 2; + +static boolean monsterIsEffectivelySubmerged( const creature *monst ) { + return (monst->bookkeepingFlags & MB_SUBMERGED) + || ((terrainFlags(monst->loc) & T_IS_DEEP_WATER) && !monst->status[STATUS_LEVITATING]); +} + +static boolean monsterIsEffectivelyInvisible( const creature *monst, boolean ignoreInvisibility) { + if (ignoreInvisibility) return false; + return monst->status[STATUS_INVISIBLE] && !pmapAt(monst->loc)->layers[GAS]; +} + +boolean monsterKnowsLocationOfMonster( + const creature *source, + const creature *target, + short sensing ) { + + // Can't locate invalid entities + if (source == NULL || target == NULL) return false; + + // Always know location of self + if (source == target) return true; + + // Special player abilities + if (source == &player) { + // Player can see entranced, psychic-linked allies, and sacrifices. + // With telepathy, they can also see any animate creature. + // *** Q: Any special cases for clairvoyantly revealed creatures? + if (target->status[STATUS_ENTRANCED]) return true; + if (target->bookkeepingFlags & MB_TELEPATHICALLY_REVEALED) return true; + if (player.status[STATUS_TELEPATHIC] && !(target->info.flags & MONST_INANIMATE)) return true; + + // Otherwise, player requires line-of-sight / clairvoyantly revealed cells. + if (!playerCanSee(target->loc.x, target->loc.y)) return false; + } + + // Can't locate dormant creatures + if (target->bookkeepingFlags & MB_IS_DORMANT) return false; + + // Can always see teammates + if (monstersAreTeammates(source, target)) return true; + + // Invisibility & submersion are ignored when bumping into the monster + if (sensing == isBumping) return true; + + // Can't see invisible monsters + if (monsterIsEffectivelyInvisible(target, sensing >= yesSeeInvis)) return false; + + // Submerged monsters and monsters in deep water can see other submerged monsters + if ((target->bookkeepingFlags & MB_SUBMERGED) + && !monsterIsEffectivelySubmerged(source)) { + return false; + } + + return true; +} + +static boolean monsterIsNotAggressiveToPlayer(const creature *monst) { + return monst == &player + || monst->creatureState == MONSTER_ALLY; +} + +boolean monsterIsAggressiveToMonster(const creature *attacker, const creature *defender) { + + // Never aggressive when... + if (attacker == defender) return false; + if (attacker == NULL || defender == NULL) return false; + if (defender->bookkeepingFlags & MB_IS_DYING) return false; + if ((attacker->bookkeepingFlags | defender->bookkeepingFlags) & MB_CAPTIVE) return false; + + // Always aggressive when discordant + if (attacker->status[STATUS_DISCORDANT] + || defender->status[STATUS_DISCORDANT]) return true; + + // Not agressive to (non-discordant) teammates + if (monstersAreTeammates(attacker,defender)) return false; + + // Don't let (sane) allies attack sacrifice targets or entranced monsters + if (attacker->creatureState == MONSTER_ALLY + && ((defender->bookkeepingFlags & MB_MARKED_FOR_SACRIFICE) + || defender->status[STATUS_ENTRANCED])) + return false; + + // Water creatures attack anything in deep water (eg. eels and krakens) + if ((attacker->info.flags & MONST_RESTRICTED_TO_LIQUID) + && !(defender->info.flags & MONST_IMMUNE_TO_WATER) + && monsterIsEffectivelySubmerged(defender)) + return true; + + // Aligned on aggressiveness toward the player.. + if (monsterIsNotAggressiveToPlayer(attacker) == monsterIsNotAggressiveToPlayer(defender)) return false; + + // Default to aggressive + return true; +} + +// DISTINCTION: +// monsterIsWillingToAttackMonster +// vs monsterIsAggressiveToMonster +// +// - Agressiveness is the native URGE to do harm +// - Willingness is the CHOICE to do harm +// +// - EXAMPLE: Allies have the URGE to harm a Revnant, but CHOOSE not to because it is futile +// - EXAMPLE: Allies do not have the URGE to harm the player, but a confused ally will CHOOSE to +boolean monsterIsWillingToAttackMonster(const creature *attacker, const creature *defender) { + + if (attacker == NULL || defender == NULL) return false; + + // Fearful monsters will never attack. + if (attacker->status[STATUS_MAGICAL_FEAR]) return false; + + // Confused monsters will attack anything + if (attacker->status[STATUS_CONFUSED]) return true; + + // Entranced monsters will attack anything except player allies + if (attacker->status[STATUS_ENTRANCED] + && defender->creatureState != MONSTER_ALLY) return true; + + // Otherwise make non-futile attacks on enemies + return (monsterIsAggressiveToMonster(attacker, defender) + && !attackWouldBeFutile(attacker, defender)); +} + +// Passing 0 for maxRange means "any distance" +boolean monsterIsAbleToStrikeMonster( + const creature *attacker, + const creature *defender, + int maxRange) { + + if (attacker == NULL || defender == NULL) return false; + + // Is the target within range? + boolean withinRange = maxRange <= 0 || distanceBetween(attacker->loc, defender->loc) <= maxRange; + + // Is the path clear of obstacles and avoidances? + // traversiblePathBetween() includes monsterAvoids() + boolean pathClear = maxRange <= 0 || traversiblePathBetween(attacker, defender->loc.x, defender->loc.y); + + // Allow the player to make futile attacks, but monsters will think first + boolean futile = attackWouldBeFutile(attacker, defender); + + // All requirements must be met + return withinRange && pathClear && !futile; +} + +// Centralize the logic of whether an attacker can/will attack the defender +boolean ableAndWillingToAttack( + const creature *attacker, + const creature *defender, + short sensing, + int maxRange ) { + + if (attacker == NULL + || defender == NULL + || rogue.gameHasEnded) return false; + + // Must be enemies + boolean areEnemies = monsterIsWillingToAttackMonster(attacker, defender); + + // Must be able to attack the cell where the defender is located + boolean canAttackCell = monsterIsAbleToStrikeMonster(attacker, defender, maxRange); + + // Must be able to detect the defender + boolean locationKnown = monsterKnowsLocationOfMonster(attacker, defender, sensing); + + // All requirements must be met + return areEnemies && canAttackCell && locationKnown; +} + +// ------------------------------------------------------------------------ + + /// @brief Determines if a creature is willing to attack another. Considers factors like discord, /// entrancement, confusion, and whether they are enemies. Terrain and location are not considered, /// except for krakens and eels that attack anything in deep water. Used for player and monster attacks. @@ -349,15 +525,24 @@ boolean monsterWillAttackTarget(const creature *attacker, const creature *defend return false; } +static boolean isFollowing(const creature *source, const creature *target) { + return ((source->bookkeepingFlags & MB_FOLLOWER) && source->leader == target) + || (source->creatureState == MONSTER_ALLY && target == &player); +} + +static boolean sameLeader(const creature *a, const creature *b) { + return (a->creatureState == MONSTER_ALLY && b->creatureState == MONSTER_ALLY) + || (a->leader == b->leader + && (a->bookkeepingFlags & MB_FOLLOWER) + && (b->bookkeepingFlags & MB_FOLLOWER)); +} + boolean monstersAreTeammates(const creature *monst1, const creature *monst2) { // if one follows the other, or the other follows the one, or they both follow the same - return ((((monst1->bookkeepingFlags & MB_FOLLOWER) && monst1->leader == monst2) - || ((monst2->bookkeepingFlags & MB_FOLLOWER) && monst2->leader == monst1) - || (monst1->creatureState == MONSTER_ALLY && monst2 == &player) - || (monst1 == &player && monst2->creatureState == MONSTER_ALLY) - || (monst1->creatureState == MONSTER_ALLY && monst2->creatureState == MONSTER_ALLY) - || ((monst1->bookkeepingFlags & MB_FOLLOWER) && (monst2->bookkeepingFlags & MB_FOLLOWER) - && monst1->leader == monst2->leader)) ? true : false); + if (monst1 == NULL || monst2 == NULL) return false; + return sameLeader(monst1, monst2) + || isFollowing(monst1, monst2) + || isFollowing(monst2, monst1); } boolean monstersAreEnemies(const creature *monst1, const creature *monst2) { @@ -1980,7 +2165,7 @@ void decrementMonsterStatus(creature *monst) { } } -boolean traversiblePathBetween(creature *monst, short x2, short y2) { +boolean traversiblePathBetween(const creature *monst, short x2, short y2) { pos originLoc = monst->loc; pos targetLoc = (pos){ .x = x2, .y = y2 }; @@ -2035,7 +2220,7 @@ boolean openPathBetween(short x1, short y1, short x2, short y2) { // will return the player if the player is at (p.x, p.y). creature *monsterAtLoc(pos p) { - if (!(pmapAt(p)->flags & (HAS_MONSTER | HAS_PLAYER))) { + if (!(isPosInMap(p) && (pmapAt(p)->flags & (HAS_MONSTER | HAS_PLAYER)))) { return NULL; } if (posEq(player.loc, p)) { diff --git a/src/brogue/Movement.c b/src/brogue/Movement.c index 1e0ab700..aed0121f 100644 --- a/src/brogue/Movement.c +++ b/src/brogue/Movement.c @@ -656,14 +656,22 @@ boolean handleWhipAttacks(creature *attacker, enum directions dir, boolean *abor return false; } + // getImpactLoc won't be stopped by monsters the player can't see pos strikeLoc; getImpactLoc(&strikeLoc, originLoc, targetLoc, 5, false, &boltCatalog[BOLT_WHIP]); defender = monsterAtLoc(strikeLoc); - if (defender - && (attacker != &player || canSeeMonster(defender)) - && !monsterIsHidden(defender, attacker) - && monsterWillAttackTarget(attacker, defender)) { + + // If defender exists, it must be known by the player (from getImpactLoc) + // but there may be an unknown obstacle in between (eg. invis enemy) + // + // Here we check to see if the attacker wants to attack the defender + // range 0 to ignore defender's location (already checked in getImpactLoc) + // and whips can't strike underwater + // + // zap() does the hard work of actually casting the whip and deciding what happens + if (ableAndWillingToAttack(attacker, defender, notSeeInvis, 0) + && !(defender->bookkeepingFlags & MB_SUBMERGED)) { if (attacker == &player) { hitList[0] = defender; @@ -690,8 +698,8 @@ boolean handleWhipAttacks(creature *attacker, enum directions dir, boolean *abor // (in which case the player/monster should move instead). boolean handleSpearAttacks(creature *attacker, enum directions dir, boolean *aborted) { creature *defender, *hitList[8] = {0}; - short range = 2, i = 0, h = 0; - boolean proceed = false, visualEffect = false; + short range = 2, i = 0, hits = 0; + boolean visualEffect = false; const char boltChar[DIRECTION_COUNT] = "||--\\//\\"; @@ -725,21 +733,13 @@ boolean handleSpearAttacks(creature *attacker, enum directions dir, boolean *abo hitlist. Any of those that are either right by us or visible will trigger the attack. */ defender = monsterAtLoc(targetLoc); - if (defender - && (!cellHasTerrainFlag(targetLoc, T_OBSTRUCTS_PASSABILITY) - || (defender->info.flags & MONST_ATTACKABLE_THRU_WALLS)) - && monsterWillAttackTarget(attacker, defender)) { - - hitList[h++] = defender; - /* We check if i=0, i.e. the defender is right next to us, because - we have to do "normal" attacking here. We can't just return - false and leave to playerMoves/moveMonster due to the collateral hitlist. */ - if (i == 0 || !monsterIsHidden(defender, attacker) - && (attacker != &player || canSeeMonster(defender))) { - // We'll attack. - proceed = true; - } + // Passing range 0 here to ignore monster location (defined by outer loop instead) + // at i == 0 we check if ANY monster we are willing to attack exists (eg. we're bumping that location) + // at i == 1 we check if we KNOW of a monster we are willing to attack in that cell + // The attack proceeds if there are any monsters in the hit list + if (ableAndWillingToAttack(attacker, defender, i == 0, 0)) { + hitList[hits++] = defender; } if (cellHasTerrainFlag(targetLoc, (T_OBSTRUCTS_PASSABILITY | T_OBSTRUCTS_VISION))) { @@ -747,7 +747,7 @@ boolean handleSpearAttacks(creature *attacker, enum directions dir, boolean *abo } } range = i; - if (proceed) { + if (hits > 0) { if (attacker == &player) { if (abortAttack(hitList)) { if (aborted) { @@ -773,7 +773,7 @@ boolean handleSpearAttacks(creature *attacker, enum directions dir, boolean *abo attacker->bookkeepingFlags &= ~MB_SUBMERGED; // Artificially reverse the order of the attacks, // so that spears of force can send both monsters flying. - for (i = h - 1; i >= 0; i--) { + for (i = hits - 1; i >= 0; i--) { attack(attacker, hitList[i], false); } if (visualEffect) { @@ -794,25 +794,22 @@ boolean handleSpearAttacks(creature *attacker, enum directions dir, boolean *abo } static void buildFlailHitList(const short x, const short y, const short newX, const short newY, creature *hitList[16]) { - short mx, my; short i = 0; + while (hitList[i]) { + i++; + } for (creatureIterator it = iterateCreatures(monsters); hasNextCreature(it);) { creature *monst = nextCreature(&it); - mx = monst->loc.x; - my = monst->loc.y; - if (distanceBetween((pos){x, y}, (pos){mx, my}) == 1 - && distanceBetween((pos){newX, newY}, (pos){mx, my}) == 1 - && canSeeMonster(monst) - && monstersAreEnemies(&player, monst) - && monst->creatureState != MONSTER_ALLY - && !(monst->bookkeepingFlags & MB_IS_DYING) - && (!cellHasTerrainFlag(monst->loc, T_OBSTRUCTS_PASSABILITY) || (monst->info.flags & MONST_ATTACKABLE_THRU_WALLS))) { - while (hitList[i]) { - i++; - } - hitList[i] = monst; + // Difference in logic old vs new: + // - Original code does not check "willingness", only enemies + // - eg. flail won't hit allies when player is confused + if (distanceBetween((pos){x, y}, monst->loc) == 1 + && distanceBetween((pos){newX, newY}, monst->loc) == 1 + && ableAndWillingToAttack(&player, monst, notSeeInvis, 0)) { + + hitList[i++] = monst; } } } @@ -994,9 +991,9 @@ boolean playerMoves(short direction) { // Attack! for (i=0; i<16; i++) { if (hitList[i] - && monsterWillAttackTarget(&player, hitList[i]) - && !(hitList[i]->bookkeepingFlags & MB_IS_DYING) - && !rogue.gameHasEnded) { + // buildHitList does this too but it has to stay + // since attacking a creature might end the game + && !rogue.gameHasEnded) { if (attack(&player, hitList[i], false)) { anyAttackHit = true; @@ -1104,24 +1101,19 @@ boolean playerMoves(short direction) { } if (rogue.weapon && (rogue.weapon->flags & ITEM_LUNGE_ATTACKS)) { - newestX = player.loc.x + 2*nbDirs[direction][0]; - newestY = player.loc.y + 2*nbDirs[direction][1]; - if (coordinatesAreInMap(newestX, newestY) && (pmap[newestX][newestY].flags & HAS_MONSTER)) { - tempMonst = monsterAtLoc((pos){ newestX, newestY }); - if (tempMonst - && (canSeeMonster(tempMonst) || monsterRevealed(tempMonst)) - && monstersAreEnemies(&player, tempMonst) - && tempMonst->creatureState != MONSTER_ALLY - && !(tempMonst->bookkeepingFlags & MB_IS_DYING) - && (!cellHasTerrainFlag(tempMonst->loc, T_OBSTRUCTS_PASSABILITY) || (tempMonst->info.flags & MONST_ATTACKABLE_THRU_WALLS))) { - - hitList[0] = tempMonst; - if (abortAttack(hitList)) { - brogueAssert(!committed); - cancelKeystroke(); - rogue.disturbed = true; - return false; - } + const pos p = posNeighborInDirection( posNeighborInDirection(player.loc, direction), direction ); + + // Difference in logic old vs new: + // - Original code does not check "willingness", only enemies + // - eg. rapier won't hit allies when player is confused + tempMonst = monsterAtLoc(p); + if (ableAndWillingToAttack(&player, tempMonst, notSeeInvis, 0)) { + hitList[0] = tempMonst; + if (abortAttack(hitList)) { + brogueAssert(!committed); + cancelKeystroke(); + rogue.disturbed = true; + return false; } } } diff --git a/src/brogue/Rogue.h b/src/brogue/Rogue.h index a3fc0e63..fb0357cc 100644 --- a/src/brogue/Rogue.h +++ b/src/brogue/Rogue.h @@ -3142,7 +3142,7 @@ extern "C" { void decrementMonsterStatus(creature *monst); boolean specifiedPathBetween(short x1, short y1, short x2, short y2, unsigned long blockingTerrain, unsigned long blockingFlags); - boolean traversiblePathBetween(creature *monst, short x2, short y2); + boolean traversiblePathBetween(const creature *monst, short x2, short y2); boolean openPathBetween(short x1, short y1, short x2, short y2); creature *monsterAtLoc(pos p); creature *dormantMonsterAtLoc(pos p); @@ -3174,6 +3174,19 @@ extern "C" { short distanceBetween(pos loc1, pos loc2); void alertMonster(creature *monst); void wakeUp(creature *monst); + + + // Convenience values for passing to the following functions + const short notSeeInvis; + const short yesSeeInvis; + const short isBumping; + boolean monsterIsAggressiveToMonster(const creature *attacker, const creature *defender); + boolean monsterIsWillingToAttackMonster(const creature *attacker, const creature *defender); + boolean monsterIsAbleToStrikeMonster(const creature *attacker, const creature *defender, int maxRange); + boolean monsterKnowsLocationOfMonster(const creature *source, const creature *target, short sensing ); + boolean ableAndWillingToAttack( const creature *attacker, const creature *defender, short sensing, int maxRange ); + + boolean monsterRevealed(creature *monst); boolean monsterHiddenBySubmersion(const creature *monst, const creature *observer); boolean monsterIsHidden(const creature *monst, const creature *observer);