diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 842ed6bf0..9dbece3d7 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -5,8 +5,8 @@ */ #include #include -#include #include +#include #include #include #include @@ -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> FindKeyboardWalkPathForSpeech(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); std::optional> FindKeyboardWalkPathForSpeechRespectingDoors(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); std::optional> FindKeyboardWalkPathForSpeechIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); @@ -189,7 +189,7 @@ std::optional> FindKeyboardWalkPathForSpeechRespectingDoorsI std::optional> FindKeyboardWalkPathForSpeechLenient(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); std::optional> FindKeyboardWalkPathToClosestReachableForSpeech(const Player &player, Point startPosition, Point destinationPosition, Point &closestPosition); void AppendKeyboardWalkPathForSpeech(std::string &message, const std::vector &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 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 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 FindNearestGroundItemId(Point playerPosition) -{ - std::optional bestId; - int bestDistance = 0; +std::optional FindNearestGroundItemId(Point playerPosition) +{ + std::optional bestId; + int bestDistance = 0; for (int y = 0; y < MAXDUNY; ++y) { for (int x = 0; x < MAXDUNX; ++x) { @@ -2715,47 +2710,47 @@ std::optional 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 FindNearestCorpseId(Point playerPosition) -{ - std::optional 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 FindNearestCorpseId(Point playerPosition) +{ + std::optional 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 CollectNearbyCorpseTrackerCandidates(Point playerPosition, int maxDistance) -{ - std::vector 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 CollectNearbyCorpseTrackerCandidates(Point playerPosition, int maxDistance) +{ + std::vector 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 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 FindNearestUnopenedChestObjectId(Point playerPosition) +{ + return FindNearestObjectId(playerPosition, IsTrackedChestObject); +} std::optional FindNearestDoorObjectId(Point playerPosition) { @@ -3353,8 +3348,9 @@ std::optional 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 nearbyCandidates = CollectNearbyMonsterTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + case TrackerTargetCategory::Monsters: { + const std::vector 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 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 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(MaxMonsters)) { - AutoWalkTrackerTargetId = -1; + case TrackerTargetCategory::Monsters: { + const int monsterId = AutoWalkTrackerTargetId; + if (monsterId < 0 || monsterId >= static_cast(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(MaxMonsters)) { - targetId = lockedTargetId; - } else { + case TrackerTargetCategory::Monsters: { + if (lockedTargetId >= 0 && lockedTargetId < static_cast(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> 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> 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"), diff --git a/Source/objects.cpp b/Source/objects.cpp index b489fa3d7..9f0706bee 100644 --- a/Source/objects.cpp +++ b/Source/objects.cpp @@ -3,6 +3,7 @@ * * Implementation of object functionality, interaction, spawning, loading, etc. */ +#include #include #include #include @@ -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(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(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; diff --git a/Source/objects.h b/Source/objects.h index c9032454d..7712fdc67 100644 --- a/Source/objects.h +++ b/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; diff --git a/test/tile_properties_test.cpp b/test/tile_properties_test.cpp index 539a41ad7..9dbb40dac 100644 --- a/test/tile_properties_test.cpp +++ b/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;