Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 29 additions & 32 deletions src/brogue/Combat.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
7 changes: 5 additions & 2 deletions src/brogue/Items.c
Original file line number Diff line number Diff line change
Expand Up @@ -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; i<n; i++) {
monst = monsterAtLoc(coords[i]);
if (monst
&& !monsterIsHidden(monst, monsterAtLoc(originLoc))

// Don't allow zapping (or whipping) submerged monsters
if (monsterKnowsLocationOfMonster(caster, monst, notSeeInvis)
&& !(monst->bookkeepingFlags & MB_SUBMERGED)) {

// Imaginary bolt hit the player or a monster.
break;
}
Expand Down
203 changes: 194 additions & 9 deletions src/brogue/Monsters.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 };

Expand Down Expand Up @@ -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)) {
Expand Down
Loading