diff --git a/Source/cursor.cpp b/Source/cursor.cpp index 64c7c871a..22d617a71 100644 --- a/Source/cursor.cpp +++ b/Source/cursor.cpp @@ -15,8 +15,10 @@ #include "doom.h" #include "engine.h" #include "engine/backbuffer_state.hpp" +#include "engine/demomode.h" #include "engine/load_cel.hpp" #include "engine/point.hpp" +#include "engine/points_in_rectangle_range.hpp" #include "engine/render/clx_render.hpp" #include "engine/trn.hpp" #include "hwcursor.hpp" @@ -298,6 +300,134 @@ bool TrySelectItem(bool flipflag, int mx, int my) return pcursitem != -1; } +bool TrySelectPixelBased(Point tile) +{ + if (demo::IsRunning() || demo::IsRecording() || HeadlessMode) { + // Recorded demos can run headless, but headless mode doesn't support loading sprites that are needed for pixel perfect selection + // => Ensure demos are always compatible + // => Never use sprites for selection when handling demos + return false; + } + + auto checkSprite = [](Point renderingTile, const ClxSprite sprite, Displacement renderingOffset) { + const Point renderPosition = GetScreenPosition(renderingTile) + renderingOffset; + Point spriteTopLeft = renderPosition - Displacement { 0, sprite.height() }; + Size spriteSize = { sprite.width(), sprite.height() }; + if (*sgOptions.Graphics.zoom) { + spriteSize *= 2; + spriteTopLeft *= 2; + } + const Rectangle spriteCoords = Rectangle(spriteTopLeft, spriteSize); + if (!spriteCoords.contains(MousePosition)) + return false; + Point pointInSprite = Point { 0, 0 } + (MousePosition - spriteCoords.position); + if (*sgOptions.Graphics.zoom) + pointInSprite /= 2; + return IsPointWithinClx(pointInSprite, sprite); + }; + + auto convertFromRenderingToWorldTile = [](Point renderingPoint) { + // Columns + Displacement ret = Displacement(Direction::East) * renderingPoint.x; + // Rows + ret += Displacement(Direction::South) * renderingPoint.y / 2; + if (renderingPoint.y & 1) + ret.deltaY += 1; + return ret; + }; + + // Try to find the selected entity from rendered pixels. + // We search the rendered rows/columns backwards, because the last rendered tile overrides previous rendered pixels. + auto searchArea = PointsInRectangle(Rectangle { { -1, -1 }, { 3, 8 } }); + for (auto it = searchArea.rbegin(); it != searchArea.rend(); ++it) { + Point renderingColumnRaw = *it; + Point adjacentTile = tile + convertFromRenderingToWorldTile(renderingColumnRaw); + if (!InDungeonBounds(adjacentTile)) + continue; + + int monsterId = dMonster[adjacentTile.x][adjacentTile.y]; + // Never select a monster if a target-player-only spell is selected + if (monsterId != 0 && IsNoneOf(pcurs, CURSOR_HEALOTHER, CURSOR_RESURRECT)) { + monsterId = abs(monsterId) - 1; + if (leveltype == DTYPE_TOWN) { + const Towner &towner = Towners[monsterId]; + const ClxSprite sprite = towner.currentSprite(); + Displacement renderingOffset = towner.getRenderingOffset(); + if (checkSprite(adjacentTile, sprite, renderingOffset)) { + cursPosition = adjacentTile; + pcursmonst = monsterId; + return true; + } + } else { + const Monster &monster = Monsters[monsterId]; + if (IsValidMonsterForSelection(monster)) { + const ClxSprite sprite = monster.animInfo.currentSprite(); + Displacement renderingOffset = monster.getRenderingOffset(sprite); + if (checkSprite(adjacentTile, sprite, renderingOffset)) { + cursPosition = adjacentTile; + pcursmonst = monsterId; + return true; + } + } + } + } + + int playerId = dPlayer[adjacentTile.x][adjacentTile.y]; + if (playerId != 0) { + playerId = abs(playerId) - 1; + if (playerId != MyPlayerId) { + const Player &player = Players[playerId]; + const ClxSprite sprite = player.currentSprite(); + Displacement renderingOffset = player.getRenderingOffset(sprite); + if (checkSprite(adjacentTile, sprite, renderingOffset)) { + cursPosition = adjacentTile; + pcursplr = playerId; + return true; + } + } + } + if (TileContainsDeadPlayer(adjacentTile)) { + for (const Player &player : Players) { + if (player.position.tile == adjacentTile && &player != MyPlayer) { + const ClxSprite sprite = player.currentSprite(); + Displacement renderingOffset = player.getRenderingOffset(sprite); + if (checkSprite(adjacentTile, sprite, renderingOffset)) { + cursPosition = adjacentTile; + pcursplr = static_cast(player.getId()); + return true; + } + } + } + } + + Object *object = FindObjectAtPosition(adjacentTile); + if (object != nullptr && object->_oSelFlag != 0) { + const ClxSprite sprite = object->currentSprite(); + Displacement renderingOffset = object->getRenderingOffset(sprite, adjacentTile); + if (checkSprite(adjacentTile, sprite, renderingOffset)) { + cursPosition = adjacentTile; + ObjectUnderCursor = object; + return true; + } + } + + uint8_t itemId = dItem[adjacentTile.x][adjacentTile.y]; + if (itemId != 0) { + itemId = itemId - 1; + const Item &item = Items[itemId]; + const ClxSprite sprite = item.AnimInfo.currentSprite(); + Displacement renderingOffset = item.getRenderingOffset(sprite); + if (checkSprite(adjacentTile, sprite, renderingOffset)) { + cursPosition = adjacentTile; + pcursitem = static_cast(itemId); + return true; + } + } + } + + return false; +} + } // namespace /** Current highlighted monster */ @@ -692,6 +822,9 @@ void CheckCursMove() return; } + if (TrySelectPixelBased(currentTile)) + return; + if (leveltype != DTYPE_TOWN) { // Never select a monster if a target-player-only spell is selected if (IsNoneOf(pcurs, CURSOR_HEALOTHER, CURSOR_RESURRECT)) { @@ -723,12 +856,12 @@ void CheckCursMove() } if (TrySelectObject(flipflag, currentTile)) { - // found a object + // found an object return; } if (TrySelectItem(flipflag, mx, my)) { - // found a item + // found an item return; }