Browse Source

Merge pull request #2 from ATGillespie25/fix/auto-walk-door-detection

pull/8474/head
mojsior 2 months ago committed by GitHub
parent
commit
4edfe9ef8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 730
      Source/diablo.cpp
  2. 19
      Source/objects.cpp
  3. 11
      Source/objects.h
  4. 23
      test/tile_properties_test.cpp

730
Source/diablo.cpp

@ -5,8 +5,8 @@
*/
#include <algorithm>
#include <array>
#include <cstdlib>
#include <cstdint>
#include <cstdlib>
#include <limits>
#include <queue>
#include <string_view>
@ -74,9 +74,9 @@
#include "levels/gendung.h"
#include "levels/setmaps.h"
#include "levels/themes.h"
#include "levels/tile_properties.hpp"
#include "levels/town.h"
#include "levels/trigs.h"
#include "levels/tile_properties.hpp"
#include "lighting.h"
#include "loadsave.h"
#include "lua/lua_global.hpp"
@ -88,15 +88,15 @@
#include "nthread.h"
#include "objects.h"
#include "options.h"
#include "panels/charpanel.hpp"
#include "panels/console.hpp"
#include "panels/info_box.hpp"
#include "panels/charpanel.hpp"
#include "panels/partypanel.hpp"
#include "panels/spell_book.hpp"
#include "panels/spell_list.hpp"
#include "pfile.h"
#include "portal.h"
#include "plrmsg.h"
#include "portal.h"
#include "qol/chatlog.h"
#include "qol/floatingnumbers.h"
#include "qol/itemlabels.h"
@ -175,13 +175,13 @@ namespace {
char gszVersionNumber[64] = "internal version unknown";
void SelectNextTownNpcKeyPressed();
void SelectPreviousTownNpcKeyPressed();
void UpdateAutoWalkTownNpc();
void UpdateAutoWalkTracker();
void AutoWalkToTrackerTargetKeyPressed();
void SpeakSelectedSpeedbookSpell();
void SpellBookKeyPressed();
void SelectNextTownNpcKeyPressed();
void SelectPreviousTownNpcKeyPressed();
void UpdateAutoWalkTownNpc();
void UpdateAutoWalkTracker();
void AutoWalkToTrackerTargetKeyPressed();
void SpeakSelectedSpeedbookSpell();
void SpellBookKeyPressed();
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeech(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false);
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechRespectingDoors(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false);
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false);
@ -189,7 +189,7 @@ std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechRespectingDoorsI
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechLenient(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false);
std::optional<std::vector<int8_t>> FindKeyboardWalkPathToClosestReachableForSpeech(const Player &player, Point startPosition, Point destinationPosition, Point &closestPosition);
void AppendKeyboardWalkPathForSpeech(std::string &message, const std::vector<int8_t> &path);
void AppendDirectionalFallback(std::string &message, const Displacement &delta);
void AppendDirectionalFallback(std::string &message, const Displacement &delta);
bool gbGameLoopStartup;
bool forceSpawn;
@ -666,8 +666,8 @@ void PressKey(SDL_Keycode vkey, uint16_t modState)
if (next) {
MyPlayer->_pRSpell = *next;
MyPlayer->_pRSplType = (MyPlayer->_pAblSpells & GetSpellBitmask(*next)) != 0 ? SpellType::Skill
: (MyPlayer->_pISpells & GetSpellBitmask(*next)) != 0 ? SpellType::Charges
: SpellType::Spell;
: (MyPlayer->_pISpells & GetSpellBitmask(*next)) != 0 ? SpellType::Charges
: SpellType::Spell;
UpdateSpellTarget(*next);
RedrawEverything();
SpeakText(pgettext("spell", GetSpellData(*next).sNameText), /*force=*/true);
@ -699,8 +699,8 @@ void PressKey(SDL_Keycode vkey, uint16_t modState)
if (next) {
MyPlayer->_pRSpell = *next;
MyPlayer->_pRSplType = (MyPlayer->_pAblSpells & GetSpellBitmask(*next)) != 0 ? SpellType::Skill
: (MyPlayer->_pISpells & GetSpellBitmask(*next)) != 0 ? SpellType::Charges
: SpellType::Spell;
: (MyPlayer->_pISpells & GetSpellBitmask(*next)) != 0 ? SpellType::Charges
: SpellType::Spell;
UpdateSpellTarget(*next);
RedrawEverything();
SpeakText(pgettext("spell", GetSpellData(*next).sNameText), /*force=*/true);
@ -748,8 +748,8 @@ void PressKey(SDL_Keycode vkey, uint16_t modState)
if (first) {
MyPlayer->_pRSpell = *first;
MyPlayer->_pRSplType = (MyPlayer->_pAblSpells & GetSpellBitmask(*first)) != 0 ? SpellType::Skill
: (MyPlayer->_pISpells & GetSpellBitmask(*first)) != 0 ? SpellType::Charges
: SpellType::Spell;
: (MyPlayer->_pISpells & GetSpellBitmask(*first)) != 0 ? SpellType::Charges
: SpellType::Spell;
UpdateSpellTarget(*first);
RedrawEverything();
SpeakText(pgettext("spell", GetSpellData(*first).sNameText), /*force=*/true);
@ -775,8 +775,8 @@ void PressKey(SDL_Keycode vkey, uint16_t modState)
if (first) {
MyPlayer->_pRSpell = *first;
MyPlayer->_pRSplType = (MyPlayer->_pAblSpells & GetSpellBitmask(*first)) != 0 ? SpellType::Skill
: (MyPlayer->_pISpells & GetSpellBitmask(*first)) != 0 ? SpellType::Charges
: SpellType::Spell;
: (MyPlayer->_pISpells & GetSpellBitmask(*first)) != 0 ? SpellType::Charges
: SpellType::Spell;
UpdateSpellTarget(*first);
RedrawEverything();
SpeakText(pgettext("spell", GetSpellData(*first).sNameText), /*force=*/true);
@ -1981,18 +1981,13 @@ void UpdateAttackableMonsterAnnouncements()
if (!door.isDoor())
return door.name();
// Door state values are defined in `Source/objects.cpp` (DOOR_CLOSED=0, DOOR_OPEN=1, DOOR_BLOCKED=2).
constexpr int DoorClosed = 0;
constexpr int DoorOpen = 1;
constexpr int DoorBlocked = 2;
// Catacombs doors are grates, so differentiate them for the screen reader / tracker.
if (IsAnyOf(door._otype, _object_id::OBJ_L2LDOOR, _object_id::OBJ_L2RDOOR)) {
if (door._oVar4 == DoorOpen)
if (door._oVar4 == DOOR_OPEN)
return _("Open Grate Door");
if (door._oVar4 == DoorClosed)
if (door._oVar4 == DOOR_CLOSED)
return _("Closed Grate Door");
if (door._oVar4 == DoorBlocked)
if (door._oVar4 == DOOR_BLOCKED)
return _("Blocked Grate Door");
return _("Grate Door");
}
@ -2101,10 +2096,10 @@ void GameLogic()
UpdateAutoWalkTracker();
UpdateLowDurabilityWarnings();
}
if (leveltype != DTYPE_TOWN) {
gGameLogicStep = GameLogicStep::ProcessMonsters;
#ifdef _DEBUG
if (!DebugInvisible)
if (leveltype != DTYPE_TOWN) {
gGameLogicStep = GameLogicStep::ProcessMonsters;
#ifdef _DEBUG
if (!DebugInvisible)
#endif
ProcessMonsters();
gGameLogicStep = GameLogicStep::ProcessObjects;
@ -2115,19 +2110,19 @@ void GameLogic()
ProcessItems();
ProcessLightList();
ProcessVisionList();
UpdateBossHealthAnnouncements();
UpdateProximityAudioCues();
UpdateAttackableMonsterAnnouncements();
UpdateInteractableDoorAnnouncements();
} else {
gGameLogicStep = GameLogicStep::ProcessTowners;
ProcessTowners();
gGameLogicStep = GameLogicStep::ProcessItemsTown;
ProcessItems();
gGameLogicStep = GameLogicStep::ProcessMissilesTown;
ProcessMissiles();
UpdateProximityAudioCues();
}
UpdateBossHealthAnnouncements();
UpdateProximityAudioCues();
UpdateAttackableMonsterAnnouncements();
UpdateInteractableDoorAnnouncements();
} else {
gGameLogicStep = GameLogicStep::ProcessTowners;
ProcessTowners();
gGameLogicStep = GameLogicStep::ProcessItemsTown;
ProcessItems();
gGameLogicStep = GameLogicStep::ProcessMissilesTown;
ProcessMissiles();
UpdateProximityAudioCues();
}
UpdatePlayerLowHpWarningSound();
@ -2227,20 +2222,20 @@ std::vector<int> TownNpcOrder;
int SelectedTownNpc = -1;
int AutoWalkTownNpcTarget = -1;
enum class TrackerTargetCategory : uint8_t {
Items,
Chests,
Doors,
Shrines,
Objects,
Breakables,
Monsters,
DeadBodies,
};
enum class TrackerTargetCategory : uint8_t {
Items,
Chests,
Doors,
Shrines,
Objects,
Breakables,
Monsters,
DeadBodies,
};
TrackerTargetCategory SelectedTrackerTargetCategory = TrackerTargetCategory::Items;
TrackerTargetCategory AutoWalkTrackerTargetCategory = TrackerTargetCategory::Items; ///< Category of the active auto-walk target.
int AutoWalkTrackerTargetId = -1; ///< ID of the target being auto-walked to, or -1 if inactive.
int AutoWalkTrackerTargetId = -1; ///< ID of the target being auto-walked to, or -1 if inactive.
Point NextPositionForWalkDirection(Point position, int8_t walkDir)
{
@ -2522,14 +2517,14 @@ namespace {
constexpr int TrackerInteractDistanceTiles = 1;
constexpr int TrackerCycleDistanceTiles = 12;
int LockedTrackerItemId = -1;
int LockedTrackerChestId = -1;
int LockedTrackerDoorId = -1;
int LockedTrackerShrineId = -1;
int LockedTrackerObjectId = -1;
int LockedTrackerBreakableId = -1;
int LockedTrackerMonsterId = -1;
int LockedTrackerDeadBodyId = -1;
int LockedTrackerItemId = -1;
int LockedTrackerChestId = -1;
int LockedTrackerDoorId = -1;
int LockedTrackerShrineId = -1;
int LockedTrackerObjectId = -1;
int LockedTrackerBreakableId = -1;
int LockedTrackerMonsterId = -1;
int LockedTrackerDeadBodyId = -1;
struct TrackerLevelKey {
dungeon_type levelType;
@ -2540,17 +2535,17 @@ struct TrackerLevelKey {
std::optional<TrackerLevelKey> LockedTrackerLevelKey;
void ClearTrackerLocks()
{
LockedTrackerItemId = -1;
LockedTrackerChestId = -1;
LockedTrackerDoorId = -1;
LockedTrackerShrineId = -1;
LockedTrackerObjectId = -1;
LockedTrackerBreakableId = -1;
LockedTrackerMonsterId = -1;
LockedTrackerDeadBodyId = -1;
}
void ClearTrackerLocks()
{
LockedTrackerItemId = -1;
LockedTrackerChestId = -1;
LockedTrackerDoorId = -1;
LockedTrackerShrineId = -1;
LockedTrackerObjectId = -1;
LockedTrackerBreakableId = -1;
LockedTrackerMonsterId = -1;
LockedTrackerDeadBodyId = -1;
}
void EnsureTrackerLocksMatchCurrentLevel()
{
@ -2583,13 +2578,13 @@ int &LockedTrackerTargetId(TrackerTargetCategory category)
return LockedTrackerObjectId;
case TrackerTargetCategory::Breakables:
return LockedTrackerBreakableId;
case TrackerTargetCategory::Monsters:
return LockedTrackerMonsterId;
case TrackerTargetCategory::DeadBodies:
return LockedTrackerDeadBodyId;
}
app_fatal("Invalid TrackerTargetCategory");
}
case TrackerTargetCategory::Monsters:
return LockedTrackerMonsterId;
case TrackerTargetCategory::DeadBodies:
return LockedTrackerDeadBodyId;
}
app_fatal("Invalid TrackerTargetCategory");
}
std::string_view TrackerTargetCategoryLabel(TrackerTargetCategory category)
{
@ -2606,14 +2601,14 @@ std::string_view TrackerTargetCategoryLabel(TrackerTargetCategory category)
return _("objects");
case TrackerTargetCategory::Breakables:
return _("breakables");
case TrackerTargetCategory::Monsters:
return _("monsters");
case TrackerTargetCategory::DeadBodies:
return _("dead bodies");
default:
return _("items");
}
}
case TrackerTargetCategory::Monsters:
return _("monsters");
case TrackerTargetCategory::DeadBodies:
return _("dead bodies");
default:
return _("items");
}
}
void SpeakTrackerTargetCategory()
{
@ -2632,14 +2627,14 @@ void CycleTrackerTargetKeyPressed()
const SDL_Keymod modState = SDL_GetModState();
const bool cyclePrevious = (modState & SDL_KMOD_SHIFT) != 0;
if (cyclePrevious) {
switch (SelectedTrackerTargetCategory) {
case TrackerTargetCategory::Items:
SelectedTrackerTargetCategory = TrackerTargetCategory::DeadBodies;
break;
case TrackerTargetCategory::Chests:
SelectedTrackerTargetCategory = TrackerTargetCategory::Items;
break;
if (cyclePrevious) {
switch (SelectedTrackerTargetCategory) {
case TrackerTargetCategory::Items:
SelectedTrackerTargetCategory = TrackerTargetCategory::DeadBodies;
break;
case TrackerTargetCategory::Chests:
SelectedTrackerTargetCategory = TrackerTargetCategory::Items;
break;
case TrackerTargetCategory::Doors:
SelectedTrackerTargetCategory = TrackerTargetCategory::Chests;
break;
@ -2652,17 +2647,17 @@ void CycleTrackerTargetKeyPressed()
case TrackerTargetCategory::Breakables:
SelectedTrackerTargetCategory = TrackerTargetCategory::Objects;
break;
case TrackerTargetCategory::Monsters:
SelectedTrackerTargetCategory = TrackerTargetCategory::Breakables;
break;
case TrackerTargetCategory::DeadBodies:
default:
SelectedTrackerTargetCategory = TrackerTargetCategory::Monsters;
break;
}
} else {
switch (SelectedTrackerTargetCategory) {
case TrackerTargetCategory::Items:
case TrackerTargetCategory::Monsters:
SelectedTrackerTargetCategory = TrackerTargetCategory::Breakables;
break;
case TrackerTargetCategory::DeadBodies:
default:
SelectedTrackerTargetCategory = TrackerTargetCategory::Monsters;
break;
}
} else {
switch (SelectedTrackerTargetCategory) {
case TrackerTargetCategory::Items:
SelectedTrackerTargetCategory = TrackerTargetCategory::Chests;
break;
case TrackerTargetCategory::Chests:
@ -2677,26 +2672,26 @@ void CycleTrackerTargetKeyPressed()
case TrackerTargetCategory::Objects:
SelectedTrackerTargetCategory = TrackerTargetCategory::Breakables;
break;
case TrackerTargetCategory::Breakables:
SelectedTrackerTargetCategory = TrackerTargetCategory::Monsters;
break;
case TrackerTargetCategory::Monsters:
SelectedTrackerTargetCategory = TrackerTargetCategory::DeadBodies;
break;
case TrackerTargetCategory::DeadBodies:
default:
SelectedTrackerTargetCategory = TrackerTargetCategory::Items;
break;
}
}
case TrackerTargetCategory::Breakables:
SelectedTrackerTargetCategory = TrackerTargetCategory::Monsters;
break;
case TrackerTargetCategory::Monsters:
SelectedTrackerTargetCategory = TrackerTargetCategory::DeadBodies;
break;
case TrackerTargetCategory::DeadBodies:
default:
SelectedTrackerTargetCategory = TrackerTargetCategory::Items;
break;
}
}
SpeakTrackerTargetCategory();
}
std::optional<int> FindNearestGroundItemId(Point playerPosition)
{
std::optional<int> bestId;
int bestDistance = 0;
std::optional<int> FindNearestGroundItemId(Point playerPosition)
{
std::optional<int> bestId;
int bestDistance = 0;
for (int y = 0; y < MAXDUNY; ++y) {
for (int x = 0; x < MAXDUNX; ++x) {
@ -2715,47 +2710,47 @@ std::optional<int> FindNearestGroundItemId(Point playerPosition)
}
}
}
return bestId;
}
[[nodiscard]] constexpr int CorpseTrackerIdForPosition(Point position)
{
return position.x + position.y * MAXDUNX;
}
[[nodiscard]] constexpr Point CorpsePositionForTrackerId(int corpseId)
{
return { corpseId % MAXDUNX, corpseId / MAXDUNX };
}
std::optional<int> FindNearestCorpseId(Point playerPosition)
{
std::optional<int> bestId;
int bestDistance = 0;
for (int y = 0; y < MAXDUNY; ++y) {
for (int x = 0; x < MAXDUNX; ++x) {
if (dCorpse[x][y] == 0)
continue;
const Point position { x, y };
const int distance = playerPosition.WalkingDistance(position);
if (!bestId || distance < bestDistance) {
bestId = CorpseTrackerIdForPosition(position);
bestDistance = distance;
}
}
}
return bestId;
}
struct TrackerCandidate {
int id;
int distance;
StringOrView name;
};
return bestId;
}
[[nodiscard]] constexpr int CorpseTrackerIdForPosition(Point position)
{
return position.x + position.y * MAXDUNX;
}
[[nodiscard]] constexpr Point CorpsePositionForTrackerId(int corpseId)
{
return { corpseId % MAXDUNX, corpseId / MAXDUNX };
}
std::optional<int> FindNearestCorpseId(Point playerPosition)
{
std::optional<int> bestId;
int bestDistance = 0;
for (int y = 0; y < MAXDUNY; ++y) {
for (int x = 0; x < MAXDUNX; ++x) {
if (dCorpse[x][y] == 0)
continue;
const Point position { x, y };
const int distance = playerPosition.WalkingDistance(position);
if (!bestId || distance < bestDistance) {
bestId = CorpseTrackerIdForPosition(position);
bestDistance = distance;
}
}
}
return bestId;
}
struct TrackerCandidate {
int id;
int distance;
StringOrView name;
};
[[nodiscard]] bool IsBetterTrackerCandidate(const TrackerCandidate &a, const TrackerCandidate &b)
{
@ -2801,45 +2796,45 @@ struct TrackerCandidate {
}
}
std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); });
return result;
}
[[nodiscard]] std::vector<TrackerCandidate> CollectNearbyCorpseTrackerCandidates(Point playerPosition, int maxDistance)
{
std::vector<TrackerCandidate> result;
const int minX = std::max(0, playerPosition.x - maxDistance);
const int minY = std::max(0, playerPosition.y - maxDistance);
const int maxX = std::min(MAXDUNX - 1, playerPosition.x + maxDistance);
const int maxY = std::min(MAXDUNY - 1, playerPosition.y + maxDistance);
for (int y = minY; y <= maxY; ++y) {
for (int x = minX; x <= maxX; ++x) {
if (dCorpse[x][y] == 0)
continue;
const Point position { x, y };
const int distance = playerPosition.WalkingDistance(position);
if (distance > maxDistance)
continue;
result.push_back(TrackerCandidate {
.id = CorpseTrackerIdForPosition(position),
.distance = distance,
.name = _("Dead body"),
});
}
}
std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); });
return result;
}
[[nodiscard]] constexpr bool IsTrackedChestObject(const Object &object)
{
return object.canInteractWith() && (object.IsChest() || object._otype == _object_id::OBJ_SIGNCHEST);
}
std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); });
return result;
}
[[nodiscard]] std::vector<TrackerCandidate> CollectNearbyCorpseTrackerCandidates(Point playerPosition, int maxDistance)
{
std::vector<TrackerCandidate> result;
const int minX = std::max(0, playerPosition.x - maxDistance);
const int minY = std::max(0, playerPosition.y - maxDistance);
const int maxX = std::min(MAXDUNX - 1, playerPosition.x + maxDistance);
const int maxY = std::min(MAXDUNY - 1, playerPosition.y + maxDistance);
for (int y = minY; y <= maxY; ++y) {
for (int x = minX; x <= maxX; ++x) {
if (dCorpse[x][y] == 0)
continue;
const Point position { x, y };
const int distance = playerPosition.WalkingDistance(position);
if (distance > maxDistance)
continue;
result.push_back(TrackerCandidate {
.id = CorpseTrackerIdForPosition(position),
.distance = distance,
.name = _("Dead body"),
});
}
}
std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); });
return result;
}
[[nodiscard]] constexpr bool IsTrackedChestObject(const Object &object)
{
return object.canInteractWith() && (object.IsChest() || object._otype == _object_id::OBJ_SIGNCHEST);
}
[[nodiscard]] constexpr bool IsTrackedDoorObject(const Object &object)
{
@ -3085,32 +3080,32 @@ void DecorateTrackerTargetNameWithOrdinalIfNeeded(int targetId, StringOrView &ta
targetName = std::move(decorated);
}
[[nodiscard]] bool IsGroundItemPresent(int itemId)
{
if (itemId < 0 || itemId > MAXITEMS)
return false;
[[nodiscard]] bool IsGroundItemPresent(int itemId)
{
if (itemId < 0 || itemId > MAXITEMS)
return false;
for (uint8_t i = 0; i < ActiveItemCount; ++i) {
if (ActiveItems[i] == itemId)
return true;
}
return false;
}
[[nodiscard]] bool IsCorpsePresent(int corpseId)
{
if (corpseId < 0 || corpseId >= MAXDUNX * MAXDUNY)
return false;
const Point position = CorpsePositionForTrackerId(corpseId);
return InDungeonBounds(position) && dCorpse[position.x][position.y] != 0;
}
std::optional<int> FindNearestUnopenedChestObjectId(Point playerPosition)
{
return FindNearestObjectId(playerPosition, IsTrackedChestObject);
}
return false;
}
[[nodiscard]] bool IsCorpsePresent(int corpseId)
{
if (corpseId < 0 || corpseId >= MAXDUNX * MAXDUNY)
return false;
const Point position = CorpsePositionForTrackerId(corpseId);
return InDungeonBounds(position) && dCorpse[position.x][position.y] != 0;
}
std::optional<int> FindNearestUnopenedChestObjectId(Point playerPosition)
{
return FindNearestObjectId(playerPosition, IsTrackedChestObject);
}
std::optional<int> FindNearestDoorObjectId(Point playerPosition)
{
@ -3353,8 +3348,9 @@ std::optional<DoorBlockInfo> FindFirstClosedDoorOnWalkPath(Point startPosition,
for (int i = 0; i < steps; ++i) {
const Point next = NextPositionForWalkDirection(position, path[i]);
Object *object = FindObjectAtPosition(next);
if (object != nullptr && object->isDoor() && object->_oSolidFlag) {
return DoorBlockInfo { .beforeDoor = position, .doorPosition = next };
// Only closed doors block the path; blocked doors (DOOR_BLOCKED) are physically passable.
if (object != nullptr && object->isDoor() && object->_oVar4 == DOOR_CLOSED) {
return DoorBlockInfo { .beforeDoor = position, .doorPosition = object->position };
}
position = next;
}
@ -3385,12 +3381,13 @@ struct TrackerPathBlockInfo {
}
Object *object = FindObjectAtPosition(next);
if (considerDoors && object != nullptr && object->isDoor() && object->_oSolidFlag) {
// Only closed doors block the path; blocked doors (DOOR_BLOCKED) are physically passable.
if (considerDoors && object != nullptr && object->isDoor() && object->_oVar4 == DOOR_CLOSED) {
return TrackerPathBlockInfo {
.type = TrackerPathBlockType::Door,
.stepIndex = i,
.beforeBlock = position,
.blockPosition = next,
.blockPosition = object->position,
};
}
if (considerBreakables && object != nullptr && object->_oSolidFlag && object->IsBreakable()) {
@ -3422,14 +3419,14 @@ struct TrackerPathBlockInfo {
return std::nullopt;
}
void NavigateToTrackerTargetKeyPressed()
{
if (!CanPlayerTakeAction() || InGameMenu())
return;
if (leveltype == DTYPE_TOWN && IsNoneOf(SelectedTrackerTargetCategory, TrackerTargetCategory::Items, TrackerTargetCategory::DeadBodies)) {
SpeakText(_("Not in a dungeon."), true);
return;
}
void NavigateToTrackerTargetKeyPressed()
{
if (!CanPlayerTakeAction() || InGameMenu())
return;
if (leveltype == DTYPE_TOWN && IsNoneOf(SelectedTrackerTargetCategory, TrackerTargetCategory::Items, TrackerTargetCategory::DeadBodies)) {
SpeakText(_("Not in a dungeon."), true);
return;
}
if (AutomapActive) {
SpeakText(_("Close the map first."), true);
return;
@ -3714,10 +3711,10 @@ void NavigateToTrackerTargetKeyPressed()
}
break;
}
case TrackerTargetCategory::Monsters: {
const std::vector<TrackerCandidate> nearbyCandidates = CollectNearbyMonsterTrackerCandidates(playerPosition, TrackerCycleDistanceTiles);
if (cycleTarget) {
targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId);
case TrackerTargetCategory::Monsters: {
const std::vector<TrackerCandidate> nearbyCandidates = CollectNearbyMonsterTrackerCandidates(playerPosition, TrackerCycleDistanceTiles);
if (cycleTarget) {
targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId);
if (!targetId) {
if (nearbyCandidates.empty())
SpeakText(_("No monsters found."), true);
@ -3750,51 +3747,51 @@ void NavigateToTrackerTargetKeyPressed()
targetName = tracked.name();
DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates);
if (!cycleTarget) {
targetPosition = tracked.position.tile;
}
break;
}
case TrackerTargetCategory::DeadBodies: {
const std::vector<TrackerCandidate> nearbyCandidates = CollectNearbyCorpseTrackerCandidates(playerPosition, TrackerCycleDistanceTiles);
if (cycleTarget) {
targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId);
if (!targetId) {
if (nearbyCandidates.empty())
SpeakText(_("No dead bodies found."), true);
else
SpeakText(_("No next dead body."), true);
return;
}
} else if (IsCorpsePresent(lockedTargetId)) {
targetId = lockedTargetId;
} else {
targetId = FindNearestCorpseId(playerPosition);
}
if (!targetId) {
SpeakText(_("No dead bodies found."), true);
return;
}
if (!IsCorpsePresent(*targetId)) {
lockedTargetId = -1;
SpeakText(_("No dead bodies found."), true);
return;
}
lockedTargetId = *targetId;
targetName = _("Dead body");
DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates);
if (!cycleTarget) {
targetPosition = CorpsePositionForTrackerId(*targetId);
}
break;
}
}
if (cycleTarget) {
SpeakText(targetName.str(), /*force=*/true);
return;
if (!cycleTarget) {
targetPosition = tracked.position.tile;
}
break;
}
case TrackerTargetCategory::DeadBodies: {
const std::vector<TrackerCandidate> nearbyCandidates = CollectNearbyCorpseTrackerCandidates(playerPosition, TrackerCycleDistanceTiles);
if (cycleTarget) {
targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId);
if (!targetId) {
if (nearbyCandidates.empty())
SpeakText(_("No dead bodies found."), true);
else
SpeakText(_("No next dead body."), true);
return;
}
} else if (IsCorpsePresent(lockedTargetId)) {
targetId = lockedTargetId;
} else {
targetId = FindNearestCorpseId(playerPosition);
}
if (!targetId) {
SpeakText(_("No dead bodies found."), true);
return;
}
if (!IsCorpsePresent(*targetId)) {
lockedTargetId = -1;
SpeakText(_("No dead bodies found."), true);
return;
}
lockedTargetId = *targetId;
targetName = _("Dead body");
DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates);
if (!cycleTarget) {
targetPosition = CorpsePositionForTrackerId(*targetId);
}
break;
}
}
if (cycleTarget) {
SpeakText(targetName.str(), /*force=*/true);
return;
}
if (!targetPosition) {
@ -4087,10 +4084,10 @@ void UpdateAutoWalkTracker()
if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, IsTrackedBreakableObject, N_("Target breakable is gone."), N_("Breakable in range."), destination))
return;
break;
case TrackerTargetCategory::Monsters: {
const int monsterId = AutoWalkTrackerTargetId;
if (monsterId < 0 || monsterId >= static_cast<int>(MaxMonsters)) {
AutoWalkTrackerTargetId = -1;
case TrackerTargetCategory::Monsters: {
const int monsterId = AutoWalkTrackerTargetId;
if (monsterId < 0 || monsterId >= static_cast<int>(MaxMonsters)) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target monster is gone."), true);
return;
}
@ -4105,29 +4102,29 @@ void UpdateAutoWalkTracker()
AutoWalkTrackerTargetId = -1;
SpeakText(_("Monster in range."), true);
return;
}
destination = FindBestAdjacentApproachTile(myPlayer, playerPosition, monsterPosition);
break;
}
case TrackerTargetCategory::DeadBodies: {
const int corpseId = AutoWalkTrackerTargetId;
if (!IsCorpsePresent(corpseId)) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target dead body is gone."), true);
return;
}
const Point corpsePosition = CorpsePositionForTrackerId(corpseId);
if (playerPosition.WalkingDistance(corpsePosition) <= TrackerInteractDistanceTiles) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Dead body in range."), true);
return;
}
destination = corpsePosition;
break;
}
}
}
destination = FindBestAdjacentApproachTile(myPlayer, playerPosition, monsterPosition);
break;
}
case TrackerTargetCategory::DeadBodies: {
const int corpseId = AutoWalkTrackerTargetId;
if (!IsCorpsePresent(corpseId)) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target dead body is gone."), true);
return;
}
const Point corpsePosition = CorpsePositionForTrackerId(corpseId);
if (playerPosition.WalkingDistance(corpsePosition) <= TrackerInteractDistanceTiles) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Dead body in range."), true);
return;
}
destination = corpsePosition;
break;
}
}
if (!destination) {
AutoWalkTrackerTargetId = -1;
@ -4246,49 +4243,34 @@ void AutoWalkToTrackerTargetKeyPressed()
break;
}
case TrackerTargetCategory::Chests:
targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition,
IsTrackedChestObject, FindNearestUnopenedChestObjectId,
[](int id) -> StringOrView { return Objects[id].name(); },
N_("No chests found."), targetName);
targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedChestObject, FindNearestUnopenedChestObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No chests found."), targetName);
if (!targetId)
return;
break;
case TrackerTargetCategory::Doors:
targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition,
IsTrackedDoorObject, FindNearestDoorObjectId,
[](int id) -> StringOrView { return DoorLabelForSpeech(Objects[id]); },
N_("No doors found."), targetName);
targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedDoorObject, FindNearestDoorObjectId, [](int id) -> StringOrView { return DoorLabelForSpeech(Objects[id]); }, N_("No doors found."), targetName);
if (!targetId)
return;
break;
case TrackerTargetCategory::Shrines:
targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition,
IsShrineLikeObject, FindNearestShrineObjectId,
[](int id) -> StringOrView { return Objects[id].name(); },
N_("No shrines found."), targetName);
targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsShrineLikeObject, FindNearestShrineObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No shrines found."), targetName);
if (!targetId)
return;
break;
case TrackerTargetCategory::Objects:
targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition,
IsTrackedMiscInteractableObject, FindNearestMiscInteractableObjectId,
[](int id) -> StringOrView { return Objects[id].name(); },
N_("No objects found."), targetName);
targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedMiscInteractableObject, FindNearestMiscInteractableObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No objects found."), targetName);
if (!targetId)
return;
break;
case TrackerTargetCategory::Breakables:
targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition,
IsTrackedBreakableObject, FindNearestBreakableObjectId,
[](int id) -> StringOrView { return Objects[id].name(); },
N_("No breakables found."), targetName);
targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedBreakableObject, FindNearestBreakableObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No breakables found."), targetName);
if (!targetId)
return;
break;
case TrackerTargetCategory::Monsters: {
if (lockedTargetId >= 0 && lockedTargetId < static_cast<int>(MaxMonsters)) {
targetId = lockedTargetId;
} else {
case TrackerTargetCategory::Monsters: {
if (lockedTargetId >= 0 && lockedTargetId < static_cast<int>(MaxMonsters)) {
targetId = lockedTargetId;
} else {
targetId = FindNearestMonsterId(playerPosition);
}
if (!targetId) {
@ -4304,30 +4286,30 @@ void AutoWalkToTrackerTargetKeyPressed()
return;
}
}
lockedTargetId = *targetId;
targetName = Monsters[*targetId].name();
break;
}
case TrackerTargetCategory::DeadBodies: {
if (IsCorpsePresent(lockedTargetId)) {
targetId = lockedTargetId;
} else {
targetId = FindNearestCorpseId(playerPosition);
}
if (!targetId) {
SpeakText(_("No dead bodies found."), true);
return;
}
if (!IsCorpsePresent(*targetId)) {
lockedTargetId = -1;
SpeakText(_("No dead bodies found."), true);
return;
}
lockedTargetId = *targetId;
targetName = _("Dead body");
break;
}
}
lockedTargetId = *targetId;
targetName = Monsters[*targetId].name();
break;
}
case TrackerTargetCategory::DeadBodies: {
if (IsCorpsePresent(lockedTargetId)) {
targetId = lockedTargetId;
} else {
targetId = FindNearestCorpseId(playerPosition);
}
if (!targetId) {
SpeakText(_("No dead bodies found."), true);
return;
}
if (!IsCorpsePresent(*targetId)) {
lockedTargetId = -1;
SpeakText(_("No dead bodies found."), true);
return;
}
lockedTargetId = *targetId;
targetName = _("Dead body");
break;
}
}
std::string msg;
StrAppend(msg, _("Going to: "), targetName);
@ -4483,8 +4465,7 @@ std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechBfs(const Player
const int8_t yDir = delta.deltaY > 0 ? WALK_SW : (delta.deltaY < 0 ? WALK_NE : WALK_NONE);
if (allowDiagonalSteps && delta.deltaX != 0 && delta.deltaY != 0) {
const int8_t diagDir =
delta.deltaX > 0 ? (delta.deltaY > 0 ? WALK_S : WALK_E) : (delta.deltaY > 0 ? WALK_W : WALK_N);
const int8_t diagDir = delta.deltaX > 0 ? (delta.deltaY > 0 ? WALK_S : WALK_E) : (delta.deltaY > 0 ? WALK_W : WALK_N);
addUniqueDir(diagDir);
}
@ -4667,8 +4648,7 @@ std::optional<std::vector<int8_t>> FindKeyboardWalkPathToClosestReachableForSpee
const int8_t yDir = delta.deltaY > 0 ? WALK_SW : (delta.deltaY < 0 ? WALK_NE : WALK_NONE);
if (allowDiagonalSteps && delta.deltaX != 0 && delta.deltaY != 0) {
const int8_t diagDir =
delta.deltaX > 0 ? (delta.deltaY > 0 ? WALK_S : WALK_E) : (delta.deltaY > 0 ? WALK_W : WALK_N);
const int8_t diagDir = delta.deltaX > 0 ? (delta.deltaY > 0 ? WALK_S : WALK_E) : (delta.deltaY > 0 ? WALK_W : WALK_N);
addUniqueDir(diagDir);
}
@ -5303,7 +5283,7 @@ void SpeakNearestExitKeyPressed()
}
const int triggerIndex = FindLockedTownDungeonTriggerIndex(dungeonCandidates)
.value_or(FindDefaultTownDungeonTriggerIndex(dungeonCandidates).value_or(dungeonCandidates.front()));
.value_or(FindDefaultTownDungeonTriggerIndex(dungeonCandidates).value_or(dungeonCandidates.front()));
LockedTownDungeonTriggerIndex = triggerIndex;
const TriggerStruct &trigger = trigs[triggerIndex];
@ -6062,14 +6042,14 @@ void InitKeymapActions()
SpeakNearestStairsUpKeyPressed,
nullptr,
[]() { return CanPlayerTakeAction() && leveltype != DTYPE_TOWN; });
options.Keymapper.AddAction(
"CycleTrackerTarget",
N_("Cycle tracker target"),
N_("Cycles what the tracker looks for (items, chests, doors, shrines, objects, breakables, monsters, dead bodies). Hold Shift to cycle backwards."),
'T',
CycleTrackerTargetKeyPressed,
nullptr,
[]() { return CanPlayerTakeAction() && !InGameMenu(); });
options.Keymapper.AddAction(
"CycleTrackerTarget",
N_("Cycle tracker target"),
N_("Cycles what the tracker looks for (items, chests, doors, shrines, objects, breakables, monsters, dead bodies). Hold Shift to cycle backwards."),
'T',
CycleTrackerTargetKeyPressed,
nullptr,
[]() { return CanPlayerTakeAction() && !InGameMenu(); });
options.Keymapper.AddAction(
"NavigateToTrackerTarget",
N_("Tracker directions"),

19
Source/objects.cpp

@ -3,6 +3,7 @@
*
* Implementation of object functionality, interaction, spawning, loading, etc.
*/
#include <cassert>
#include <climits>
#include <cmath>
#include <cstdint>
@ -100,14 +101,6 @@ enum shrine_type : uint8_t {
NumberOfShrineTypes
};
enum {
// clang-format off
DOOR_CLOSED = 0,
DOOR_OPEN = 1,
DOOR_BLOCKED = 2,
// clang-format on
};
int trapid;
int trapdir;
OptionalOwnedClxSpriteList pObjCels[40];
@ -1182,11 +1175,19 @@ void AddDoor(Object &door)
case OBJ_L5LDOOR:
door._oVar1 = dPiece[door.position.x][door.position.y] + 1;
door._oVar2 = dPiece[door.position.x][door.position.y - 1] + 1;
// Register the archway tile so FindObjectAtPosition resolves it to this door,
// enabling auto-walk door detection and IsTileWalkable with ignoreDoors.
assert(door.position.y > 0);
dObject[door.position.x][door.position.y - 1] = -(static_cast<int8_t>(door.GetId()) + 1);
break;
case OBJ_L1RDOOR:
case OBJ_L5RDOOR:
door._oVar1 = dPiece[door.position.x][door.position.y] + 1;
door._oVar2 = dPiece[door.position.x - 1][door.position.y] + 1;
// Register the archway tile so FindObjectAtPosition resolves it to this door,
// enabling auto-walk door detection and IsTileWalkable with ignoreDoors.
assert(door.position.x > 0);
dObject[door.position.x - 1][door.position.y] = -(static_cast<int8_t>(door.GetId()) + 1);
break;
default:
break;
@ -4303,7 +4304,7 @@ void MonstCheckDoors(const Monster &monster)
continue;
Object &door = *object;
// Doors use _oVar4 to track open/closed state, non-zero values indicate an open door
// Doors use _oVar4 to track state (DOOR_CLOSED, DOOR_OPEN, or DOOR_BLOCKED); skip non-closed doors
if (!door.isDoor() || door._oVar4 != DOOR_CLOSED)
continue;

11
Source/objects.h

@ -29,6 +29,17 @@ namespace devilution {
#define MAXOBJECTS 127
static_assert(MAXOBJECTS <= 127, "MAXOBJECTS must fit in int8_t for the dObject encoding scheme");
/** Door state values stored in Object::_oVar4 for door-type objects. */
enum {
// clang-format off
DOOR_CLOSED = 0,
DOOR_OPEN = 1,
DOOR_BLOCKED = 2,
// clang-format on
};
struct Object {
_object_id _otype = OBJ_NULL;
bool applyLighting = false;

23
test/tile_properties_test.cpp

@ -56,6 +56,29 @@ TEST(TilePropertiesTest, Walkable)
EXPECT_TRUE(IsTileWalkable({ 5, 5 }, true)) << "Solid tiles occupied by an open door become walkable when ignoring doors";
}
TEST(TilePropertiesTest, DoorArchwaySolidTileBecomesWalkableWhenIgnoringDoors)
{
dPiece[5][4] = 0;
SOLData[0] = TileProperties::Solid;
dObject[5][4] = 0;
EXPECT_FALSE(IsTileWalkable({ 5, 4 }, true))
<< "Solid tile with no object is unwalkable even when ignoring doors";
Objects[0]._otype = _object_id::OBJ_L1LDOOR;
Objects[0]._oSolidFlag = false;
dObject[5][5] = 1;
dObject[5][4] = -1; // Negative dObject value: extended area of Objects[0] (the door)
EXPECT_TRUE(IsTileWalkable({ 5, 4 }, true))
<< "Solid archway tile referencing a door becomes walkable when ignoring doors";
EXPECT_FALSE(IsTileWalkable({ 5, 4 }))
<< "Solid archway tile referencing a door is still unwalkable normally";
// Cleanup
dObject[5][5] = 0;
dObject[5][4] = 0;
Objects[0] = {};
}
TEST(TilePropertiesTest, CanStepTest)
{
dPiece[0][0] = 0;

Loading…
Cancel
Save