/*
* Movement.c
* Brogue
*
* Created by Brian Walker on 1/10/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"
void playerRuns(short direction) {
short newX, newY, dir;
boolean cardinalPassability[4];
rogue.disturbed = (player.status[STATUS_CONFUSED] ? true : false);
for (dir = 0; dir < 4; dir++) {
newX = player.xLoc + nbDirs[dir][0];
newY = player.yLoc + nbDirs[dir][1];
cardinalPassability[dir] = monsterAvoids(&player, newX, newY);
}
while (!rogue.disturbed) {
if (!playerMoves(direction)) {
rogue.disturbed = true;
break;
}
newX = player.xLoc + nbDirs[direction][0];
newY = player.yLoc + nbDirs[direction][1];
if (!coordinatesAreInMap(newX, newY)
|| monsterAvoids(&player, newX, newY)) {
rogue.disturbed = true;
}
if (isDisturbed(player.xLoc, player.yLoc)) {
rogue.disturbed = true;
} else if (direction < 4) {
for (dir = 0; dir < 4; dir++) {
newX = player.xLoc + nbDirs[dir][0];
newY = player.yLoc + nbDirs[dir][1];
if (cardinalPassability[dir] != monsterAvoids(&player, newX, newY)
&& !(nbDirs[dir][0] + nbDirs[direction][0] == 0 &&
nbDirs[dir][1] + nbDirs[direction][1] == 0)) {
// dir is not the x-opposite or y-opposite of direction
rogue.disturbed = true;
}
}
}
}
updateFlavorText();
}
enum dungeonLayers highestPriorityLayer(short x, short y, boolean skipGas) {
short bestPriority = 10000;
enum dungeonLayers tt, best = 0;
for (tt = 0; tt < NUMBER_TERRAIN_LAYERS; tt++) {
if (tt == GAS && skipGas) {
continue;
}
if (pmap[x][y].layers[tt] && tileCatalog[pmap[x][y].layers[tt]].drawPriority < bestPriority) {
bestPriority = tileCatalog[pmap[x][y].layers[tt]].drawPriority;
best = tt;
}
}
return best;
}
enum dungeonLayers layerWithTMFlag(short x, short y, unsigned long flag) {
enum dungeonLayers layer;
for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
if (tileCatalog[pmap[x][y].layers[layer]].mechFlags & flag) {
return layer;
}
}
return NO_LAYER;
}
enum dungeonLayers layerWithFlag(short x, short y, unsigned long flag) {
enum dungeonLayers layer;
for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
if (tileCatalog[pmap[x][y].layers[layer]].flags & flag) {
return layer;
}
}
return NO_LAYER;
}
// Retrieves a pointer to the flavor text of the highest-priority terrain at the given location
char *tileFlavor(short x, short y) {
return tileCatalog[pmap[x][y].layers[highestPriorityLayer(x, y, false)]].flavorText;
}
// Retrieves a pointer to the description text of the highest-priority terrain at the given location
char *tileText(short x, short y) {
return tileCatalog[pmap[x][y].layers[highestPriorityLayer(x, y, false)]].description;
}
void describedItemBasedOnParameters(short theCategory, short theKind, short theQuantity, short theOriginDepth, char *buf) {
item *tempItem = initializeItem();
tempItem->category = theCategory;
tempItem->kind = theKind;
tempItem->quantity = theQuantity;
tempItem->originDepth = theOriginDepth;
itemName(tempItem, buf, false, true, NULL);
free(tempItem);
return;
}
// Describes the item in question either by naming it if the player has already seen its name,
// or by tersely identifying its category otherwise.
void describedItemName(item *theItem, char *buf) {
if (rogue.playbackOmniscience || (!player.status[STATUS_HALLUCINATING])) {
itemName(theItem, buf, (theItem->category & (WEAPON | ARMOR) ? false : true), true, NULL);
} else {
describeHallucinatedItem(buf);
}
}
void describeLocation(char *buf, short x, short y) {
creature *monst;
item *theItem, *magicItem;
boolean standsInTerrain;
boolean subjectMoving;
boolean prepositionLocked = false;
boolean monsterDormant;
char subject[COLS * 3];
char verb[COLS * 3];
char preposition[COLS * 3];
char object[COLS * 3];
char adjective[COLS * 3];
assureCosmeticRNG;
if (x == player.xLoc && y == player.yLoc) {
if (player.status[STATUS_LEVITATING]) {
sprintf(buf, "you are hovering above %s.", tileText(x, y));
} else {
strcpy(buf, tileFlavor(x, y));
}
restoreRNG;
return;
}
monst = NULL;
standsInTerrain = ((tileCatalog[pmap[x][y].layers[highestPriorityLayer(x, y, false)]].mechFlags & TM_STAND_IN_TILE) ? true : false);
theItem = itemAtLoc(x, y);
monsterDormant = false;
if (pmap[x][y].flags & HAS_MONSTER) {
monst = monsterAtLoc(x, y);
} else if (pmap[x][y].flags & HAS_DORMANT_MONSTER) {
monst = dormantMonsterAtLoc(x, y);
monsterDormant = true;
}
// detecting magical items
magicItem = NULL;
if (theItem && !playerCanSeeOrSense(x, y)
&& (theItem->flags & ITEM_MAGIC_DETECTED)
&& itemMagicPolarity(theItem)) {
magicItem = theItem;
} else if (monst && !canSeeMonster(monst)
&& monst->carriedItem
&& (monst->carriedItem->flags & ITEM_MAGIC_DETECTED)
&& itemMagicPolarity(monst->carriedItem)) {
magicItem = monst->carriedItem;
}
if (magicItem && !(pmap[x][y].flags & DISCOVERED)) {
switch (itemMagicPolarity(magicItem)) {
case 1:
strcpy(object, magicItem->category == AMULET ? "the Amulet of Yendor" : "benevolent magic");
break;
case -1:
strcpy(object, "malevolent magic");
break;
default:
strcpy(object, "mysterious magic");
break;
}
sprintf(buf, "you can detect the aura of %s here.", object);
restoreRNG;
return;
}
// telepathy
if (monst
&& !canSeeMonster(monst)
&& monsterRevealed(monst)) {
strcpy(adjective, (((!player.status[STATUS_HALLUCINATING] || rogue.playbackOmniscience) && !monst->info.isLarge)
|| (player.status[STATUS_HALLUCINATING] && !rogue.playbackOmniscience && rand_range(0, 1)) ? "small" : "large"));
if (pmap[x][y].flags & DISCOVERED) {
strcpy(object, tileText(x, y));
if (monst->bookkeepingFlags & MB_SUBMERGED) {
strcpy(preposition, "under ");
} else if (monsterDormant) {
strcpy(preposition, "coming from within ");
} else if (standsInTerrain) {
strcpy(preposition, "in ");
} else {
strcpy(preposition, "over ");
}
} else {
strcpy(object, "here");
strcpy(preposition, "");
}
sprintf(buf, "you can sense a %s psychic emanation %s%s.", adjective, preposition, object);
restoreRNG;
return;
}
if (monst && !canSeeMonster(monst) && !rogue.playbackOmniscience) {
// Monster is not visible.
monst = NULL;
}
if (!playerCanSeeOrSense(x, y)) {
if (pmap[x][y].flags & DISCOVERED) { // memory
if (pmap[x][y].rememberedItemCategory) {
if (player.status[STATUS_HALLUCINATING] && !rogue.playbackOmniscience) {
describeHallucinatedItem(object);
} else {
describedItemBasedOnParameters(pmap[x][y].rememberedItemCategory, pmap[x][y].rememberedItemKind,
pmap[x][y].rememberedItemQuantity, pmap[x][y].rememberedItemOriginDepth, object);
}
} else {
strcpy(object, tileCatalog[pmap[x][y].rememberedTerrain].description);
}
sprintf(buf, "you remember seeing %s here.", object);
restoreRNG;
return;
} else if (pmap[x][y].flags & MAGIC_MAPPED) { // magic mapped
sprintf(buf, "you expect %s to be here.", tileCatalog[pmap[x][y].rememberedTerrain].description);
restoreRNG;
return;
}
strcpy(buf, "");
restoreRNG;
return;
}
if (monst) {
monsterName(subject, monst, true);
if (pmap[x][y].layers[GAS] && monst->status[STATUS_INVISIBLE]) { // phantoms in gas
sprintf(buf, "you can perceive the faint outline of %s in %s.", subject, tileCatalog[pmap[x][y].layers[GAS]].description);
restoreRNG;
return;
}
subjectMoving = (monst->turnsSpentStationary == 0
&& !(monst->info.flags & (MONST_GETS_TURN_ON_ACTIVATION | MONST_IMMOBILE))
&& monst->creatureState != MONSTER_SLEEPING
&& !(monst->bookkeepingFlags & (MB_SEIZED | MB_CAPTIVE)));
if ((monst->info.flags & MONST_ATTACKABLE_THRU_WALLS)
&& cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY)) {
strcpy(verb, "is embedded");
} else if (cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY)) {
strcpy(verb, "is trapped");
subjectMoving = false;
} else if (monst->bookkeepingFlags & MB_CAPTIVE) {
strcpy(verb, "is shackled in place");
subjectMoving = false;
} else if (monst->status[STATUS_PARALYZED]) {
strcpy(verb, "is frozen in place");
subjectMoving = false;
} else if (monst->status[STATUS_STUCK]) {
strcpy(verb, "is entangled");
subjectMoving = false;
} else if (monst->status[STATUS_LEVITATING]) {
strcpy(verb, (subjectMoving ? "is flying" : "is hovering"));
strcpy(preposition, "over");
prepositionLocked = true;
} else if (monsterCanSubmergeNow(monst)) {
strcpy(verb, (subjectMoving ? "is gliding" : "is drifting"));
} else if (cellHasTerrainFlag(x, y, T_MOVES_ITEMS) && !(monst->info.flags & MONST_SUBMERGES)) {
strcpy(verb, (subjectMoving ? "is swimming" : "is struggling"));
} else if (cellHasTerrainFlag(x, y, T_AUTO_DESCENT)) {
strcpy(verb, "is suspended in mid-air");
strcpy(preposition, "over");
prepositionLocked = true;
subjectMoving = false;
} else if (monst->status[STATUS_CONFUSED]) {
strcpy(verb, "is staggering");
} else if ((monst->info.flags & MONST_RESTRICTED_TO_LIQUID)
&& !cellHasTMFlag(monst->xLoc, monst->yLoc, TM_ALLOWS_SUBMERGING)) {
strcpy(verb, "is lying");
subjectMoving = false;
} else if (monst->info.flags & MONST_IMMOBILE) {
strcpy(verb, "is resting");
} else {
switch (monst->creatureState) {
case MONSTER_SLEEPING:
strcpy(verb, "is sleeping");
subjectMoving = false;
break;
case MONSTER_WANDERING:
strcpy(verb, subjectMoving ? "is wandering" : "is standing");
break;
case MONSTER_FLEEING:
strcpy(verb, subjectMoving ? "is fleeing" : "is standing");
break;
case MONSTER_TRACKING_SCENT:
strcpy(verb, subjectMoving ? "is charging" : "is standing");
break;
case MONSTER_ALLY:
strcpy(verb, subjectMoving ? "is following you" : "is standing");
break;
default:
strcpy(verb, "is standing");
break;
}
}
if (monst->status[STATUS_BURNING] && !(monst->info.flags & MONST_FIERY)) {
strcat(verb, ", burning,");
}
if (theItem) {
strcpy(preposition, "over");
describedItemName(theItem, object);
} else {
if (!prepositionLocked) {
strcpy(preposition, subjectMoving ? (standsInTerrain ? "through" : "across")
: (standsInTerrain ? "in" : "on"));
}
strcpy(object, tileText(x, y));
}
} else { // no monster
strcpy(object, tileText(x, y));
if (theItem) {
describedItemName(theItem, subject);
subjectMoving = cellHasTerrainFlag(x, y, T_MOVES_ITEMS);
if (player.status[STATUS_HALLUCINATING] && !rogue.playbackOmniscience) {
strcpy(verb, "is");
} else {
strcpy(verb, (theItem->quantity > 1 || (theItem->category & GOLD)) ? "are" : "is");
}
if (cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY)) {
strcat(verb, " enclosed");
} else {
strcat(verb, subjectMoving ? " drifting" : " lying");
}
strcpy(preposition, standsInTerrain ? (subjectMoving ? "through" : "in")
: (subjectMoving ? "across" : "on"));
} else { // no item
sprintf(buf, "you %s %s.", (playerCanDirectlySee(x, y) ? "see" : "sense"), object);
restoreRNG;
return;
}
}
sprintf(buf, "%s %s %s %s.", subject, verb, preposition, object);
restoreRNG;
}
void printLocationDescription(short x, short y) {
char buf[DCOLS*3];
describeLocation(buf, x, y);
flavorMessage(buf);
}
void useKeyAt(item *theItem, short x, short y) {
short layer, i;
creature *monst;
char buf[COLS], buf2[COLS], terrainName[COLS], preposition[10];
boolean disposable;
strcpy(terrainName, "unknown terrain"); // redundant failsafe
for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
if (tileCatalog[pmap[x][y].layers[layer]].mechFlags & TM_PROMOTES_WITH_KEY) {
if (tileCatalog[pmap[x][y].layers[layer]].description[0] == 'a'
&& tileCatalog[pmap[x][y].layers[layer]].description[1] == ' ') {
sprintf(terrainName, "the %s", &(tileCatalog[pmap[x][y].layers[layer]].description[2]));
} else {
strcpy(terrainName, tileCatalog[pmap[x][y].layers[layer]].description);
}
if (tileCatalog[pmap[x][y].layers[layer]].mechFlags & TM_STAND_IN_TILE) {
strcpy(preposition, "in");
} else {
strcpy(preposition, "on");
}
promoteTile(x, y, layer, false);
}
}
disposable = false;
for (i=0; i < KEY_ID_MAXIMUM && (theItem->keyLoc[i].x || theItem->keyLoc[i].machine); i++) {
if (theItem->keyLoc[i].x == x && theItem->keyLoc[i].y == y && theItem->keyLoc[i].disposableHere) {
disposable = true;
} else if (theItem->keyLoc[i].machine == pmap[x][y].machineNumber && theItem->keyLoc[i].disposableHere) {
disposable = true;
}
}
if (disposable) {
if (removeItemFromChain(theItem, packItems)) {
itemName(theItem, buf2, true, false, NULL);
sprintf(buf, "you use your %s %s %s.",
buf2,
preposition,
terrainName);
messageWithColor(buf, &itemMessageColor, false);
deleteItem(theItem);
} else if (removeItemFromChain(theItem, floorItems)) {
deleteItem(theItem);
pmap[x][y].flags &= ~HAS_ITEM;
} else if (pmap[x][y].flags & HAS_MONSTER) {
monst = monsterAtLoc(x, y);
if (monst->carriedItem && monst->carriedItem == theItem) {
monst->carriedItem = NULL;
deleteItem(theItem);
}
}
}
}
short randValidDirectionFrom(creature *monst, short x, short y, boolean respectAvoidancePreferences) {
short i, newX, newY, validDirections[8], count = 0;
brogueAssert(rogue.RNG == RNG_SUBSTANTIVE);
for (i=0; i<8; i++) {
newX = x + nbDirs[i][0];
newY = y + nbDirs[i][1];
if (coordinatesAreInMap(newX, newY)
&& !cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_PASSABILITY)
&& !diagonalBlocked(x, y, newX, newY, false)
&& (!respectAvoidancePreferences
|| (!monsterAvoids(monst, newX, newY))
|| ((pmap[newX][newY].flags & HAS_PLAYER) && monst->creatureState != MONSTER_ALLY))) {
validDirections[count++] = i;
}
}
if (count == 0) {
// Rare, and important in this case that the function returns BEFORE a random roll is made to avoid OOS.
return NO_DIRECTION;
}
return validDirections[rand_range(0, count - 1)];
}
void vomit(creature *monst) {
char buf[COLS], monstName[COLS];
spawnDungeonFeature(monst->xLoc, monst->yLoc, &dungeonFeatureCatalog[DF_VOMIT], true, false);
if (canDirectlySeeMonster(monst)
&& !rogue.automationActive) {
monsterName(monstName, monst, true);
sprintf(buf, "%s vomit%s profusely", monstName, (monst == &player ? "" : "s"));
combatMessage(buf, NULL);
}
}
void moveEntrancedMonsters(enum directions dir) {
creature *monst, *nextMonst;
dir = oppositeDirection(dir);
if (rogue.patchVersion >= 3) {
for (monst = monsters->nextCreature; monst != NULL; monst = monst->nextCreature) {
monst->bookkeepingFlags &= ~MB_HAS_ENTRANCED_MOVED;
}
for (monst = monsters->nextCreature; monst != NULL; monst = monst->nextCreature) {
if (!(monst->bookkeepingFlags & MB_HAS_ENTRANCED_MOVED)
&& monst->status[STATUS_ENTRANCED]
&& !monst->status[STATUS_STUCK]
&& !monst->status[STATUS_PARALYZED]
&& !(monst->bookkeepingFlags & MB_CAPTIVE)) {
moveMonster(monst, nbDirs[dir][0], nbDirs[dir][1]);
monst->bookkeepingFlags |= MB_HAS_ENTRANCED_MOVED;
monst = monsters; // loop through from the beginning to be safe
}
}
} else {
for (monst = monsters->nextCreature; monst != NULL; monst = nextMonst) {
nextMonst = monst->nextCreature;
if (monst->status[STATUS_ENTRANCED]
&& !monst->status[STATUS_STUCK]
&& !monst->status[STATUS_PARALYZED]
&& !(monst->bookkeepingFlags & MB_CAPTIVE)) {
moveMonster(monst, nbDirs[dir][0], nbDirs[dir][1]);
}
}
}
}
void becomeAllyWith(creature *monst) {
demoteMonsterFromLeadership(monst);
// Drop your item.
if (monst->carriedItem) {
makeMonsterDropItem(monst);
}
// If you're going to change into something, it should be friendly.
if (monst->carriedMonster) {
becomeAllyWith(monst->carriedMonster);
}
monst->creatureState = MONSTER_ALLY;
monst->bookkeepingFlags |= MB_FOLLOWER;
monst->leader = &player;
monst->bookkeepingFlags &= ~(MB_CAPTIVE | MB_SEIZED);
refreshDungeonCell(monst->xLoc, monst->yLoc);
}
void freeCaptive(creature *monst) {
char buf[COLS * 3], monstName[COLS];
becomeAllyWith(monst);
monsterName(monstName, monst, false);
sprintf(buf, "you free the grateful %s and gain a faithful ally.", monstName);
message(buf, false);
}
boolean freeCaptivesEmbeddedAt(short x, short y) {
creature *monst;
if (pmap[x][y].flags & HAS_MONSTER) {
// Free any captives trapped in the tunnelized terrain.
monst = monsterAtLoc(x, y);
if ((monst->bookkeepingFlags & MB_CAPTIVE)
&& !(monst->info.flags & MONST_ATTACKABLE_THRU_WALLS)
&& (cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY))) {
freeCaptive(monst);
return true;
}
}
return false;
}
// Do we need confirmation so we don't accidently hit an acid mound?
boolean abortAttackAgainstAcidicTarget(creature *hitList[8]) {
short i;
char monstName[COLS], weaponName[COLS];
char buf[COLS*3];
if (rogue.weapon
&& !(rogue.weapon->flags & ITEM_PROTECTED)
&& !player.status[STATUS_HALLUCINATING]
&& !player.status[STATUS_CONFUSED]) {
for (i=0; i<8; i++) {
if (hitList[i]
&& (hitList[i]->info.flags & MONST_DEFEND_DEGRADE_WEAPON)
&& canSeeMonster(hitList[i])
&& (!(rogue.weapon->flags & ITEM_RUNIC)
|| !(rogue.weapon->flags & ITEM_RUNIC_IDENTIFIED)
|| rogue.weapon->enchant2 != W_SLAYING
|| !monsterIsInClass(hitList[i], rogue.weapon->vorpalEnemy))) {
monsterName(monstName, hitList[i], true);
itemName(rogue.weapon, weaponName, false, false, NULL);
sprintf(buf, "Degrade your %s by attacking %s?", weaponName, monstName);
if (confirm(buf, false)) {
return false; // Fire when ready!
} else {
return true; // Abort!
}
}
}
}
return false;
}
// Returns true if a whip attack was launched.
// If "aborted" pointer is provided, sets it to true if it was aborted because
// the player opted not to attack an acid mound (in which case the whole turn
// should be aborted), as opposed to there being no valid whip attack available
// (in which case the player/monster should move instead).
boolean handleWhipAttacks(creature *attacker, enum directions dir, boolean *aborted) {
bolt theBolt;
creature *defender, *hitList[8] = {0};
short strikeLoc[2], originLoc[2], targetLoc[2];
const char boltChar[DIRECTION_COUNT] = "||~~\\//\\";
brogueAssert(dir > NO_DIRECTION && dir < DIRECTION_COUNT);
if (attacker == &player) {
if (!rogue.weapon || !(rogue.weapon->flags & ITEM_ATTACKS_EXTEND)) {
return false;
}
} else if (!(attacker->info.abilityFlags & MA_ATTACKS_EXTEND)) {
return false;
}
originLoc[0] = attacker->xLoc;
originLoc[1] = attacker->yLoc;
targetLoc[0] = attacker->xLoc + nbDirs[dir][0];
targetLoc[1] = attacker->yLoc + nbDirs[dir][1];
getImpactLoc(strikeLoc, originLoc, targetLoc, 5, false);
defender = monsterAtLoc(strikeLoc[0], strikeLoc[1]);
if (defender
&& (attacker != &player || canSeeMonster(defender))
&& !monsterIsHidden(defender, attacker)
&& monsterWillAttackTarget(attacker, defender)) {
if (attacker == &player) {
hitList[0] = defender;
if (abortAttackAgainstAcidicTarget(hitList)) {
if (aborted) {
*aborted = true;
}
return false;
}
}
attacker->bookkeepingFlags &= ~MB_SUBMERGED;
theBolt = boltCatalog[BOLT_WHIP];
theBolt.theChar = boltChar[dir];
zap(originLoc, targetLoc, &theBolt, false);
return true;
}
return false;
}
// Returns true if a spear attack was launched.
// If "aborted" pointer is provided, sets it to true if it was aborted because
// the player opted not to attack an acid mound (in which case the whole turn
// should be aborted), as opposed to there being no valid spear attack available
// (in which case the player/monster should move instead).
boolean handleSpearAttacks(creature *attacker, enum directions dir, boolean *aborted) {
creature *defender, *hitList[8] = {0};
short targetLoc[2], range = 2, i = 0, h = 0;
boolean proceed = false, visualEffect = false;
const char boltChar[DIRECTION_COUNT] = "||--\\//\\";
brogueAssert(dir > NO_DIRECTION && dir < DIRECTION_COUNT);
if (attacker == &player) {
if (!rogue.weapon || !(rogue.weapon->flags & ITEM_ATTACKS_PENETRATE)) {
return false;
}
} else if (!(attacker->info.abilityFlags & MA_ATTACKS_PENETRATE)) {
return false;
}
for (i = 0; i < range; i++) {
targetLoc[0] = attacker->xLoc + (1 + i) * nbDirs[dir][0];
targetLoc[1] = attacker->yLoc + (1 + i) * nbDirs[dir][1];
if (!coordinatesAreInMap(targetLoc[0], targetLoc[1])) {
break;
}
/* Add creatures that we are willing to attack to the potential
hitlist. Any of those that are either right by us or visible will
trigger the attack. */
defender = monsterAtLoc(targetLoc[0], targetLoc[1]);
if (defender
&& (!cellHasTerrainFlag(targetLoc[0], targetLoc[1], 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;
}
}
if (cellHasTerrainFlag(targetLoc[0], targetLoc[1], (T_OBSTRUCTS_PASSABILITY | T_OBSTRUCTS_VISION))) {
break;
}
}
range = i;
if (proceed) {
if (attacker == &player) {
if (abortAttackAgainstAcidicTarget(hitList)) {
if (aborted) {
*aborted = true;
}
return false;
}
}
if (!rogue.playbackFastForward) {
for (i = 0; i < range; i++) {
targetLoc[0] = attacker->xLoc + (1 + i) * nbDirs[dir][0];
targetLoc[1] = attacker->yLoc + (1 + i) * nbDirs[dir][1];
if (coordinatesAreInMap(targetLoc[0], targetLoc[1])
&& playerCanSeeOrSense(targetLoc[0], targetLoc[1])) {
visualEffect = true;
plotForegroundChar(boltChar[dir], targetLoc[0], targetLoc[1], &lightBlue, true);
}
}
}
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--) {
attack(attacker, hitList[i], false);
}
if (visualEffect) {
pauseBrogue(16);
for (i = 0; i < range; i++) {
targetLoc[0] = attacker->xLoc + (1 + i) * nbDirs[dir][0];
targetLoc[1] = attacker->yLoc + (1 + i) * nbDirs[dir][1];
if (coordinatesAreInMap(targetLoc[0], targetLoc[1])) {
refreshDungeonCell(targetLoc[0], targetLoc[1]);
}
}
}
return true;
}
return false;
}
void buildFlailHitList(const short x, const short y, const short newX, const short newY, creature *hitList[16]) {
creature *monst;
short mx, my;
short i = 0;
for (monst = monsters->nextCreature; monst != NULL; monst = monst->nextCreature) {
mx = monst->xLoc;
my = monst->yLoc;
if (distanceBetween(x, y, mx, my) == 1
&& distanceBetween(newX, newY, mx, my) == 1
&& canSeeMonster(monst)
&& monstersAreEnemies(&player, monst)
&& monst->creatureState != MONSTER_ALLY
&& !(monst->bookkeepingFlags & MB_IS_DYING)
&& (!cellHasTerrainFlag(monst->xLoc, monst->yLoc, T_OBSTRUCTS_PASSABILITY) || (monst->info.flags & MONST_ATTACKABLE_THRU_WALLS))) {
while (hitList[i]) {
i++;
}
hitList[i] = monst;
}
}
}
boolean diagonalBlocked(const short x1, const short y1, const short x2, const short y2, const boolean limitToPlayerKnowledge) {
unsigned long tFlags;
if (x1 == x2 || y1 == y2) {
return false; // If it's not a diagonal, it's not diagonally blocked.
}
getLocationFlags(x1, y2, &tFlags, NULL, NULL, limitToPlayerKnowledge);
if (tFlags & T_OBSTRUCTS_DIAGONAL_MOVEMENT) {
return true;
}
getLocationFlags(x2, y1, &tFlags, NULL, NULL, limitToPlayerKnowledge);
if (tFlags & T_OBSTRUCTS_DIAGONAL_MOVEMENT) {
return true;
}
return false;
}
// Called whenever the player voluntarily tries to move in a given direction.
// Can be called from movement keys, exploration, or auto-travel.
boolean playerMoves(short direction) {
short initialDirection = direction, i, layer;
short x = player.xLoc, y = player.yLoc;
short newX, newY, newestX, newestY;
boolean playerMoved = false, alreadyRecorded = false, specialAttackAborted = false, anyAttackHit = false;
creature *defender = NULL, *tempMonst = NULL, *hitList[16] = {NULL};
char monstName[COLS];
char buf[COLS*3];
const int directionKeys[8] = {UP_KEY, DOWN_KEY, LEFT_KEY, RIGHT_KEY, UPLEFT_KEY, DOWNLEFT_KEY, UPRIGHT_KEY, DOWNRIGHT_KEY};
brogueAssert(direction >= 0 && direction < DIRECTION_COUNT);
newX = x + nbDirs[direction][0];
newY = y + nbDirs[direction][1];
if (!coordinatesAreInMap(newX, newY)) {
return false;
}
if (player.status[STATUS_CONFUSED]) {
// Confirmation dialog if you're moving while confused and you're next to lava and not levitating or immune to fire.
if (player.status[STATUS_LEVITATING] <= 1
&& player.status[STATUS_IMMUNE_TO_FIRE] <= 1) {
for (i=0; i<8; i++) {
newestX = x + nbDirs[i][0];
newestY = y + nbDirs[i][1];
if (coordinatesAreInMap(newestX, newestY)
&& (pmap[newestX][newestY].flags & (DISCOVERED | MAGIC_MAPPED))
&& !diagonalBlocked(x, y, newestX, newestY, false)
&& cellHasTerrainFlag(newestX, newestY, T_LAVA_INSTA_DEATH)
&& !cellHasTerrainFlag(newestX, newestY, T_OBSTRUCTS_PASSABILITY | T_ENTANGLES)
&& !((pmap[newestX][newestY].flags & HAS_MONSTER)
&& canSeeMonster(monsterAtLoc(newestX, newestY))
&& monsterAtLoc(newestX, newestY)->creatureState != MONSTER_ALLY)) {
if (!confirm("Risk stumbling into lava?", false)) {
return false;
} else {
break;
}
}
}
}
direction = randValidDirectionFrom(&player, x, y, false);
if (direction == -1) {
return false;
} else {
newX = x + nbDirs[direction][0];
newY = y + nbDirs[direction][1];
if (!coordinatesAreInMap(newX, newY)) {
return false;
}
if (!alreadyRecorded) {
recordKeystroke(directionKeys[initialDirection], false, false);
alreadyRecorded = true;
}
}
}
if (pmap[newX][newY].flags & HAS_MONSTER) {
defender = monsterAtLoc(newX, newY);
}
// If there's no enemy at the movement location that the player is aware of, consider terrain promotions.
if (!defender
|| (!canSeeMonster(defender) && !monsterRevealed(defender))
|| !monstersAreEnemies(&player, defender)) {
if (cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_PASSABILITY) && cellHasTMFlag(newX, newY, TM_PROMOTES_ON_PLAYER_ENTRY)) {
layer = layerWithTMFlag(newX, newY, TM_PROMOTES_ON_PLAYER_ENTRY);
if (tileCatalog[pmap[newX][newY].layers[layer]].flags & T_OBSTRUCTS_PASSABILITY) {
if (!alreadyRecorded) {
recordKeystroke(directionKeys[initialDirection], false, false);
alreadyRecorded = true;
}
message(tileCatalog[pmap[newX][newY].layers[layer]].flavorText, false);
promoteTile(newX, newY, layer, false);
playerTurnEnded();
return true;
}
}
if (rogue.patchVersion < 1 && player.status[STATUS_STUCK] && cellHasTerrainFlag(x, y, T_ENTANGLES)) {
// Don't interrupt exploration with this message.
if (--player.status[STATUS_STUCK]) {
if (!rogue.automationActive) {
message("you struggle but cannot free yourself.", false);
}
} else {
if (!rogue.automationActive) {
message("you break free!", false);
}
if (tileCatalog[pmap[x][y].layers[SURFACE]].flags & T_ENTANGLES) {
pmap[x][y].layers[SURFACE] = NOTHING;
}
}
moveEntrancedMonsters(direction);
if (!alreadyRecorded) {
recordKeystroke(directionKeys[initialDirection], false, false);
alreadyRecorded = true;
}
if (player.status[STATUS_STUCK]) {
playerTurnEnded();
return true;
}
}
}
if (((!cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_PASSABILITY) || (cellHasTMFlag(newX, newY, TM_PROMOTES_WITH_KEY) && keyInPackFor(newX, newY)))
&& !diagonalBlocked(x, y, newX, newY, false)
&& (!cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY) || (cellHasTMFlag(x, y, TM_PROMOTES_WITH_KEY) && keyInPackFor(x, y))))
|| (defender && defender->info.flags & MONST_ATTACKABLE_THRU_WALLS)) {
// if the move is not blocked
if (handleWhipAttacks(&player, direction, &specialAttackAborted)
|| handleSpearAttacks(&player, direction, &specialAttackAborted)) {
if (!alreadyRecorded) {
recordKeystroke(directionKeys[initialDirection], false, false);
alreadyRecorded = true;
}
playerRecoversFromAttacking(true);
moveEntrancedMonsters(direction);
playerTurnEnded();
return true;
} else if (specialAttackAborted) { // Canceled an attack against an acid mound.
brogueAssert(!alreadyRecorded);
rogue.disturbed = true;
return false;
}
if (defender) {
// if there is a monster there
if (defender->bookkeepingFlags & MB_CAPTIVE) {
monsterName(monstName, defender, false);
sprintf(buf, "Free the captive %s?", monstName);
if (alreadyRecorded || confirm(buf, false)) {
if (!alreadyRecorded) {
recordKeystroke(directionKeys[initialDirection], false, false);
alreadyRecorded = true;
}
if (cellHasTMFlag(newX, newY, TM_PROMOTES_WITH_KEY) && keyInPackFor(newX, newY)) {
useKeyAt(keyInPackFor(newX, newY), newX, newY);
}
freeCaptive(defender);
player.ticksUntilTurn += player.attackSpeed;
playerTurnEnded();
return true;
} else {
return false;
}
}
if (defender->creatureState != MONSTER_ALLY) {
// Make a hit list of monsters the player is attacking this turn.
// We separate this tallying phase from the actual attacking phase because sometimes the attacks themselves
// create more monsters, and those shouldn't be attacked in the same turn.
buildHitList(hitList, &player, defender,
rogue.weapon && (rogue.weapon->flags & ITEM_ATTACKS_ALL_ADJACENT));
if (abortAttackAgainstAcidicTarget(hitList)) { // Acid mound attack confirmation.
brogueAssert(!alreadyRecorded);
rogue.disturbed = true;
return false;
}
if (player.status[STATUS_NAUSEOUS]) {
if (!alreadyRecorded) {
recordKeystroke(directionKeys[initialDirection], false, false);
alreadyRecorded = true;
}
if (rand_percent(25)) {
vomit(&player);
playerTurnEnded();
return false;
}
}
// Proceeding with the attack.
if (!alreadyRecorded) {
recordKeystroke(directionKeys[initialDirection], false, false);
alreadyRecorded = true;
}
// Attack!
for (i=0; i<16; i++) {
if (hitList[i]
&& monsterWillAttackTarget(&player, hitList[i])
&& !(hitList[i]->bookkeepingFlags & MB_IS_DYING)
&& !rogue.gameHasEnded) {
if (attack(&player, hitList[i], false)) {
anyAttackHit = true;
}
}
}
playerRecoversFromAttacking(anyAttackHit);
moveEntrancedMonsters(direction);
playerTurnEnded();
return true;
}
}
if (player.bookkeepingFlags & MB_SEIZED) {
for (tempMonst = monsters->nextCreature; tempMonst != NULL; tempMonst = tempMonst->nextCreature) {
if ((tempMonst->bookkeepingFlags & MB_SEIZING)
&& monstersAreEnemies(&player, tempMonst)
&& distanceBetween(player.xLoc, player.yLoc, tempMonst->xLoc, tempMonst->yLoc) == 1
&& !diagonalBlocked(player.xLoc, player.yLoc, tempMonst->xLoc, tempMonst->yLoc, false)
&& !tempMonst->status[STATUS_ENTRANCED]) {
monsterName(monstName, tempMonst, true);
if (alreadyRecorded || !canSeeMonster(tempMonst)) {
if (!alreadyRecorded) {
recordKeystroke(directionKeys[initialDirection], false, false);
alreadyRecorded = true;
}
sprintf(buf, "you struggle but %s is holding your legs!", monstName);
moveEntrancedMonsters(direction);
message(buf, false);
playerTurnEnded();
return true;
} else {
sprintf(buf, "you cannot move; %s is holding your legs!", monstName);
message(buf, false);
return false;
}
}
}
player.bookkeepingFlags &= ~MB_SEIZED; // failsafe
}
if (pmap[newX][newY].flags & (DISCOVERED | MAGIC_MAPPED)
&& player.status[STATUS_LEVITATING] <= 1
&& !player.status[STATUS_CONFUSED]
&& cellHasTerrainFlag(newX, newY, T_LAVA_INSTA_DEATH)
&& player.status[STATUS_IMMUNE_TO_FIRE] <= 1
&& !cellHasTerrainFlag(newX, newY, T_ENTANGLES)
&& !cellHasTMFlag(newX, newY, TM_IS_SECRET)) {
message("that would be certain death!", false);
return false; // player won't willingly step into lava
} else if (pmap[newX][newY].flags & (DISCOVERED | MAGIC_MAPPED)
&& player.status[STATUS_LEVITATING] <= 1
&& !player.status[STATUS_CONFUSED]
&& cellHasTerrainFlag(newX, newY, T_AUTO_DESCENT)
&& !cellHasTerrainFlag(newX, newY, T_ENTANGLES)
&& !cellHasTMFlag(newX, newY, TM_IS_SECRET)
&& !confirm("Dive into the depths?", false)) {
return false;
} else if (playerCanSee(newX, newY)
&& !player.status[STATUS_CONFUSED]
&& !player.status[STATUS_BURNING]
&& player.status[STATUS_IMMUNE_TO_FIRE] <= 1
&& cellHasTerrainFlag(newX, newY, T_IS_FIRE)
&& !cellHasTMFlag(newX, newY, TM_EXTINGUISHES_FIRE)
&& !confirm("Venture into flame?", false)) {
return false;
} else if (playerCanSee(newX, newY)
&& !player.status[STATUS_CONFUSED]
&& !player.status[STATUS_BURNING]
&& cellHasTerrainFlag(newX, newY, T_CAUSES_CONFUSION | T_CAUSES_PARALYSIS)
&& (!rogue.armor || !(rogue.armor->flags & ITEM_RUNIC) || !(rogue.armor->flags & ITEM_RUNIC_IDENTIFIED) || rogue.armor->enchant2 != A_RESPIRATION)
&& !confirm("Venture into dangerous gas?", false)) {
return false;
} else if (pmap[newX][newY].flags & (ANY_KIND_OF_VISIBLE | MAGIC_MAPPED)
&& player.status[STATUS_LEVITATING] <= 1
&& !player.status[STATUS_CONFUSED]
&& cellHasTerrainFlag(newX, newY, T_IS_DF_TRAP)
&& !(pmap[newX][newY].flags & PRESSURE_PLATE_DEPRESSED)
&& !cellHasTMFlag(newX, newY, TM_IS_SECRET)
&& !confirm("Step onto the pressure plate?", false)) {
return false;
}
if (rogue.weapon && (rogue.weapon->flags & ITEM_LUNGE_ATTACKS)) {
newestX = player.xLoc + 2*nbDirs[direction][0];
newestY = player.yLoc + 2*nbDirs[direction][1];
if (coordinatesAreInMap(newestX, newestY) && (pmap[newestX][newestY].flags & HAS_MONSTER)) {
tempMonst = monsterAtLoc(newestX, newestY);
if (tempMonst
&& canSeeMonster(tempMonst)
&& monstersAreEnemies(&player, tempMonst)
&& tempMonst->creatureState != MONSTER_ALLY
&& !(tempMonst->bookkeepingFlags & MB_IS_DYING)
&& (!cellHasTerrainFlag(tempMonst->xLoc, tempMonst->yLoc, T_OBSTRUCTS_PASSABILITY) || (tempMonst->info.flags & MONST_ATTACKABLE_THRU_WALLS))) {
hitList[0] = tempMonst;
if (abortAttackAgainstAcidicTarget(hitList)) { // Acid mound attack confirmation.
brogueAssert(!alreadyRecorded);
rogue.disturbed = true;
return false;
}
}
}
}
if (rogue.weapon && (rogue.weapon->flags & ITEM_PASS_ATTACKS)) {
buildFlailHitList(x, y, newX, newY, hitList);
if (abortAttackAgainstAcidicTarget(hitList)) { // Acid mound attack confirmation.
brogueAssert(!alreadyRecorded);
rogue.disturbed = true;
return false;
}
}
if (rogue.patchVersion >= 1 && player.status[STATUS_STUCK] && cellHasTerrainFlag(x, y, T_ENTANGLES)) {
// Don't interrupt exploration with this message.
if (--player.status[STATUS_STUCK]) {
if (!rogue.automationActive) {
message("you struggle but cannot free yourself.", false);
}
moveEntrancedMonsters(direction);
if (!alreadyRecorded) {
recordKeystroke(directionKeys[initialDirection], false, false);
alreadyRecorded = true;
}
playerTurnEnded();
return true;
} else {
if (!rogue.automationActive) {
message("you break free!", false);
}
if (tileCatalog[pmap[x][y].layers[SURFACE]].flags & T_ENTANGLES) {
pmap[x][y].layers[SURFACE] = NOTHING;
}
}
}
if (player.status[STATUS_NAUSEOUS]) {
if (!alreadyRecorded) {
recordKeystroke(directionKeys[initialDirection], false, false);
alreadyRecorded = true;
}
if (rand_percent(25)) {
vomit(&player);
playerTurnEnded();
return true;
}
}
// Are we taking the stairs?
if (rogue.downLoc[0] == newX && rogue.downLoc[1] == newY) {
if (!alreadyRecorded) {
recordKeystroke(directionKeys[initialDirection], false, false);
alreadyRecorded = true;
}
useStairs(1);
} else if (rogue.upLoc[0] == newX && rogue.upLoc[1] == newY) {
if (!alreadyRecorded) {
recordKeystroke(directionKeys[initialDirection], false, false);
alreadyRecorded = true;
}
useStairs(-1);
} else {
// Okay, we're finally moving!
if (!alreadyRecorded) {
recordKeystroke(directionKeys[initialDirection], false, false);
alreadyRecorded = true;
}
player.xLoc += nbDirs[direction][0];
player.yLoc += nbDirs[direction][1];
pmap[x][y].flags &= ~HAS_PLAYER;
pmap[player.xLoc][player.yLoc].flags |= HAS_PLAYER;
pmap[player.xLoc][player.yLoc].flags &= ~IS_IN_PATH;
if (defender && defender->creatureState == MONSTER_ALLY) { // Swap places with ally.
pmap[defender->xLoc][defender->yLoc].flags &= ~HAS_MONSTER;
defender->xLoc = x;
defender->yLoc = y;
if (monsterAvoids(defender, x, y)) {
getQualifyingPathLocNear(&(defender->xLoc), &(defender->yLoc), player.xLoc, player.yLoc, true, forbiddenFlagsForMonster(&(defender->info)), 0, 0, (HAS_PLAYER | HAS_MONSTER | HAS_STAIRS), false);
}
//getQualifyingLocNear(loc, player.xLoc, player.yLoc, true, NULL, forbiddenFlagsForMonster(&(defender->info)) & ~(T_IS_DF_TRAP | T_IS_DEEP_WATER | T_SPONTANEOUSLY_IGNITES), HAS_MONSTER, false, false);
//defender->xLoc = loc[0];
//defender->yLoc = loc[1];
pmap[defender->xLoc][defender->yLoc].flags |= HAS_MONSTER;
}
if (pmap[player.xLoc][player.yLoc].flags & HAS_ITEM) {
pickUpItemAt(player.xLoc, player.yLoc);
rogue.disturbed = true;
}
refreshDungeonCell(x, y);
refreshDungeonCell(player.xLoc, player.yLoc);
playerMoved = true;
checkForMissingKeys(x, y);
if (monsterShouldFall(&player)) {
player.bookkeepingFlags |= MB_IS_FALLING;
}
moveEntrancedMonsters(direction);
// Perform a lunge or flail attack if appropriate.
for (i=0; i<16; i++) {
if (hitList[i]) {
if (attack(&player, hitList[i], (rogue.weapon && (rogue.weapon->flags & ITEM_LUNGE_ATTACKS)))) {
anyAttackHit = true;
}
}
}
if (hitList[0]) {
playerRecoversFromAttacking(anyAttackHit);
}
playerTurnEnded();
}
} else if (cellHasTerrainFlag(newX, newY, T_OBSTRUCTS_PASSABILITY)) {
i = pmap[newX][newY].layers[layerWithFlag(newX, newY, T_OBSTRUCTS_PASSABILITY)];
if ((tileCatalog[i].flags & T_OBSTRUCTS_PASSABILITY)
&& (!diagonalBlocked(x, y, newX, newY, false) || !cellHasTMFlag(newX, newY, TM_PROMOTES_WITH_KEY))) {
if (!(pmap[newX][newY].flags & DISCOVERED)) {
if (!alreadyRecorded) {
recordKeystroke(directionKeys[initialDirection], false, false);
alreadyRecorded = true;
}
discoverCell(newX, newY);
refreshDungeonCell(newX, newY);
}
messageWithColor(tileCatalog[i].flavorText, &backgroundMessageColor, false);
}
}
return playerMoved;
}
// replaced in Dijkstra.c:
/*
// returns true if the cell value changed
boolean updateDistanceCell(short **distanceMap, short x, short y) {
short dir, newX, newY;
boolean somethingChanged = false;
if (distanceMap[x][y] >= 0 && distanceMap[x][y] < 30000) {
for (dir=0; dir< DIRECTION_COUNT; dir++) {
newX = x + nbDirs[dir][0];
newY = y + nbDirs[dir][1];
if (coordinatesAreInMap(newX, newY)
&& distanceMap[newX][newY] >= distanceMap[x][y] + 2
&& !diagonalBlocked(x, y, newX, newY)) {
distanceMap[newX][newY] = distanceMap[x][y] + 1;
somethingChanged = true;
}
}
}
return somethingChanged;
}
void dijkstraScan(short **distanceMap, char passMap[DCOLS][DROWS], boolean allowDiagonals) {
short i, j, maxDir;
enum directions dir;
boolean somethingChanged;
maxDir = (allowDiagonals ? 8 : 4);
do {
somethingChanged = false;
for (i=1; i<DCOLS-1; i++) {
for (j=1; j<DROWS-1; j++) {
if (!passMap || passMap[i][j]) {
for (dir = 0; dir < maxDir; dir++) {
if (coordinatesAreInMap(i + nbDirs[dir][0], j + nbDirs[dir][1])
&& (!passMap || passMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]])
&& distanceMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]] >= distanceMap[i][j] + 2) {
distanceMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]] = distanceMap[i][j] + 1;
somethingChanged = true;
}
}
}
}
}
for (i = DCOLS - 1; i >= 0; i--) {
for (j = DROWS - 1; j >= 0; j--) {
if (!passMap || passMap[i][j]) {
for (dir = 0; dir < maxDir; dir++) {
if (coordinatesAreInMap(i + nbDirs[dir][0], j + nbDirs[dir][1])
&& (!passMap || passMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]])
&& distanceMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]] >= distanceMap[i][j] + 2) {
distanceMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]] = distanceMap[i][j] + 1;
somethingChanged = true;
}
}
}
}
}
} while (somethingChanged);
}*/
/*void enqueue(short x, short y, short val, distanceQueue *dQ) {
short *qX2, *qY2, *qVal2;
// if we need to allocate more memory:
if (dQ->qLen + 1 > dQ->qMaxLen) {
dQ->qMaxLen *= 2;
qX2 = realloc(dQ->qX, dQ->qMaxLen);
if (qX2) {
free(dQ->qX);
dQ->qX = qX2;
} else {
// out of memory
}
qY2 = realloc(dQ->qY, dQ->qMaxLen);
if (qY2) {
free(dQ->qY);
dQ->qY = qY2;
} else {
// out of memory
}
qVal2 = realloc(dQ->qVal, dQ->qMaxLen);
if (qVal2) {
free(dQ->qVal);
dQ->qVal = qVal2;
} else {
// out of memory
}
}
dQ->qX[dQ->qLen] = x;
dQ->qY[dQ->qLen] = y;
(dQ->qVal)[dQ->qLen] = val;
dQ->qLen++;
if (val < dQ->qMinVal) {
dQ->qMinVal = val;
dQ->qMinCount = 1;
} else if (val == dQ->qMinVal) {
dQ->qMinCount++;
}
}
void updateQueueMinCache(distanceQueue *dQ) {
short i;
dQ->qMinCount = 0;
dQ->qMinVal = 30001;
for (i = 0; i < dQ->qLen; i++) {
if (dQ->qVal[i] < dQ->qMinVal) {
dQ->qMinVal = dQ->qVal[i];
dQ->qMinCount = 1;
} else if (dQ->qVal[i] == dQ->qMinVal) {
dQ->qMinCount++;
}
}
}
// removes the lowest value from the queue, populates x/y/value variables and updates min caching
void dequeue(short *x, short *y, short *val, distanceQueue *dQ) {
short i, minIndex;
if (dQ->qMinCount <= 0) {
updateQueueMinCache(dQ);
}
*val = dQ->qMinVal;
// find the last instance of the minVal
for (minIndex = dQ->qLen - 1; minIndex >= 0 && dQ->qVal[minIndex] != *val; minIndex--);
// populate the return variables
*x = dQ->qX[minIndex];
*y = dQ->qY[minIndex];
dQ->qLen--;
// delete the minValue queue entry
for (i = minIndex; i < dQ->qLen; i++) {
dQ->qX[i] = dQ->qX[i+1];
dQ->qY[i] = dQ->qY[i+1];
dQ->qVal[i] = dQ->qVal[i+1];
}
// update min values
dQ->qMinCount--;
if (!dQ->qMinCount && dQ->qLen) {
updateQueueMinCache(dQ);
}
}
void dijkstraScan(short **distanceMap, char passMap[DCOLS][DROWS], boolean allowDiagonals) {
short i, j, maxDir, val;
enum directions dir;
distanceQueue dQ;
dQ.qMaxLen = DCOLS * DROWS * 1.5;
dQ.qX = (short *) malloc(dQ.qMaxLen * sizeof(short));
dQ.qY = (short *) malloc(dQ.qMaxLen * sizeof(short));
dQ.qVal = (short *) malloc(dQ.qMaxLen * sizeof(short));
dQ.qLen = 0;
dQ.qMinVal = 30000;
dQ.qMinCount = 0;
maxDir = (allowDiagonals ? 8 : 4);
// seed the queue with the entire map
for (i=0; i<DCOLS; i++) {
for (j=0; j<DROWS; j++) {
if (!passMap || passMap[i][j]) {
enqueue(i, j, distanceMap[i][j], &dQ);
}
}
}
// iterate through queue updating lowest entries until the queue is empty
while (dQ.qLen) {
dequeue(&i, &j, &val, &dQ);
if (distanceMap[i][j] == val) { // if it hasn't been improved since joining the queue
for (dir = 0; dir < maxDir; dir++) {
if (coordinatesAreInMap(i + nbDirs[dir][0], j + nbDirs[dir][1])
&& (!passMap || passMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]])
&& distanceMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]] >= distanceMap[i][j] + 2) {
distanceMap[i + nbDirs[dir][0]][j + nbDirs[dir][1]] = distanceMap[i][j] + 1;
enqueue(i + nbDirs[dir][0], j + nbDirs[dir][1], distanceMap[i][j] + 1, &dQ);
}
}
}
}
free(dQ.qX);
free(dQ.qY);
free(dQ.qVal);
}*/
/*
void calculateDistances(short **distanceMap, short destinationX, short destinationY, unsigned long blockingTerrainFlags, creature *traveler) {
short i, j;
boolean somethingChanged;
for (i=0; i<DCOLS; i++) {
for (j=0; j<DROWS; j++) {
distanceMap[i][j] = ((traveler && traveler == &player && !(pmap[i][j].flags & (DISCOVERED | MAGIC_MAPPED)))
|| ((traveler && monsterAvoids(traveler, i, j))
|| cellHasTerrainFlag(i, j, blockingTerrainFlags))) ? -1 : 30000;
}
}
distanceMap[destinationX][destinationY] = 0;
// dijkstraScan(distanceMap);
do {
somethingChanged = false;
for (i=0; i<DCOLS; i++) {
for (j=0; j<DROWS; j++) {
if (updateDistanceCell(distanceMap, i, j)) {
somethingChanged = true;
}
}
}
for (i = DCOLS - 1; i >= 0; i--) {
for (j = DROWS - 1; j >= 0; j--) {
if (updateDistanceCell(distanceMap, i, j)) {
somethingChanged = true;
}
}
}
} while (somethingChanged);
}*/
// Returns -1 if there are no beneficial moves.
// If preferDiagonals is true, we will prefer diagonal moves.
// Always rolls downhill on the distance map.
// If monst is provided, do not return a direction pointing to
// a cell that the monster avoids.
short nextStep(short **distanceMap, short x, short y, creature *monst, boolean preferDiagonals) {
short newX, newY, bestScore;
enum directions dir, bestDir;
creature *blocker;
boolean blocked;
brogueAssert(coordinatesAreInMap(x, y));
bestScore = 0;
bestDir = NO_DIRECTION;
for (dir = (preferDiagonals ? 7 : 0);
(preferDiagonals ? dir >= 0 : dir < DIRECTION_COUNT);
(preferDiagonals ? dir-- : dir++)) {
newX = x + nbDirs[dir][0];
newY = y + nbDirs[dir][1];
brogueAssert(coordinatesAreInMap(newX, newY));
if (coordinatesAreInMap(newX, newY)) {
blocked = false;
blocker = monsterAtLoc(newX, newY);
if (monst
&& monsterAvoids(monst, newX, newY)) {
blocked = true;
} else if (monst
&& blocker
&& !canPass(monst, blocker)
&& !monstersAreTeammates(monst, blocker)
&& !monstersAreEnemies(monst, blocker)) {
blocked = true;
}
if ((distanceMap[x][y] - distanceMap[newX][newY]) > bestScore
&& !diagonalBlocked(x, y, newX, newY, monst == &player)
&& knownToPlayerAsPassableOrSecretDoor(newX, newY)
&& !blocked) {
bestDir = dir;
bestScore = distanceMap[x][y] - distanceMap[newX][newY];
}
}
}
return bestDir;
}
void displayRoute(short **distanceMap, boolean removeRoute) {
short currentX = player.xLoc, currentY = player.yLoc, dir, newX, newY;
boolean advanced;
if (distanceMap[player.xLoc][player.yLoc] < 0 || distanceMap[player.xLoc][player.yLoc] == 30000) {
return;
}
do {
if (removeRoute) {
refreshDungeonCell(currentX, currentY);
} else {
hiliteCell(currentX, currentY, &hiliteColor, 50, true);
}
advanced = false;
for (dir = 7; dir >= 0; dir--) {
newX = currentX + nbDirs[dir][0];
newY = currentY + nbDirs[dir][1];
if (coordinatesAreInMap(newX, newY)
&& distanceMap[newX][newY] >= 0 && distanceMap[newX][newY] < distanceMap[currentX][currentY]
&& !diagonalBlocked(currentX, currentY, newX, newY, true)) {
currentX = newX;
currentY = newY;
advanced = true;
break;
}
}
} while (advanced);
}
void travelRoute(short path[1000][2], short steps) {
short i, j;
short dir;
creature *monst;
brogueAssert(!rogue.playbackMode);
rogue.disturbed = false;
rogue.automationActive = true;
for (monst = monsters->nextCreature; monst != NULL; monst = monst->nextCreature) {
if (canSeeMonster(monst)) {
monst->bookkeepingFlags |= MB_ALREADY_SEEN;
} else {
monst->bookkeepingFlags &= ~MB_ALREADY_SEEN;
}
}
for (i=0; i < steps && !rogue.disturbed; i++) {
for (j = i + 1; j < steps - 1; j++) {
// Check to see if the path has become obstructed or avoided since the last time we saw it.
if (diagonalBlocked(path[j-1][0], path[j-1][1], path[j][0], path[j][1], true)
|| monsterAvoids(&player, path[j][0], path[j][1])) {
rogue.disturbed = true;
break;
}
}
for (dir = 0; dir < DIRECTION_COUNT && !rogue.disturbed; dir++) {
if (player.xLoc + nbDirs[dir][0] == path[i][0]
&& player.yLoc + nbDirs[dir][1] == path[i][1]) {
if (!playerMoves(dir)) {
rogue.disturbed = true;
}
if (pauseBrogue(25)) {
rogue.disturbed = true;
}
break;
}
}
}
rogue.disturbed = true;
rogue.automationActive = false;
updateFlavorText();
}
void travelMap(short **distanceMap) {
short currentX = player.xLoc, currentY = player.yLoc, dir, newX, newY;
boolean advanced;
rogue.disturbed = false;
rogue.automationActive = true;
if (distanceMap[player.xLoc][player.yLoc] < 0 || distanceMap[player.xLoc][player.yLoc] == 30000) {
return;
}
do {
advanced = false;
for (dir = 7; dir >= 0; dir--) {
newX = currentX + nbDirs[dir][0];
newY = currentY + nbDirs[dir][1];
if (coordinatesAreInMap(newX, newY)
&& distanceMap[newX][newY] >= 0
&& distanceMap[newX][newY] < distanceMap[currentX][currentY]
&& !diagonalBlocked(currentX, currentY, newX, newY, true)) {
if (!playerMoves(dir)) {
rogue.disturbed = true;
}
if (pauseBrogue(500)) {
rogue.disturbed = true;
}
currentX = newX;
currentY = newY;
advanced = true;
break;
}
}
} while (advanced && !rogue.disturbed);
rogue.disturbed = true;
rogue.automationActive = false;
updateFlavorText();
}
void travel(short x, short y, boolean autoConfirm) {
short **distanceMap, i;
rogueEvent theEvent;
unsigned short staircaseConfirmKey;
confirmMessages();
if (D_WORMHOLING) {
recordMouseClick(mapToWindowX(x), mapToWindowY(y), true, false);
pmap[player.xLoc][player.yLoc].flags &= ~HAS_PLAYER;
refreshDungeonCell(player.xLoc, player.yLoc);
player.xLoc = x;
player.yLoc = y;
pmap[x][y].flags |= HAS_PLAYER;
updatePlayerUnderwaterness();
refreshDungeonCell(x, y);
updateVision(true);
return;
}
if (abs(player.xLoc - x) + abs(player.yLoc - y) == 1) {
// targeting a cardinal neighbor
for (i=0; i<4; i++) {
if (nbDirs[i][0] == (x - player.xLoc) && nbDirs[i][1] == (y - player.yLoc)) {
playerMoves(i);
break;
}
}
return;
}
if (!(pmap[x][y].flags & (DISCOVERED | MAGIC_MAPPED))) {
message("You have not explored that location.", false);
return;
}
distanceMap = allocGrid();
calculateDistances(distanceMap, x, y, 0, &player, false, false);
if (distanceMap[player.xLoc][player.yLoc] < 30000) {
if (autoConfirm) {
travelMap(distanceMap);
//refreshSideBar(-1, -1, false);
} else {
if (rogue.upLoc[0] == x && rogue.upLoc[1] == y) {
staircaseConfirmKey = ASCEND_KEY;
} else if (rogue.downLoc[0] == x && rogue.downLoc[1] == y) {
staircaseConfirmKey = DESCEND_KEY;
} else {
staircaseConfirmKey = 0;
}
displayRoute(distanceMap, false);
message("Travel this route? (y/n)", false);
do {
nextBrogueEvent(&theEvent, true, false, false);
} while (theEvent.eventType != MOUSE_UP && theEvent.eventType != KEYSTROKE);
displayRoute(distanceMap, true); // clear route display
confirmMessages();
if ((theEvent.eventType == MOUSE_UP && windowToMapX(theEvent.param1) == x && windowToMapY(theEvent.param2) == y)
|| (theEvent.eventType == KEYSTROKE && (theEvent.param1 == 'Y' || theEvent.param1 == 'y'
|| theEvent.param1 == RETURN_KEY
|| (theEvent.param1 == staircaseConfirmKey
&& theEvent.param1 != 0)))) {
travelMap(distanceMap);
//refreshSideBar(-1, -1, false);
commitDraws();
} else if (theEvent.eventType == MOUSE_UP) {
executeMouseClick(&theEvent);
}
}
// if (player.xLoc == x && player.yLoc == y) {
// rogue.cursorLoc[0] = rogue.cursorLoc[1] = 0;
// } else {
// rogue.cursorLoc[0] = x;
// rogue.cursorLoc[1] = y;
// }
} else {
rogue.cursorLoc[0] = rogue.cursorLoc[1] = -1;
message("No path is available.", false);
}
freeGrid(distanceMap);
}
void populateGenericCostMap(short **costMap) {
short i, j;
for (i=0; i<DCOLS; i++) {
for (j=0; j<DROWS; j++) {
if (cellHasTerrainFlag(i, j, T_OBSTRUCTS_PASSABILITY)
&& (!cellHasTMFlag(i, j, TM_IS_SECRET) || (discoveredTerrainFlagsAtLoc(i, j) & T_OBSTRUCTS_PASSABILITY))) {
costMap[i][j] = cellHasTerrainFlag(i, j, T_OBSTRUCTS_DIAGONAL_MOVEMENT) ? PDS_OBSTRUCTION : PDS_FORBIDDEN;
} else if (cellHasTerrainFlag(i, j, T_PATHING_BLOCKER & ~T_OBSTRUCTS_PASSABILITY)) {
costMap[i][j] = PDS_FORBIDDEN;
} else {
costMap[i][j] = 1;
}
}
}
}
void getLocationFlags(const short x, const short y,
unsigned long *tFlags, unsigned long *TMFlags, unsigned long *cellFlags,
const boolean limitToPlayerKnowledge) {
if (limitToPlayerKnowledge
&& (pmap[x][y].flags & (DISCOVERED | MAGIC_MAPPED))
&& !playerCanSee(x, y)) {
if (tFlags) {
*tFlags = pmap[x][y].rememberedTerrainFlags;
}
if (TMFlags) {
*TMFlags = pmap[x][y].rememberedTMFlags;
}
if (cellFlags) {
*cellFlags = pmap[x][y].rememberedCellFlags;
}
} else {
if (tFlags) {
*tFlags = terrainFlags(x, y);
}
if (TMFlags) {
*TMFlags = terrainMechFlags(x, y);
}
if (cellFlags) {
*cellFlags = pmap[x][y].flags;
}
}
}
void populateCreatureCostMap(short **costMap, creature *monst) {
short i, j, unexploredCellCost;
creature *currentTenant;
item *theItem;
unsigned long tFlags, cFlags;
unexploredCellCost = 10 + (clamp(rogue.depthLevel, 5, 15) - 5) * 2;
for (i=0; i<DCOLS; i++) {
for (j=0; j<DROWS; j++) {
if (monst == &player && !(pmap[i][j].flags & (DISCOVERED | MAGIC_MAPPED))) {
costMap[i][j] = PDS_OBSTRUCTION;
continue;
}
getLocationFlags(i, j, &tFlags, NULL, &cFlags, monst == &player);
if ((tFlags & T_OBSTRUCTS_PASSABILITY)
&& (!cellHasTMFlag(i, j, TM_IS_SECRET) || (discoveredTerrainFlagsAtLoc(i, j) & T_OBSTRUCTS_PASSABILITY) || monst == &player)) {
costMap[i][j] = (tFlags & T_OBSTRUCTS_DIAGONAL_MOVEMENT) ? PDS_OBSTRUCTION : PDS_FORBIDDEN;
continue;
}
if ((tFlags & T_LAVA_INSTA_DEATH)
&& !(monst->info.flags & (MONST_IMMUNE_TO_FIRE | MONST_FLIES | MONST_INVULNERABLE))
&& (monst->status[STATUS_LEVITATING] || monst->status[STATUS_IMMUNE_TO_FIRE])
&& max(monst->status[STATUS_LEVITATING], monst->status[STATUS_IMMUNE_TO_FIRE]) < (rogue.mapToShore[i][j] + distanceBetween(i, j, monst->xLoc, monst->yLoc) * monst->movementSpeed / 100)) {
// Only a temporary effect will permit the monster to survive the lava, and the remaining duration either isn't
// enough to get it to the spot, or it won't suffice to let it return to shore if it does get there.
// Treat these locations as obstacles.
costMap[i][j] = PDS_FORBIDDEN;
continue;
}
if (((tFlags & T_AUTO_DESCENT) || (tFlags & T_IS_DEEP_WATER) && !(monst->info.flags & MONST_IMMUNE_TO_WATER))
&& !(monst->info.flags & MONST_FLIES)
&& (monst->status[STATUS_LEVITATING])
&& monst->status[STATUS_LEVITATING] < (rogue.mapToShore[i][j] + distanceBetween(i, j, monst->xLoc, monst->yLoc) * monst->movementSpeed / 100)) {
// Only a temporary effect will permit the monster to levitate over the chasm/water, and the remaining duration either isn't
// enough to get it to the spot, or it won't suffice to let it return to shore if it does get there.
// Treat these locations as obstacles.
costMap[i][j] = PDS_FORBIDDEN;
continue;
}
if (monsterAvoids(monst, i, j)) {
costMap[i][j] = PDS_FORBIDDEN;
continue;
}
if (cFlags & HAS_MONSTER) {
currentTenant = monsterAtLoc(i, j);
if (currentTenant
&& (currentTenant->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE))
&& !canPass(monst, currentTenant)) {
costMap[i][j] = PDS_FORBIDDEN;
continue;
}
}
if ((cFlags & KNOWN_TO_BE_TRAP_FREE)
|| (monst != &player && monst->creatureState != MONSTER_ALLY)) {
costMap[i][j] = 10;
} else {
// Player and allies give locations that are known to be free of traps
// an advantage that increases with depth level, based on the depths
// at which traps are generated.
costMap[i][j] = unexploredCellCost;
}
if (!(monst->info.flags & MONST_INVULNERABLE)) {
if ((tFlags & T_CAUSES_NAUSEA)
|| cellHasTMFlag(i, j, TM_PROMOTES_ON_ITEM_PICKUP)
|| (tFlags & T_ENTANGLES) && !(monst->info.flags & MONST_IMMUNE_TO_WEBS)) {
costMap[i][j] += 20;
}
}
if (monst == &player) {
theItem = itemAtLoc(i, j);
if (theItem && (theItem->flags & ITEM_PLAYER_AVOIDS)) {
costMap[i][j] += 10;
}
}
}
}
}
enum directions adjacentFightingDir() {
short newX, newY;
enum directions dir;
creature *monst;
if (cellHasTerrainFlag(player.xLoc, player.yLoc, T_OBSTRUCTS_PASSABILITY)) {
return NO_DIRECTION;
}
for (dir = 0; dir < DIRECTION_COUNT; dir++) {
newX = player.xLoc + nbDirs[dir][0];
newY = player.yLoc + nbDirs[dir][1];
monst = monsterAtLoc(newX, newY);
if (monst
&& canSeeMonster(monst)
&& (!diagonalBlocked(player.xLoc, player.yLoc, newX, newY, false) || (monst->info.flags & MONST_ATTACKABLE_THRU_WALLS))
&& monstersAreEnemies(&player, monst)
&& !(monst->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE))) {
return dir;
}
}
return NO_DIRECTION;
}
#define exploreGoalValue(x, y) (0 - abs((x) - DCOLS / 2) / 3 - abs((x) - DCOLS / 2) / 4)
void getExploreMap(short **map, boolean headingToStairs) {// calculate explore map
short i, j;
short **costMap;
item *theItem;
costMap = allocGrid();
populateCreatureCostMap(costMap, &player);
for (i=0; i<DCOLS; i++) {
for (j=0; j<DROWS; j++) {
map[i][j] = 30000; // Can be overridden later.
theItem = itemAtLoc(i, j);
if (!(pmap[i][j].flags & DISCOVERED)) {
if ((pmap[i][j].flags & MAGIC_MAPPED)
&& (tileCatalog[pmap[i][j].layers[DUNGEON]].flags | tileCatalog[pmap[i][j].layers[LIQUID]].flags) & T_PATHING_BLOCKER) {
// Magic-mapped cells revealed as obstructions should be treated as such even though they're not discovered.
costMap[i][j] = cellHasTerrainFlag(i, j, T_OBSTRUCTS_DIAGONAL_MOVEMENT) ? PDS_OBSTRUCTION : PDS_FORBIDDEN;
} else {
costMap[i][j] = 1;
map[i][j] = exploreGoalValue(i, j);
}
} else if (theItem
&& !monsterAvoids(&player, i, j)) {
if (theItem->flags & ITEM_PLAYER_AVOIDS) {
costMap[i][j] = 20;
} else {
costMap[i][j] = 1;
map[i][j] = exploreGoalValue(i, j) - 10;
}
}
}
}
costMap[rogue.downLoc[0]][rogue.downLoc[1]] = 100;
costMap[rogue.upLoc[0]][rogue.upLoc[1]] = 100;
if (headingToStairs) {
map[rogue.downLoc[0]][rogue.downLoc[1]] = 0; // head to the stairs
}
dijkstraScan(map, costMap, true);
//displayGrid(costMap);
freeGrid(costMap);
}
boolean explore(short frameDelay) {
short **distanceMap;
short path[1000][2], steps;
boolean madeProgress, headingToStairs;
enum directions dir;
creature *monst;
// Explore commands should never be written to a recording.
// Instead, the elemental movement commands that compose it
// should be written individually.
brogueAssert(!rogue.playbackMode);
clearCursorPath();
madeProgress = false;
headingToStairs = false;
if (player.status[STATUS_CONFUSED]) {
message("Not while you're confused.", false);
return false;
}
if (cellHasTerrainFlag(player.xLoc, player.yLoc, T_OBSTRUCTS_PASSABILITY)) {
message("Not while you're trapped.", false);
return false;
}
for (monst = monsters->nextCreature; monst != NULL; monst = monst->nextCreature) {
if (canSeeMonster(monst)) {
monst->bookkeepingFlags |= MB_ALREADY_SEEN;
} else {
monst->bookkeepingFlags &= ~MB_ALREADY_SEEN;
}
}
// fight any adjacent enemies
dir = adjacentFightingDir();
if (dir != NO_DIRECTION
&& startFighting(dir, (player.status[STATUS_HALLUCINATING] ? true : false))) {
return true;
}
if (!rogue.autoPlayingLevel) {
message(KEYBOARD_LABELS ? "Exploring... press any key to stop." : "Exploring... touch anywhere to stop.",
false);
// A little hack so the exploring message remains bright while exploring and then auto-dims when
// another message is displayed:
confirmMessages();
printString(KEYBOARD_LABELS ? "Exploring... press any key to stop." : "Exploring... touch anywhere to stop.",
mapToWindowX(0), mapToWindowY(-1), &white, &black, NULL);
}
rogue.disturbed = false;
rogue.automationActive = true;
distanceMap = allocGrid();
do {
// fight any adjacent enemies
dir = adjacentFightingDir();
if (dir != NO_DIRECTION) {
startFighting(dir, (player.status[STATUS_HALLUCINATING] ? true : false));
if (rogue.disturbed) {
madeProgress = true;
continue;
}
}
if (rogue.disturbed) {
continue;
}
getExploreMap(distanceMap, headingToStairs);
// hilite path
steps = getPlayerPathOnMap(path, distanceMap, player.xLoc, player.yLoc);
hilitePath(path, steps, false);
// take a step
dir = nextStep(distanceMap, player.xLoc, player.yLoc, NULL, false);
if (!headingToStairs && rogue.autoPlayingLevel && dir == NO_DIRECTION) {
headingToStairs = true;
continue;
}
refreshSideBar(-1, -1, false);
if (dir == NO_DIRECTION) {
rogue.disturbed = true;
} else if (!playerMoves(dir)) {
rogue.disturbed = true;
} else {
madeProgress = true;
if (pauseBrogue(frameDelay)) {
rogue.disturbed = true;
rogue.autoPlayingLevel = false;
}
}
hilitePath(path, steps, true);
} while (!rogue.disturbed);
//clearCursorPath();
rogue.automationActive = false;
refreshSideBar(-1, -1, false);
freeGrid(distanceMap);
return madeProgress;
}
void autoPlayLevel(boolean fastForward) {
boolean madeProgress;
rogue.autoPlayingLevel = true;
confirmMessages();
message(KEYBOARD_LABELS ? "Playing... press any key to stop." : "Playing... touch anywhere to stop.", false);
// explore until we are not making progress
do {
madeProgress = explore(fastForward ? 1 : 50);
//refreshSideBar(-1, -1, false);
if (!madeProgress && rogue.downLoc[0] == player.xLoc && rogue.downLoc[1] == player.yLoc) {
useStairs(1);
madeProgress = true;
}
} while (madeProgress && rogue.autoPlayingLevel);
confirmMessages();
rogue.autoPlayingLevel = false;
}
short directionOfKeypress(unsigned short ch) {
switch (ch) {
case LEFT_KEY:
case LEFT_ARROW:
case NUMPAD_4:
return LEFT;
case RIGHT_KEY:
case RIGHT_ARROW:
case NUMPAD_6:
return RIGHT;
case UP_KEY:
case UP_ARROW:
case NUMPAD_8:
return UP;
case DOWN_KEY:
case DOWN_ARROW:
case NUMPAD_2:
return DOWN;
case UPLEFT_KEY:
case NUMPAD_7:
return UPLEFT;
case UPRIGHT_KEY:
case NUMPAD_9:
return UPRIGHT;
case DOWNLEFT_KEY:
case NUMPAD_1:
return DOWNLEFT;
case DOWNRIGHT_KEY:
case NUMPAD_3:
return DOWNRIGHT;
default:
return -1;
}
}
boolean startFighting(enum directions dir, boolean tillDeath) {
short x, y, expectedDamage;
creature *monst;
x = player.xLoc + nbDirs[dir][0];
y = player.yLoc + nbDirs[dir][1];
monst = monsterAtLoc(x, y);
if (monst->info.flags & (MONST_IMMUNE_TO_WEAPONS | MONST_INVULNERABLE)) {
return false;
}
expectedDamage = monst->info.damage.upperBound * monsterDamageAdjustmentAmount(monst) / FP_FACTOR;
if (rogue.easyMode) {
expectedDamage /= 5;
}
rogue.blockCombatText = true;
rogue.disturbed = false;
do {
if (!playerMoves(dir)) {
break;
}
if (pauseBrogue(1)) {
break;
}
} while (!rogue.disturbed && !rogue.gameHasEnded && (tillDeath || player.currentHP > expectedDamage)
&& (pmap[x][y].flags & HAS_MONSTER) && monsterAtLoc(x, y) == monst);
rogue.blockCombatText = false;
return rogue.disturbed;
}
boolean isDisturbed(short x, short y) {
short i;
creature *monst;
for (i=0; i< DIRECTION_COUNT; i++) {
monst = monsterAtLoc(x + nbDirs[i][0], y + nbDirs[i][1]);
if (pmap[x + nbDirs[i][0]][y + nbDirs[i][1]].flags & (HAS_ITEM)) {
// Do not trigger for submerged or invisible or unseen monsters.
return true;
}
if (monst
&& !(monst->creatureState == MONSTER_ALLY)
&& (canSeeMonster(monst) || monsterRevealed(monst))) {
// Do not trigger for submerged or invisible or unseen monsters.
return true;
}
}
return false;
}
void discover(short x, short y) {
enum dungeonLayers layer;
dungeonFeature *feat;
if (cellHasTMFlag(x, y, TM_IS_SECRET)) {
for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
if (tileCatalog[pmap[x][y].layers[layer]].mechFlags & TM_IS_SECRET) {
feat = &dungeonFeatureCatalog[tileCatalog[pmap[x][y].layers[layer]].discoverType];
pmap[x][y].layers[layer] = (layer == DUNGEON ? FLOOR : NOTHING);
spawnDungeonFeature(x, y, feat, true, false);
}
}
refreshDungeonCell(x, y);
if (playerCanSee(x, y)) {
rogue.disturbed = true;
}
}
}
// returns true if found anything
boolean search(short searchStrength) {
short i, j, radius, x, y, percent;
boolean foundSomething = false;
radius = searchStrength / 10;
x = player.xLoc;
y = player.yLoc;
for (i = x - radius; i <= x + radius; i++) {
for (j = y - radius; j <= y + radius; j++) {
if (coordinatesAreInMap(i, j)
&& playerCanDirectlySee(i, j)) {
percent = searchStrength - distanceBetween(x, y, i, j) * 10;
if (cellHasTerrainFlag(i, j, T_OBSTRUCTS_PASSABILITY)) {
percent = percent * 2/3;
}
if (percent >= 100) {
pmap[i][j].flags |= KNOWN_TO_BE_TRAP_FREE;
}
percent = min(percent, 100);
if (cellHasTMFlag(i, j, TM_IS_SECRET)) {
if (rand_percent(percent)) {
discover(i, j);
foundSomething = true;
}
}
}
}
}
return foundSomething;
}
boolean proposeOrConfirmLocation(short x, short y, char *failureMessage) {
boolean retval = false;
if (player.xLoc == x && player.yLoc == y) {
message("you are already there.", false);
} else if (pmap[x][y].flags & (DISCOVERED | MAGIC_MAPPED)) {
if (rogue.cursorLoc[0] == x && rogue.cursorLoc[1] == y) {
retval = true;
} else {
rogue.cursorLoc[0] = x;
rogue.cursorLoc[1] = y;
}
} else {
message(failureMessage, false);
}
return retval;
}
boolean useStairs(short stairDirection) {
boolean succeeded = false;
//cellDisplayBuffer fromBuf[COLS][ROWS], toBuf[COLS][ROWS];
if (stairDirection == 1) {
if (rogue.depthLevel < DEEPEST_LEVEL) {
//copyDisplayBuffer(fromBuf, displayBuffer);
rogue.cursorLoc[0] = rogue.cursorLoc[1] = -1;
rogue.depthLevel++;
message("You descend.", false);
startLevel(rogue.depthLevel - 1, stairDirection);
if (rogue.depthLevel > rogue.deepestLevel) {
rogue.deepestLevel = rogue.depthLevel;
}
//copyDisplayBuffer(toBuf, displayBuffer);
//irisFadeBetweenBuffers(fromBuf, toBuf, mapToWindowX(player.xLoc), mapToWindowY(player.yLoc), 20, false);
} else if (numberOfMatchingPackItems(AMULET, 0, 0, false)) {
victory(true);
} else {
confirmMessages();
messageWithColor("the crystal archway repels you with a mysterious force!", &lightBlue, false);
messageWithColor("(Only the bearer of the Amulet of Yendor may pass.)", &backgroundMessageColor, false);
}
succeeded = true;
} else {
if (rogue.depthLevel > 1 || numberOfMatchingPackItems(AMULET, 0, 0, false)) {
rogue.cursorLoc[0] = rogue.cursorLoc[1] = -1;
rogue.depthLevel--;
if (rogue.depthLevel == 0) {
victory(false);
} else {
//copyDisplayBuffer(fromBuf, displayBuffer);
message("You ascend.", false);
startLevel(rogue.depthLevel + 1, stairDirection);
//copyDisplayBuffer(toBuf, displayBuffer);
//irisFadeBetweenBuffers(fromBuf, toBuf, mapToWindowX(player.xLoc), mapToWindowY(player.yLoc), 20, true);
}
succeeded = true;
} else {
confirmMessages();
messageWithColor("The dungeon exit is magically sealed!", &lightBlue, false);
messageWithColor("(Only the bearer of the Amulet of Yendor may pass.)", &backgroundMessageColor, false);
}
}
if (succeeded) {
updatePlayerUnderwaterness();
}
return succeeded;
}
void storeMemories(const short x, const short y) {
pmap[x][y].rememberedTerrainFlags = terrainFlags(x, y);
pmap[x][y].rememberedTMFlags = terrainMechFlags(x, y);
pmap[x][y].rememberedCellFlags = pmap[x][y].flags;
pmap[x][y].rememberedTerrain = pmap[x][y].layers[highestPriorityLayer(x, y, false)];
}
void updateFieldOfViewDisplay(boolean updateDancingTerrain, boolean refreshDisplay) {
short i, j;
item *theItem;
char buf[COLS*3], name[COLS*3];
assureCosmeticRNG;
for (i=0; i<DCOLS; i++) {
for (j = DROWS-1; j >= 0; j--) {
if (pmap[i][j].flags & IN_FIELD_OF_VIEW
&& (max(0, tmap[i][j].light[0])
+ max(0, tmap[i][j].light[1])
+ max(0, tmap[i][j].light[2]) > VISIBILITY_THRESHOLD)
&& !(pmap[i][j].flags & CLAIRVOYANT_DARKENED)) {
pmap[i][j].flags |= VISIBLE;
}
if ((pmap[i][j].flags & VISIBLE) && !(pmap[i][j].flags & WAS_VISIBLE)) { // if the cell became visible this move
if (!(pmap[i][j].flags & DISCOVERED) && rogue.automationActive) {
if (pmap[i][j].flags & HAS_ITEM) {
theItem = itemAtLoc(i, j);
if (theItem && (theItem->category & KEY)) {
itemName(theItem, name, false, true, NULL);
sprintf(buf, "you see %s.", name);
messageWithColor(buf, &itemMessageColor, false);
}
}
if (!(pmap[i][j].flags & MAGIC_MAPPED)
&& cellHasTMFlag(i, j, TM_INTERRUPT_EXPLORATION_WHEN_SEEN)) {
strcpy(name, tileCatalog[pmap[i][j].layers[layerWithTMFlag(i, j, TM_INTERRUPT_EXPLORATION_WHEN_SEEN)]].description);
sprintf(buf, "you see %s.", name);
messageWithColor(buf, &backgroundMessageColor, false);
}
}
discoverCell(i, j);
if (refreshDisplay) {
refreshDungeonCell(i, j);
}
} else if (!(pmap[i][j].flags & VISIBLE) && (pmap[i][j].flags & WAS_VISIBLE)) { // if the cell ceased being visible this move
storeMemories(i, j);
if (refreshDisplay) {
refreshDungeonCell(i, j);
}
} else if (!(pmap[i][j].flags & CLAIRVOYANT_VISIBLE) && (pmap[i][j].flags & WAS_CLAIRVOYANT_VISIBLE)) { // ceased being clairvoyantly visible
storeMemories(i, j);
if (refreshDisplay) {
refreshDungeonCell(i, j);
}
} else if (!(pmap[i][j].flags & WAS_CLAIRVOYANT_VISIBLE) && (pmap[i][j].flags & CLAIRVOYANT_VISIBLE)) { // became clairvoyantly visible
pmap[i][j].flags &= ~STABLE_MEMORY;
if (refreshDisplay) {
refreshDungeonCell(i, j);
}
} else if (!(pmap[i][j].flags & TELEPATHIC_VISIBLE) && (pmap[i][j].flags & WAS_TELEPATHIC_VISIBLE)) { // ceased being telepathically visible
storeMemories(i, j);
if (refreshDisplay) {
refreshDungeonCell(i, j);
}
} else if (!(pmap[i][j].flags & WAS_TELEPATHIC_VISIBLE) && (pmap[i][j].flags & TELEPATHIC_VISIBLE)) { // became telepathically visible
if (!(pmap[i][j].flags & DISCOVERED)
&& !cellHasTerrainFlag(i, j, T_PATHING_BLOCKER)) {
rogue.xpxpThisTurn++;
}
pmap[i][j].flags &= ~STABLE_MEMORY;
if (refreshDisplay) {
refreshDungeonCell(i, j);
}
} else if (playerCanSeeOrSense(i, j)
&& (tmap[i][j].light[0] != tmap[i][j].oldLight[0] ||
tmap[i][j].light[1] != tmap[i][j].oldLight[1] ||
tmap[i][j].light[2] != tmap[i][j].oldLight[2])) { // if the cell's light color changed this move
if (refreshDisplay) {
refreshDungeonCell(i, j);
}
} else if (updateDancingTerrain
&& playerCanSee(i, j)
&& (!rogue.automationActive || !(rogue.playerTurnNumber % 5))
&& ((tileCatalog[pmap[i][j].layers[DUNGEON]].backColor) && tileCatalog[pmap[i][j].layers[DUNGEON]].backColor->colorDances
|| (tileCatalog[pmap[i][j].layers[DUNGEON]].foreColor) && tileCatalog[pmap[i][j].layers[DUNGEON]].foreColor->colorDances
|| (tileCatalog[pmap[i][j].layers[LIQUID]].backColor) && tileCatalog[pmap[i][j].layers[LIQUID]].backColor->colorDances
|| (tileCatalog[pmap[i][j].layers[LIQUID]].foreColor) && tileCatalog[pmap[i][j].layers[LIQUID]].foreColor->colorDances
|| (tileCatalog[pmap[i][j].layers[SURFACE]].backColor) && tileCatalog[pmap[i][j].layers[SURFACE]].backColor->colorDances
|| (tileCatalog[pmap[i][j].layers[SURFACE]].foreColor) && tileCatalog[pmap[i][j].layers[SURFACE]].foreColor->colorDances
|| (tileCatalog[pmap[i][j].layers[GAS]].backColor) && tileCatalog[pmap[i][j].layers[GAS]].backColor->colorDances
|| (tileCatalog[pmap[i][j].layers[GAS]].foreColor) && tileCatalog[pmap[i][j].layers[GAS]].foreColor->colorDances
|| player.status[STATUS_HALLUCINATING])) {
pmap[i][j].flags &= ~STABLE_MEMORY;
if (refreshDisplay) {
refreshDungeonCell(i, j);
}
}
}
}
restoreRNG;
}
// Octants: //
// \7|8/ //
// 6\|/1 //
// --@-- //
// 5/|\2 //
// /4|3\ //
void betweenOctant1andN(short *x, short *y, short x0, short y0, short n) {
short x1 = *x, y1 = *y;
short dx = x1 - x0, dy = y1 - y0;
switch (n) {
case 1:
return;
case 2:
*y = y0 - dy;
return;
case 5:
*x = x0 - dx;
*y = y0 - dy;
return;
case 6:
*x = x0 - dx;
return;
case 8:
*x = x0 - dy;
*y = y0 - dx;
return;
case 3:
*x = x0 - dy;
*y = y0 + dx;
return;
case 7:
*x = x0 + dy;
*y = y0 - dx;
return;
case 4:
*x = x0 + dy;
*y = y0 + dx;
return;
}
}
// Returns a boolean grid indicating whether each square is in the field of view of (xLoc, yLoc).
// forbiddenTerrain is the set of terrain flags that will block vision (but the blocking cell itself is
// illuminated); forbiddenFlags is the set of map flags that will block vision.
// If cautiousOnWalls is set, we will not illuminate blocking tiles unless the tile one space closer to the origin
// is visible to the player; this is to prevent lights from illuminating a wall when the player is on the other
// side of the wall.
void getFOVMask(char grid[DCOLS][DROWS], short xLoc, short yLoc, fixpt maxRadius,
unsigned long forbiddenTerrain, unsigned long forbiddenFlags, boolean cautiousOnWalls) {
short i;
for (i=1; i<=8; i++) {
scanOctantFOV(grid, xLoc, yLoc, i, maxRadius, 1, LOS_SLOPE_GRANULARITY * -1, 0,
forbiddenTerrain, forbiddenFlags, cautiousOnWalls);
}
}
// This is a custom implementation of recursive shadowcasting.
void scanOctantFOV(char grid[DCOLS][DROWS], short xLoc, short yLoc, short octant, fixpt maxRadius,
short columnsRightFromOrigin, long startSlope, long endSlope, unsigned long forbiddenTerrain,
unsigned long forbiddenFlags, boolean cautiousOnWalls) {
if (columnsRightFromOrigin * FP_FACTOR >= maxRadius) return;
short i, a, b, iStart, iEnd, x, y, x2, y2; // x and y are temporary variables on which we do the octant transform
long newStartSlope, newEndSlope;
boolean cellObstructed;
newStartSlope = startSlope;
a = ((LOS_SLOPE_GRANULARITY / -2 + 1) + startSlope * columnsRightFromOrigin) / LOS_SLOPE_GRANULARITY;
b = ((LOS_SLOPE_GRANULARITY / -2 + 1) + endSlope * columnsRightFromOrigin) / LOS_SLOPE_GRANULARITY;
iStart = min(a, b);
iEnd = max(a, b);
// restrict vision to a circle of radius maxRadius
if ((columnsRightFromOrigin*columnsRightFromOrigin + iEnd*iEnd) >= maxRadius*maxRadius / FP_FACTOR / FP_FACTOR) {
return;
}
if ((columnsRightFromOrigin*columnsRightFromOrigin + iStart*iStart) >= maxRadius*maxRadius / FP_FACTOR / FP_FACTOR) {
iStart = (int) (-1 * fp_sqrt((maxRadius*maxRadius / FP_FACTOR) - (columnsRightFromOrigin*columnsRightFromOrigin * FP_FACTOR)) / FP_FACTOR);
}
x = xLoc + columnsRightFromOrigin;
y = yLoc + iStart;
betweenOctant1andN(&x, &y, xLoc, yLoc, octant);
boolean currentlyLit = coordinatesAreInMap(x, y) && !(cellHasTerrainFlag(x, y, forbiddenTerrain) ||
(pmap[x][y].flags & forbiddenFlags));
for (i = iStart; i <= iEnd; i++) {
x = xLoc + columnsRightFromOrigin;
y = yLoc + i;
betweenOctant1andN(&x, &y, xLoc, yLoc, octant);
if (!coordinatesAreInMap(x, y)) {
// We're off the map -- here there be memory corruption.
continue;
}
cellObstructed = (cellHasTerrainFlag(x, y, forbiddenTerrain) || (pmap[x][y].flags & forbiddenFlags));
// if we're cautious on walls and this is a wall:
if (cautiousOnWalls && cellObstructed) {
// (x2, y2) is the tile one space closer to the origin from the tile we're on:
x2 = xLoc + columnsRightFromOrigin - 1;
y2 = yLoc + i;
if (i < 0) {
y2++;
} else if (i > 0) {
y2--;
}
betweenOctant1andN(&x2, &y2, xLoc, yLoc, octant);
if (pmap[x2][y2].flags & IN_FIELD_OF_VIEW) {
// previous tile is visible, so illuminate
grid[x][y] = 1;
}
} else {
// illuminate
grid[x][y] = 1;
}
if (!cellObstructed && !currentlyLit) { // next column slope starts here
newStartSlope = (long int) ((LOS_SLOPE_GRANULARITY * (i) - LOS_SLOPE_GRANULARITY / 2) / (columnsRightFromOrigin * 2 + 1) * 2);
currentlyLit = true;
} else if (cellObstructed && currentlyLit) { // next column slope ends here
newEndSlope = (long int) ((LOS_SLOPE_GRANULARITY * (i) - LOS_SLOPE_GRANULARITY / 2)
/ (columnsRightFromOrigin * 2 - 1) * 2);
if (newStartSlope <= newEndSlope) {
// run next column
scanOctantFOV(grid, xLoc, yLoc, octant, maxRadius, columnsRightFromOrigin + 1, newStartSlope, newEndSlope,
forbiddenTerrain, forbiddenFlags, cautiousOnWalls);
}
currentlyLit = false;
}
}
if (currentlyLit) { // got to the bottom of the scan while lit
newEndSlope = endSlope;
if (newStartSlope <= newEndSlope) {
// run next column
scanOctantFOV(grid, xLoc, yLoc, octant, maxRadius, columnsRightFromOrigin + 1, newStartSlope, newEndSlope,
forbiddenTerrain, forbiddenFlags, cautiousOnWalls);
}
}
}
void addScentToCell(short x, short y, short distance) {
unsigned short value;
if (!cellHasTerrainFlag(x, y, T_OBSTRUCTS_SCENT) || !cellHasTerrainFlag(x, y, T_OBSTRUCTS_PASSABILITY)) {
value = rogue.scentTurnNumber - distance;
scentMap[x][y] = max(value, (unsigned short) scentMap[x][y]);
}
}