From eff6150ab03ba02e25addca830a1a557030f4340 Mon Sep 17 00:00:00 2001 From: mojsior Date: Mon, 9 Feb 2026 11:29:00 +0100 Subject: [PATCH] access: unique stats, item quality, Lazarus exit, stash focus --- Source/controls/plrctrls.cpp | 54 ++++++-- Source/controls/plrctrls.h | 1 + Source/controls/tracker.cpp | 249 ++++++++++++++++++++++------------- Source/diablo.cpp | 10 +- Source/items.cpp | 29 ++-- Translations/pl.po | 18 +++ 6 files changed, 250 insertions(+), 111 deletions(-) diff --git a/Source/controls/plrctrls.cpp b/Source/controls/plrctrls.cpp index 7444a0710..1c28595bd 100644 --- a/Source/controls/plrctrls.cpp +++ b/Source/controls/plrctrls.cpp @@ -2254,18 +2254,48 @@ void InvalidateInventorySlot() /** * @brief Moves the mouse to the first inventory slot. */ -void FocusOnInventory() -{ - Slot = SLOTXY_INV_FIRST; - ResetInvCursorPosition(); - SpeakInventorySlotForAccessibility(); -} - -void InventoryMoveFromKeyboard(AxisDirection dir) -{ - if (!invflag) - return; - +void FocusOnInventory() +{ + Slot = SLOTXY_INV_FIRST; + ResetInvCursorPosition(); + SpeakInventorySlotForAccessibility(); +} + +void ToggleStashFocus() +{ + if (!IsStashOpen || MyPlayer == nullptr) + return; + + const Item &holdItem = MyPlayer->HoldItem; + + // If currently focused on inventory/belt, jump to stash. Otherwise jump back to inventory. + if (Slot >= 0) { + BeltReturnsToStash = false; + Slot = -1; + ActiveStashSlot = FindClosestStashSlot(MousePosition); + if (ActiveStashSlot == InvalidStashPoint) + ActiveStashSlot = { 0, 0 }; + + Point mousePos = GetStashSlotCoord(ActiveStashSlot); + Size itemSize = holdItem.isEmpty() ? Size { 1, 1 } : GetInventorySize(holdItem); + mousePos += Displacement { itemSize.width * INV_SLOT_HALF_SIZE_PX, itemSize.height * INV_SLOT_HALF_SIZE_PX }; + SetCursorPos(mousePos); + SpeakText(_("Stash"), /*force=*/true); + return; + } + + BeltReturnsToStash = false; + ActiveStashSlot = InvalidStashPoint; + Slot = FindClosestInventorySlot(MousePosition, holdItem); + ResetInvCursorPosition(); + SpeakInventorySlotForAccessibility(); +} + +void InventoryMoveFromKeyboard(AxisDirection dir) +{ + if (!invflag) + return; + CheckInventoryMove(dir); } diff --git a/Source/controls/plrctrls.h b/Source/controls/plrctrls.h index eeba6dd98..341839d6f 100644 --- a/Source/controls/plrctrls.h +++ b/Source/controls/plrctrls.h @@ -71,6 +71,7 @@ void UpdateSpellTarget(SpellID spell); bool TryDropItem(); void InvalidateInventorySlot(); void FocusOnInventory(); +void ToggleStashFocus(); void InventoryMoveFromKeyboard(AxisDirection dir); void HotSpellMove(AxisDirection dir); void PerformSpellAction(); diff --git a/Source/controls/tracker.cpp b/Source/controls/tracker.cpp index aca8c3571..3363981a3 100644 --- a/Source/controls/tracker.cpp +++ b/Source/controls/tracker.cpp @@ -280,17 +280,51 @@ struct TrackerCandidate { StringOrView name; }; -[[nodiscard]] bool IsBetterTrackerCandidate(const TrackerCandidate &a, const TrackerCandidate &b) -{ - if (a.distance != b.distance) - return a.distance < b.distance; - return a.id < b.id; -} - -[[nodiscard]] std::vector CollectNearbyItemTrackerCandidates(Point playerPosition, int maxDistance) -{ - std::vector result; - result.reserve(ActiveItemCount); +[[nodiscard]] bool IsBetterTrackerCandidate(const TrackerCandidate &a, const TrackerCandidate &b) +{ + if (a.distance != b.distance) + return a.distance < b.distance; + return a.id < b.id; +} + +[[nodiscard]] constexpr int RedPortalTrackerIdForPosition(Point position) +{ + // Encode tile position into a stable negative id. + // MAXDUNX/MAXDUNY are 112, so this easily fits in int. + return -((position.y * MAXDUNX) + position.x + 1); +} + +[[nodiscard]] constexpr bool IsRedPortalTrackerId(int id) +{ + return id < 0; +} + +[[nodiscard]] constexpr Point RedPortalPositionForTrackerId(int id) +{ + const int encoded = -id - 1; + return { encoded % MAXDUNX, encoded / MAXDUNX }; +} + +[[nodiscard]] StringOrView ItemLabelForSpeech(const Item &item) +{ + const StringOrView name = item.getName(); + if (name.empty()) + return name; + + switch (item._iMagical) { + case ITEM_QUALITY_MAGIC: + return StrCat(name, ", ", _("magic item")); + case ITEM_QUALITY_UNIQUE: + return StrCat(name, ", ", _("unique item")); + default: + return name; + } +} + +[[nodiscard]] std::vector CollectNearbyItemTrackerCandidates(Point playerPosition, int maxDistance) +{ + std::vector result; + result.reserve(ActiveItemCount); const int minX = std::max(0, playerPosition.x - maxDistance); const int minY = std::max(0, playerPosition.y - maxDistance); @@ -316,14 +350,14 @@ struct TrackerCandidate { if (distance > maxDistance) continue; - result.push_back(TrackerCandidate { - .id = itemId, - .distance = distance, - .name = item.getName(), - }); - } - } - + result.push_back(TrackerCandidate { + .id = itemId, + .distance = distance, + .name = ItemLabelForSpeech(item), + }); + } + } + std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); return result; } @@ -644,11 +678,11 @@ template return result; } - for (int i = 0; i < numtrigs; ++i) { - const TriggerStruct &trigger = trigs[i]; - if (setlevel) { - if (trigger._tmsg != WM_DIABRTNLVL) - continue; + for (int i = 0; i < numtrigs; ++i) { + const TriggerStruct &trigger = trigs[i]; + if (setlevel) { + if (trigger._tmsg != WM_DIABRTNLVL) + continue; } else { if (!IsAnyOf(trigger._tmsg, WM_DIABPREVLVL, WM_DIABTWARPUP)) continue; @@ -659,13 +693,31 @@ template result.push_back(TrackerCandidate { .id = i, .distance = distance, - .name = TriggerLabelForSpeech(trigger), - }); - } - - std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); - return result; -} + .name = TriggerLabelForSpeech(trigger), + }); + } + + // Lazarus' set level (SL_VILEBETRAYER) uses a RedPortal missile instead of a return trigger. + // Include it so the player can navigate out like other quest levels. + if (setlevel) { + for (const Missile &missile : Missiles) { + if (missile._mitype != MissileID::RedPortal) + continue; + const Point portalPosition = missile.position.tile; + if (!InDungeonBounds(portalPosition)) + continue; + const int distance = playerPosition.WalkingDistance(portalPosition); + result.push_back(TrackerCandidate { + .id = RedPortalTrackerIdForPosition(portalPosition), + .distance = distance, + .name = _("Red portal"), + }); + } + } + + std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); + return result; +} [[nodiscard]] std::optional FindTownPortalPositionInTownByPortalIndex(int portalIndex) { @@ -774,24 +826,39 @@ template if (MyPlayer == nullptr || leveltype == DTYPE_TOWN) return result; - if (setlevel) { - for (int i = 0; i < numtrigs; ++i) { - const TriggerStruct &trigger = trigs[i]; - if (trigger._tmsg != WM_DIABRTNLVL) - continue; + if (setlevel) { + for (int i = 0; i < numtrigs; ++i) { + const TriggerStruct &trigger = trigs[i]; + if (trigger._tmsg != WM_DIABRTNLVL) + continue; const Point triggerPosition { trigger.position.x, trigger.position.y }; const int distance = playerPosition.WalkingDistance(triggerPosition); result.push_back(TrackerCandidate { .id = i, .distance = distance, - .name = TriggerLabelForSpeech(trigger), - }); - } - - std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); - return result; - } + .name = TriggerLabelForSpeech(trigger), + }); + } + + // Lazarus' set level (SL_VILEBETRAYER) uses a RedPortal missile instead of a return trigger. + for (const Missile &missile : Missiles) { + if (missile._mitype != MissileID::RedPortal) + continue; + const Point portalPosition = missile.position.tile; + if (!InDungeonBounds(portalPosition)) + continue; + const int distance = playerPosition.WalkingDistance(portalPosition); + result.push_back(TrackerCandidate { + .id = RedPortalTrackerIdForPosition(portalPosition), + .distance = distance, + .name = _("Red portal"), + }); + } + + std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); + return result; + } constexpr size_t NumQuests = sizeof(Quests) / sizeof(Quests[0]); result.reserve(NumQuests); @@ -1977,10 +2044,10 @@ void NavigateToTrackerTargetKeyPressed() } break; } - case TrackerTargetCategory::DungeonEntrances: { - const std::vector nearbyCandidates = CollectDungeonEntranceTrackerCandidates(playerPosition); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + case TrackerTargetCategory::DungeonEntrances: { + const std::vector nearbyCandidates = CollectDungeonEntranceTrackerCandidates(playerPosition); + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); if (!targetId) { if (nearbyCandidates.empty()) SpeakText(_("No dungeon entrances found."), true); @@ -1988,32 +2055,35 @@ void NavigateToTrackerTargetKeyPressed() SpeakText(_("No next dungeon entrance."), true); return; } - } else if (lockedTargetId >= 0 && lockedTargetId < numtrigs) { - targetId = lockedTargetId; - } else if (!nearbyCandidates.empty()) { - targetId = nearbyCandidates.front().id; - } - if (!targetId) { - SpeakText(_("No dungeon entrances found."), true); - return; - } + } else if (!nearbyCandidates.empty()) { + const auto lockedIt = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; }); + targetId = lockedIt != nearbyCandidates.end() ? lockedTargetId : nearbyCandidates.front().id; + } + if (!targetId) { + SpeakText(_("No dungeon entrances found."), true); + return; + } const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); if (it == nearbyCandidates.end()) { lockedTargetId = -1; SpeakText(_("No dungeon entrances found."), true); return; - } - - lockedTargetId = *targetId; - targetName = TriggerLabelForSpeech(trigs[*targetId]); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - if (!cycleTarget) { - const TriggerStruct &trigger = trigs[*targetId]; - targetPosition = Point { trigger.position.x, trigger.position.y }; - } - break; - } + } + + lockedTargetId = *targetId; + targetName = std::string(it->name.str()); + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + if (!cycleTarget) { + if (IsRedPortalTrackerId(*targetId)) { + targetPosition = RedPortalPositionForTrackerId(*targetId); + } else { + const TriggerStruct &trigger = trigs[*targetId]; + targetPosition = Point { trigger.position.x, trigger.position.y }; + } + } + break; + } case TrackerTargetCategory::Stairs: { const std::vector nearbyCandidates = CollectStairsTrackerCandidates(playerPosition); if (cycleTarget) { @@ -2051,10 +2121,10 @@ void NavigateToTrackerTargetKeyPressed() } break; } - case TrackerTargetCategory::QuestLocations: { - const std::vector nearbyCandidates = CollectQuestLocationTrackerCandidates(playerPosition); - if (cycleTarget) { - targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); + case TrackerTargetCategory::QuestLocations: { + const std::vector nearbyCandidates = CollectQuestLocationTrackerCandidates(playerPosition); + if (cycleTarget) { + targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); if (!targetId) { if (nearbyCandidates.empty()) SpeakText(_("No quest locations found."), true); @@ -2062,14 +2132,13 @@ void NavigateToTrackerTargetKeyPressed() SpeakText(_("No next quest location."), true); return; } - } else if ((setlevel && lockedTargetId >= 0 && lockedTargetId < numtrigs) || (!setlevel && lockedTargetId >= 0 && lockedTargetId < static_cast(sizeof(Quests) / sizeof(Quests[0])))) { - targetId = lockedTargetId; - } else if (!nearbyCandidates.empty()) { - targetId = nearbyCandidates.front().id; - } - if (!targetId) { - SpeakText(_("No quest locations found."), true); - return; + } else if (!nearbyCandidates.empty()) { + const auto lockedIt = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; }); + targetId = lockedIt != nearbyCandidates.end() ? lockedTargetId : nearbyCandidates.front().id; + } + if (!targetId) { + SpeakText(_("No quest locations found."), true); + return; } const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); @@ -2081,15 +2150,19 @@ void NavigateToTrackerTargetKeyPressed() lockedTargetId = *targetId; targetName = std::string(it->name.str()); - DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); - if (!cycleTarget) { - if (setlevel) { - const TriggerStruct &trigger = trigs[*targetId]; - targetPosition = Point { trigger.position.x, trigger.position.y }; - } else { - const Quest &quest = Quests[static_cast(*targetId)]; - targetPosition = quest.position; - } + DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); + if (!cycleTarget) { + if (setlevel) { + if (IsRedPortalTrackerId(*targetId)) { + targetPosition = RedPortalPositionForTrackerId(*targetId); + } else { + const TriggerStruct &trigger = trigs[*targetId]; + targetPosition = Point { trigger.position.x, trigger.position.y }; + } + } else { + const Quest &quest = Quests[static_cast(*targetId)]; + targetPosition = quest.position; + } } break; } diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 3ae374b9e..a5bb42478 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -1906,7 +1906,15 @@ void InitKeymapActions() SDLK_TAB, DoAutoMap, nullptr, - IsGameRunning); + []() { return IsGameRunning() && !IsStashOpen; }); + options.Keymapper.AddAction( + "ToggleStashFocus", + N_("Toggle stash focus"), + N_("Tab: switches between inventory and stash."), + SDLK_TAB, + ToggleStashFocus, + nullptr, + []() { return IsStashOpen && !InGameMenu() && !ChatLogFlag; }); options.Keymapper.AddAction( "CycleAutomapType", N_("Cycle map type"), diff --git a/Source/items.cpp b/Source/items.cpp index 2f6ce462c..fec67f813 100644 --- a/Source/items.cpp +++ b/Source/items.cpp @@ -4139,16 +4139,25 @@ void PrintItemDetails(const Item &item) if (item._iPrePower != -1) { AddItemInfoBoxString(PrintItemPower(item._iPrePower, item)); } - if (item._iSufPower != -1) { - AddItemInfoBoxString(PrintItemPower(item._iSufPower, item)); - } - if (item._iMagical == ITEM_QUALITY_UNIQUE) { - AddItemInfoBoxString(_("unique item")); - ShowUniqueItemInfoBox = true; - curruitem = item; - } - PrintItemInfo(item); -} + if (item._iSufPower != -1) { + AddItemInfoBoxString(PrintItemPower(item._iSufPower, item)); + } + if (item._iMagical == ITEM_QUALITY_MAGIC) { + AddItemInfoBoxString(_("magic item")); + } + if (item._iMagical == ITEM_QUALITY_UNIQUE) { + AddItemInfoBoxString(_("unique item")); + const UniqueItem &uitem = UniqueItems[item._iUid]; + for (const auto &power : uitem.powers) { + if (power.type == IPL_INVALID) + break; + AddItemInfoBoxString(PrintItemPower(power.type, item)); + } + ShowUniqueItemInfoBox = true; + curruitem = item; + } + PrintItemInfo(item); +} void PrintItemDur(const Item &item) { diff --git a/Translations/pl.po b/Translations/pl.po index 6713d416a..667800908 100644 --- a/Translations/pl.po +++ b/Translations/pl.po @@ -1544,6 +1544,12 @@ msgid "Toggles if automap is displayed." msgstr "Przełącza, czy wyświetlana jest automapa." #: Source/diablo.cpp:1903 +msgid "Toggle stash focus" +msgstr "Przełącz fokus skrytki" + +msgid "Tab: switches between inventory and stash." +msgstr "Tab: przełącza między ekwipunkiem a skrytką." + msgid "Cycle map type" msgstr "Przełącz typ mapy" @@ -3347,6 +3353,10 @@ msgstr "Ładunki: {:d}/{:d}" msgid "unique item" msgstr "unikat" +#: Source/items.cpp:4146 Source/controls/tracker.cpp:316 +msgid "magic item" +msgstr "magiczny" + #: Source/items.cpp:4167 Source/items.cpp:4175 Source/items.cpp:4181 msgid "Not Identified" msgstr "Nie zidentyfikowano" @@ -3457,6 +3467,10 @@ msgstr "Do Krypty - poziom {:d}" msgid "Back to Level {:d}" msgstr "Wróć na poziom {:d}" +#: Source/controls/tracker.cpp:713 Source/controls/tracker.cpp:855 +msgid "Red portal" +msgstr "czerwony portal" + #: Source/loadsave.cpp:2013 Source/loadsave.cpp:2470 msgid "Unable to open save file archive" msgstr "Nie można otworzyć pliku zapisu" @@ -3477,6 +3491,10 @@ msgstr "" "Nieprawidłowy rozmiar skrytki. Jeśli spróbujesz uzyskać dostęp do skrytki, " "dane zostaną nadpisane!!" +#: Source/controls/plrctrls.cpp:2283 +msgid "Stash" +msgstr "skrytka" + #: Source/loadsave.cpp:2474 msgid "Invalid save file" msgstr "Nieprawidłowy plik zapisu"