diff --git a/Source/controls/controller_motion.cpp b/Source/controls/controller_motion.cpp index 51039198c..ace38ad80 100644 --- a/Source/controls/controller_motion.cpp +++ b/Source/controls/controller_motion.cpp @@ -28,7 +28,12 @@ namespace devilution { -bool SimulatingMouseWithPadmapper; +static bool GamepadAimActive = false; + +bool IsGamepadAimActive() +{ + return GamepadAimActive; +} namespace { @@ -106,15 +111,18 @@ bool IsPressedForMovement(ControllerButton button) && !(SpellSelectFlag && TriggersQuickSpellAction(button)); } -void SetSimulatingMouseWithPadmapper(bool value) +void SetGamepadAimActive(bool value) { - if (SimulatingMouseWithPadmapper == value) + if (GamepadAimActive == value) return; - SimulatingMouseWithPadmapper = value; + GamepadAimActive = value; if (value) { - LogVerbose("Control: begin simulating mouse with D-Pad"); + LogVerbose("GamepadAim: begin aiming"); + // Show cursor for aiming (if needed) } else { - LogVerbose("Control: end simulating mouse with D-Pad"); + LogVerbose("GamepadAim: end aiming"); + // Hide/reset cursor when movement resumes + ResetCursor(); } } @@ -196,14 +204,41 @@ void ProcessControllerMotion(const SDL_Event &event) GameController *const controller = GameController::Get(event); if (controller != nullptr && devilution::GameController::ProcessAxisMotion(event)) { ScaleJoysticks(); - SetSimulatingMouseWithPadmapper(false); + // End aiming and hide cursor only when left stick axis moved + if (event.type == SDL_CONTROLLERAXISMOTION) { + if (event.caxis.axis == SDL_CONTROLLER_AXIS_LEFTX || event.caxis.axis == SDL_CONTROLLER_AXIS_LEFTY) { + SetGamepadAimActive(false); + } + } return; } #endif Joystick *const joystick = Joystick::Get(event); if (joystick != nullptr && devilution::Joystick::ProcessAxisMotion(event)) { ScaleJoysticks(); - SetSimulatingMouseWithPadmapper(false); + if (event.type == SDL_JOYAXISMOTION) { +#ifdef JOY_AXIS_LEFTX + if (event.jaxis.axis == JOY_AXIS_LEFTX) + SetGamepadAimActive(false); +#endif +#ifdef JOY_AXIS_LEFTY + if (event.jaxis.axis == JOY_AXIS_LEFTY) + SetGamepadAimActive(false); +#endif + } + } + + // D-pad movement detection (button events, not padmapper mouse simulation) + StaticVector buttonEvents = ToControllerButtonEvents(event); + for (const auto &ctrlEvent : buttonEvents) { + if (!ctrlEvent.up && IsDPadButton(ctrlEvent.button)) { + // Only clear aim if this is NOT a padmapper mouse simulation combo + // (i.e., no modifier or not SELECT + D-pad) + ControllerButtonCombo combo(ctrlEvent.button); + if (combo.modifier == ControllerButton_NONE) { + SetGamepadAimActive(false); + } + } } } @@ -255,7 +290,7 @@ AxisDirection GetLeftStickOrDpadDirection(bool usePadmapper) isDownPressed |= PadmapperIsActionActive("MoveDown"); isLeftPressed |= PadmapperIsActionActive("MoveLeft"); isRightPressed |= PadmapperIsActionActive("MoveRight"); - } else if (!SimulatingMouseWithPadmapper) { + } else if (!IsGamepadAimActive()) { isUpPressed |= IsPressedForMovement(ControllerButton_BUTTON_DPAD_UP); isDownPressed |= IsPressedForMovement(ControllerButton_BUTTON_DPAD_DOWN); isLeftPressed |= IsPressedForMovement(ControllerButton_BUTTON_DPAD_LEFT); @@ -299,8 +334,6 @@ void SimulateRightStickWithPadmapper(ControllerButtonEvent ctrlEvent) const bool leftTriggered = actionName == "MouseLeft"; const bool rightTriggered = actionName == "MouseRight"; if (!upTriggered && !downTriggered && !leftTriggered && !rightTriggered) { - if (rightStickX == 0 && rightStickY == 0) - SetSimulatingMouseWithPadmapper(false); return; } @@ -319,7 +352,8 @@ void SimulateRightStickWithPadmapper(ControllerButtonEvent ctrlEvent) rightStickX -= 1.F; if (rightActive) rightStickX += 1.F; - SetSimulatingMouseWithPadmapper(true); + // Begin aiming when any direction is active + SetGamepadAimActive(true); } } // namespace devilution diff --git a/Source/controls/controller_motion.h b/Source/controls/controller_motion.h index fe507c09a..670d07712 100644 --- a/Source/controls/controller_motion.h +++ b/Source/controls/controller_motion.h @@ -12,9 +12,8 @@ #include "controls/controller.h" namespace devilution { - -// Whether we're currently simulating the mouse with SELECT + D-Pad. -extern bool SimulatingMouseWithPadmapper; +// Returns true if gamepad aiming is currently active (right stick or padmapped mouse). +bool IsGamepadAimActive(); // Raw axis values. extern float leftStickXUnscaled, leftStickYUnscaled, rightStickXUnscaled, rightStickYUnscaled; diff --git a/Source/controls/modifier_hints.cpp b/Source/controls/modifier_hints.cpp index 729655916..816bb59a4 100644 --- a/Source/controls/modifier_hints.cpp +++ b/Source/controls/modifier_hints.cpp @@ -141,7 +141,7 @@ void DrawSpellsCircleMenuHint(const Surface &out, const Point &origin) void DrawGamepadMenuNavigator(const Surface &out) { - if (!PadMenuNavigatorActive || SimulatingMouseWithPadmapper) + if (!PadMenuNavigatorActive || IsGamepadAimActive()) return; static const CircleMenuHint DPad(/*top=*/HintIcon::IconMenu, /*right=*/HintIcon::IconInv, /*bottom=*/HintIcon::IconMap, /*left=*/HintIcon::IconChar); static const CircleMenuHint Buttons(/*top=*/HintIcon::IconNull, /*right=*/HintIcon::IconNull, /*bottom=*/HintIcon::IconSpells, /*left=*/HintIcon::IconQuests); @@ -152,7 +152,7 @@ void DrawGamepadMenuNavigator(const Surface &out) void DrawGamepadHotspellMenu(const Surface &out) { - if (!PadHotspellMenuActive || SimulatingMouseWithPadmapper) + if (!PadHotspellMenuActive || IsGamepadAimActive()) return; const Rectangle &mainPanel = GetMainPanel(); diff --git a/Source/controls/plrctrls.cpp b/Source/controls/plrctrls.cpp index c4ec5092f..a765da392 100644 --- a/Source/controls/plrctrls.cpp +++ b/Source/controls/plrctrls.cpp @@ -1,5 +1,3 @@ -#include "controls/plrctrls.h" - #include #include #include @@ -25,6 +23,7 @@ #endif #include "controls/control_mode.hpp" #include "controls/game_controls.h" +#include "controls/plrctrls.h" #include "controls/touch/gamepad.h" #include "cursor.h" #include "doom.h" @@ -138,7 +137,8 @@ int GetDistance(Point destination, int maxDistance) int8_t walkpath[MaxPathLengthPlayer]; Player &myPlayer = *MyPlayer; - const int steps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, myPlayer.position.future, destination, walkpath, std::min(maxDistance, MaxPathLengthPlayer)); + const int steps = FindPath( + CanStep, [&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, myPlayer.position.future, destination, walkpath, std::min(maxDistance, MaxPathLengthPlayer)); if (steps > maxDistance) return 0; @@ -420,10 +420,14 @@ void CheckPlayerNearby() void FindActor() { - if (leveltype != DTYPE_TOWN) + if (leveltype != DTYPE_TOWN) { CheckMonstersNearby(); - else + // Always prefer monsters for spell casting if a monster is targeted + if (pcursmonst != -1) + cursPosition = Monsters[pcursmonst].position.tile; + } else { CheckTownersNearby(); + } if (gbIsMultiplayer) CheckPlayerNearby(); @@ -488,6 +492,13 @@ void FindTrigger() CheckRportal(); } +void PreferMonsterTarget() +{ + if (ControlMode == ControlTypes::Gamepad && pcursmonst != -1 && (ObjectUnderCursor != nullptr || pcursitem != -1)) { + cursPosition = Monsters[pcursmonst].position.tile; + } +} + bool IsStandingGround() { if (ControlMode == ControlTypes::Gamepad) { @@ -504,6 +515,13 @@ bool IsStandingGround() void Interact() { + if (IsGamepadAimActive()) { + const Player &myPlayer = *MyPlayer; + NetSendCmdLoc(MyPlayerId, true, myPlayer.UsesRangedWeapon() ? CMD_RATTACKXY : CMD_SATTACKXY, cursPosition); + LastPlayerAction = PlayerActionType::Attack; + return; + } + if (leveltype == DTYPE_TOWN && pcursmonst != -1) { NetSendCmdLocParam1(true, CMD_TALKXY, Towners[pcursmonst].position, pcursmonst); return; @@ -1676,7 +1694,7 @@ bool ContinueSimulatedMouseEvent(const SDL_Event &event, const ControllerButtonE return true; } - return SimulatingMouseWithPadmapper || IsSimulatedMouseClickBinding(gamepadEvent); + return IsGamepadAimActive() || IsSimulatedMouseClickBinding(gamepadEvent); } std::string_view ControlTypeToString(ControlTypes controlType) @@ -1768,11 +1786,14 @@ void DetectInputMethod(const SDL_Event &event, const ControllerButtonEvent &game ControlDevice = newControlDevice; #ifndef USE_SDL1 - if (ControlDevice != ControlTypes::KeyboardAndMouse) { - if (IsHardwareCursor()) - SetHardwareCursor(CursorInfo::UnknownCursor()); - } else { - ResetCursor(); + // Prevent cursor hiding and device/mode swap while gamepad aiming is active + if (!IsGamepadAimActive()) { + if (ControlDevice != ControlTypes::KeyboardAndMouse) { + if (IsHardwareCursor()) + SetHardwareCursor(CursorInfo::UnknownCursor()); + } else { + ResetCursor(); + } } if (ControlDevice == ControlTypes::Gamepad) { const GamepadLayout newGamepadLayout = GameController::getLayout(event); @@ -1970,6 +1991,7 @@ void plrctrls_after_check_curs_move() FindActor(); FindItemOrObject(); FindTrigger(); + PreferMonsterTarget(); } } @@ -2006,6 +2028,11 @@ void UseBeltItem(BeltItemType type) void PerformPrimaryAction() { + if (IsGamepadAimActive()) { + Interact(); + return; + } + if (SpellSelectFlag) { SetSpell(); return; @@ -2035,31 +2062,30 @@ void PerformPrimaryAction() Interact(); } -bool SpellHasActorTarget() +bool SpellHasActorTarget(SpellID spell) { - const SpellID spl = MyPlayer->_pRSpell; - if (spl == SpellID::TownPortal || spl == SpellID::Teleport) + if (spell == SpellID::TownPortal || spell == SpellID::Teleport) return false; - if (IsWallSpell(spl) && pcursmonst != -1) { - cursPosition = Monsters[pcursmonst].position.tile; - } - return PlayerUnderCursor != nullptr || pcursmonst != -1; } void UpdateSpellTarget(SpellID spell) { - if (SpellHasActorTarget()) + // For wall spells, if a monster is targeted, always set cursPosition to the monster's tile + if (IsWallSpell(spell) && pcursmonst != -1) { + cursPosition = Monsters[pcursmonst].position.tile; + return; + } + + if (SpellHasActorTarget(spell)) return; PlayerUnderCursor = nullptr; pcursmonst = -1; const Player &myPlayer = *MyPlayer; - const int range = spell == SpellID::Teleport ? 4 : 1; - cursPosition = myPlayer.position.future + Displacement(myPlayer._pdir) * range; } @@ -2131,8 +2157,21 @@ void PerformSpellAction() if (pcurs > CURSOR_HAND) NewCursor(CURSOR_HAND); - const Player &myPlayer = *MyPlayer; + Player &myPlayer = *MyPlayer; const SpellID spl = myPlayer._pRSpell; + + // Controller aiming: always cast at cursor position and turn player + if (IsGamepadAimActive()) { + // Turn player to face cursor + Direction newDir = GetDirection(myPlayer.position.tile, cursPosition); + myPlayer._pdir = newDir; + // Set spell target + cursPosition = cursPosition; + CheckPlrSpell(false); + LastPlayerAction = PlayerActionType::Spell; + return; + } + if ((PlayerUnderCursor == nullptr && (spl == SpellID::Resurrect || spl == SpellID::HealOther)) || (ObjectUnderCursor == nullptr && spl == SpellID::TrapDisarm)) { myPlayer.Say(HeroSpeech::ICantCastThatHere); @@ -2222,14 +2261,33 @@ void PerformSecondaryAction() if (!MyPlayer->HoldItem.isEmpty() && !TryDropItem()) return; + + if (pcurs == CURSOR_TELEKINESIS + || pcurs == CURSOR_IDENTIFY + || pcurs == CURSOR_REPAIR + || pcurs == CURSOR_RECHARGE + || pcurs == CURSOR_DISARM + || pcurs == CURSOR_OIL + || pcurs == CURSOR_RESURRECT + || pcurs == CURSOR_TELEPORT + || pcurs == CURSOR_HEALOTHER) { + TryIconCurs(); + return; + } + if (pcurs > CURSOR_HAND) NewCursor(CURSOR_HAND); if (pcursitem != -1) { - NetSendCmdLocParam1(true, CMD_GOTOAGETITEM, cursPosition, pcursitem); + // Use item's actual position for interaction + const Item &item = Items[pcursitem]; + NetSendCmdLocParam1(true, CMD_GOTOAGETITEM, item.position, pcursitem); + return; } else if (ObjectUnderCursor != nullptr) { - NetSendCmdLoc(MyPlayerId, true, CMD_OPOBJXY, cursPosition); + // Use object's actual position for interaction + NetSendCmdLoc(MyPlayerId, true, CMD_OPOBJXY, ObjectUnderCursor->position); LastPlayerAction = PlayerActionType::OperateObject; + return; } else { if (pcursmissile != nullptr) { MakePlrPath(myPlayer, pcursmissile->position.tile, true); @@ -2251,7 +2309,7 @@ void QuickCast(size_t slot) const SpellID spell = myPlayer._pSplHotKey[slot]; const SpellType spellType = myPlayer._pSplTHotKey[slot]; - if (ControlMode != ControlTypes::KeyboardAndMouse) { + if (!IsGamepadAimActive) { UpdateSpellTarget(spell); }