diff --git a/Core/GameEngine/Include/Common/OptionPreferences.h b/Core/GameEngine/Include/Common/OptionPreferences.h index 85aba4228be..73e57e57e77 100644 --- a/Core/GameEngine/Include/Common/OptionPreferences.h +++ b/Core/GameEngine/Include/Common/OptionPreferences.h @@ -38,6 +38,19 @@ typedef UnsignedInt CursorCaptureMode; typedef UnsignedInt ScreenEdgeScrollMode; +// TheSuperHackers @feature How targeted commands (guard, attack move, abilities) are triggered. +// Only affects hotkey activation -- a mouse click on a cameo leaves the cursor over the control +// bar, where there is no world position to cast at, so it always uses the normal two step flow. +enum CastMode CPP_11(: Int) +{ + CastMode_Normal = 0, ///< click the button, then click the world (retail behavior) + CastMode_QuickCast, ///< hotkey fires immediately at the cursor + CastMode_QuickCastWithIndicator, ///< as above, but flash the targeting decal where it fired + + CastMode_Count, + CastMode_Default = CastMode_Normal +}; + //----------------------------------------------------------------------------- // OptionsPreferences options menu class //----------------------------------------------------------------------------- @@ -76,6 +89,7 @@ class OptionPreferences : public UserPreferences Real getScrollFactor(); Bool getDrawScrollAnchor(); Bool getMoveScrollAnchor(); + CastMode getCastMode() const; Bool getCursorCaptureEnabledInWindowedGame() const; Bool getCursorCaptureEnabledInWindowedMenu() const; Bool getCursorCaptureEnabledInFullscreenGame() const; diff --git a/Core/GameEngine/Include/GameClient/HotKey.h b/Core/GameEngine/Include/GameClient/HotKey.h index 70206f24cf6..698514220aa 100644 --- a/Core/GameEngine/Include/GameClient/HotKey.h +++ b/Core/GameEngine/Include/GameClient/HotKey.h @@ -98,6 +98,19 @@ class HotKeyManager : public SubsystemInterface AsciiString searchHotKey( const AsciiString& label); AsciiString searchHotKey( const UnicodeString& uStr ); + // TheSuperHackers @feature True while a button press is being synthesized from a keyboard + // hotkey rather than an actual mouse click. Quick cast needs to tell the two apart. + static void setExecutingHotKey( Bool executing ) { s_executingHotKey = executing; } + static Bool isExecutingHotKey( void ) { return s_executingHotKey; } + + // TheSuperHackers @feature True on the key down half of a hold to aim quick cast, when the + // command should arm and show its decal rather than fire. + static void setQuickCastAiming( Bool aiming ) { s_quickCastAiming = aiming; } + static Bool isQuickCastAiming( void ) { return s_quickCastAiming; } + + static Bool s_executingHotKey; + static Bool s_quickCastAiming; + private: typedef std::map HotKeyMap; HotKeyMap m_hotKeyMap; diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp index e681ef8b192..96d9094ba36 100644 --- a/Core/GameEngine/Source/Common/OptionPreferences.cpp +++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp @@ -216,6 +216,27 @@ Bool OptionPreferences::getRightMouseScrollWithAlternateMouseEnabled() const return FALSE; } +// TheSuperHackers @feature Options.ini: CastMode = Normal | QuickCast | QuickCastWithIndicator +CastMode OptionPreferences::getCastMode() const +{ + OptionPreferences::const_iterator it = find("CastMode"); + if (it == end()) + return CastMode_Default; + + if (stricmp(it->second.str(), "QuickCastWithIndicator") == 0) + return CastMode_QuickCastWithIndicator; + if (stricmp(it->second.str(), "QuickCast") == 0) + return CastMode_QuickCast; + if (stricmp(it->second.str(), "Normal") == 0) + return CastMode_Normal; + + Int mode = atoi(it->second.str()); + if (mode >= 0 && mode < CastMode_Count) + return (CastMode)mode; + + return CastMode_Default; +} + Bool OptionPreferences::getRetaliationModeEnabled() { OptionPreferences::const_iterator it = find("Retaliation"); diff --git a/Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarCommandProcessing.cpp b/Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarCommandProcessing.cpp index 5e2c41a3d48..6128082c76d 100644 --- a/Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarCommandProcessing.cpp +++ b/Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarCommandProcessing.cpp @@ -51,6 +51,15 @@ #include "GameClient/GameWindowManager.h" #include "GameClient/InGameUI.h" #include "GameClient/AnimateWindowManager.h" +// TheSuperHackers @feature for quick cast +#if RTS_ZEROHOUR +#include "Common/OptionPreferences.h" +#include "Common/Recorder.h" +#include "GameClient/HotKey.h" +#include "GameClient/Mouse.h" +#include "GameClient/View.h" +#include "GameLogic/Module/SpecialPowerModule.h" +#endif #include "GameLogic/GameLogic.h" #include "GameLogic/Object.h" @@ -117,6 +126,123 @@ CBCommandStatus ControlBar::processCommandTransitionUI( GameWindow *control, Gad /** Process a button selected message from the window system that should be for one of * our GUI commands */ //------------------------------------------------------------------------------------------------- +#if RTS_ZEROHOUR +// TheSuperHackers @feature Quick cast (Options.ini: CastMode). +/** + * Fire a targeted command at the cursor instead of waiting for a second click. + * + * Returns TRUE only if the command was actually dispatched. Every rejection path returns + * FALSE so the caller arms the command normally -- an input is never silently eaten. + * + * Deliberately limited to keyboard activation. Clicking a cameo with the mouse leaves the + * cursor over the control bar, where there is no world position worth targeting. + */ +static Bool tryQuickCast( const CommandButton *commandButton ) +{ + if( commandButton == nullptr || TheGlobalData == nullptr ) + return FALSE; + + if( TheGlobalData->m_castMode == CastMode_Normal ) + return FALSE; + + // only from a hotkey -- see the note above + if( !HotKeyManager::isExecutingHotKey() ) + return FALSE; + + // Hold to aim: on the key down pass we want the normal arming path, which already shows the + // targeting decal and lets the player move the cursor. The key up pass then fires. + if( HotKeyManager::isQuickCastAiming() ) + return FALSE; + + if( TheInGameUI == nullptr || TheMouse == nullptr || TheTacticalView == nullptr ) + return FALSE; + + // leave replay playback alone, matching InGameUI::setGUICommand + if( TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK ) + return FALSE; + + const UnsignedInt options = commandButton->getOptions(); + + // A single use command burns the button permanently, so a misfire is unrecoverable -- + // never fire one blind. + if( BitIsSet( options, SINGLE_USE_COMMAND ) ) + return FALSE; + + // Rally points and beacons place a marker wherever the cursor happens to be, which is + // silent and easy to miss. Structure placement needs a deliberate footprint. + switch( commandButton->getCommandType() ) + { + case GUI_COMMAND_SET_RALLY_POINT: + case GUICOMMANDMODE_PLACE_BEACON: + case GUI_COMMAND_DOZER_CONSTRUCT: + case GUI_COMMAND_SPECIAL_POWER_CONSTRUCT: + case GUI_COMMAND_SPECIAL_POWER_CONSTRUCT_FROM_SHORTCUT: + return FALSE; + + // Superweapons are excluded on purpose: firing one at an unintended spot cannot be + // undone, and the stray keypress that does it is easy to make. + case GUI_COMMAND_SPECIAL_POWER: + case GUI_COMMAND_SPECIAL_POWER_FROM_SHORTCUT: + return FALSE; + + default: + break; + } + + // The cursor has to be over the battlefield, not the command bar or another panel. + const MouseIO *mouseIO = TheMouse->getMouseStatus(); + if( mouseIO == nullptr ) + return FALSE; + + if( TheWindowManager && + TheWindowManager->getWindowUnderCursor( mouseIO->pos.x, mouseIO->pos.y ) != nullptr ) + return FALSE; + + // TheSuperHackers @feature If the ability is still recharging, remember the cast and let + // InGameUI fire it the moment the logic side says it is ready, rather than throwing the + // input away. The cooldown itself is untouched -- this only stops the press being wasted. + if( commandButton->getSpecialPowerTemplate() ) + { + Drawable *draw = TheInGameUI->getFirstSelectedDrawable(); + Object *source = draw ? draw->getObject() : nullptr; + if( source ) + { + SpecialPowerModuleInterface *mod = + source->getSpecialPowerModule( commandButton->getSpecialPowerTemplate() ); + if( mod && !mod->isReady() ) + { + TheInGameUI->queueQuickCast( commandButton, mouseIO->pos ); + TheInGameUI->triggerQuickCastHint( commandButton, mouseIO->pos ); + return TRUE; + } + } + } + + // Hand the click to the normal path. Synthesizing the message rather than calling the + // do*Command helpers directly means quick cast reuses the engine's own validation, + // voice responses and cleanup, and cannot drift away from normal behaviour. + // + // In hold to aim mode the command was already armed on key down; re-arming here is + // harmless and covers the case where something cleared it while the key was held. + TheInGameUI->setGUICommand( commandButton ); + + // GUICommandTranslator reads the click position from pixelRegion.hi + IRegion2D clickRegion; + clickRegion.lo = mouseIO->pos; + clickRegion.hi = mouseIO->pos; + + GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_MOUSE_LEFT_CLICK ); + msg->appendPixelRegionArgument( clickRegion ); + + // In indicator mode the decal has been visible the whole time the key was held, so let it + // linger briefly at the point it fired rather than vanishing the instant the key comes up. + if( TheGlobalData->m_castMode == CastMode_QuickCastWithIndicator ) + TheInGameUI->triggerQuickCastHint( commandButton, mouseIO->pos ); + + return TRUE; +} +#endif // RTS_ZEROHOUR + CBCommandStatus ControlBar::processCommandUI( GameWindow *control, GadgetGameMessage gadgetMessage ) { @@ -221,8 +347,27 @@ CBCommandStatus ControlBar::processCommandUI( GameWindow *control, //with. For example, the terrorist can jack a car and convert it into a carbomb, but he has to //click on a valid car. In this case the doCommandOrHint code will determine if the mode is valid //or not and the cursor modes will be set appropriately. + +#if RTS_ZEROHOUR + // TheSuperHackers @feature Quick cast fires the command at the cursor instead of waiting for + // a second click. If it declines -- wrong mode, unsafe command, cursor not over the + // battlefield -- fall through and arm normally, so nothing is ever silently swallowed. + if( tryQuickCast( commandButton ) ) + return CBC_COMMAND_USED; +#endif + TheInGameUI->setGUICommand( commandButton ); } +#if RTS_ZEROHOUR + // TheSuperHackers @fix In hold to aim mode the key down pass exists only to arm targeted + // commands so the decal shows while aiming. Everything else must act on the key up pass + // alone - otherwise a production hotkey queues two units per press and a toggle undoes + // itself, one per key transition. + else if( HotKeyManager::isQuickCastAiming() ) + { + // swallow the key down half; the key up pass executes normally + } +#endif else switch( commandButton->getCommandType() ) { diff --git a/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp b/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp index 0b7ecf22d1e..989dfddd71f 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp @@ -51,6 +51,9 @@ // USER INCLUDES ////////////////////////////////////////////////////////////// //----------------------------------------------------------------------------- #include "GameClient/HotKey.h" +// TheSuperHackers @feature for hold to aim quick cast +#include "Common/GlobalData.h" +#include "Common/OptionPreferences.h" #include "GameClient/KeyDefs.h" #include "GameClient/MetaEvent.h" #include "GameClient/GameWindow.h" @@ -72,7 +75,17 @@ GameMessageDisposition HotKeyTranslator::translateGameMessage(const GameMessage GameMessageDisposition disp = KEEP_MESSAGE; GameMessage::Type t = msg->getType(); - if ( t == GameMessage::MSG_RAW_KEY_UP) + // TheSuperHackers @feature In QuickCastWithIndicator mode a hotkey arms on key down so the + // player can see the targeting decal and still adjust aim, then fires on key up. Every other + // mode keeps the original behaviour of acting only on key up. +#if RTS_ZEROHOUR + const Bool holdToAim = TheGlobalData && + TheGlobalData->m_castMode == CastMode_QuickCastWithIndicator; +#else + const Bool holdToAim = FALSE; +#endif + + if ( t == GameMessage::MSG_RAW_KEY_UP || (holdToAim && t == GameMessage::MSG_RAW_KEY_DOWN) ) { //char key = msg->getArgument(0)->integer; @@ -103,8 +116,16 @@ GameMessageDisposition HotKeyTranslator::translateGameMessage(const GameMessage uKey.concat(key); AsciiString aKey; aKey.translate(uKey); - if(TheHotKeyManager && TheHotKeyManager->executeHotKey(aKey)) - disp = DESTROY_MESSAGE; + if( TheHotKeyManager ) + { + // key down arms and previews, key up commits at wherever the cursor ended up + HotKeyManager::setQuickCastAiming( holdToAim && t == GameMessage::MSG_RAW_KEY_DOWN ); + + if( TheHotKeyManager->executeHotKey(aKey) ) + disp = DESTROY_MESSAGE; + + HotKeyManager::setQuickCastAiming( FALSE ); + } } return disp; } @@ -157,6 +178,10 @@ void HotKeyManager::addHotKey( GameWindow *win, const AsciiString& keyIn) m_hotKeyMap[key] = newHK; } +// TheSuperHackers @feature See HotKey.h -- set only while synthesizing a button press. +Bool HotKeyManager::s_executingHotKey = FALSE; +Bool HotKeyManager::s_quickCastAiming = FALSE; + //----------------------------------------------------------------------------- Bool HotKeyManager::executeHotKey( const AsciiString& keyIn ) { @@ -170,9 +195,41 @@ Bool HotKeyManager::executeHotKey( const AsciiString& keyIn ) return FALSE; if( !BitIsSet( win->winGetStatus(), WIN_STATUS_HIDDEN ) ) { - if( BitIsSet( win->winGetStatus(), WIN_STATUS_ENABLED ) ) + // TheSuperHackers @feature A button that is merely not ready yet -- a recharging ability, + // or a weapon still working through its burst -- is disabled, which normally swallows the + // hotkey outright. In quick cast let it through anyway. + // + // Without this a repeat press is dropped here, before quick cast ever runs, and the only + // way to retarget is to Stop first. A FIRE_WEAPON cameo reports COMMAND_NOT_READY for as + // long as its weapon is not READY_TO_FIRE, so any unit still shooting has its own button + // disabled underneath the player. Retargeting mid burst is expected behaviour -- see the + // DragonTank firewall note in ControlBarCommand.cpp, which describes the same case. + // + // Deliberately narrow: this only opens up buttons disabled by WIN_STATUS_NOT_READY. Ones + // that are restricted or unaffordable stay rejected, and the order still only does + // anything if the logic side accepts it, so this cannot fire something that is genuinely + // unavailable -- it just stops the keypress being thrown away before it is even looked at. +#if RTS_ZEROHOUR + Bool allowWhileNotReady = FALSE; + if( !BitIsSet( win->winGetStatus(), WIN_STATUS_ENABLED ) && + BitIsSet( win->winGetStatus(), WIN_STATUS_NOT_READY ) && + TheGlobalData && + TheGlobalData->m_castMode != CastMode_Normal ) + { + allowWhileNotReady = TRUE; + } +#else + const Bool allowWhileNotReady = FALSE; +#endif + + if( BitIsSet( win->winGetStatus(), WIN_STATUS_ENABLED ) || allowWhileNotReady ) { + // TheSuperHackers @feature Tell the command bar this press came from the keyboard, so + // quick cast can fire at the cursor. A mouse click on the cameo leaves the cursor over + // the control bar, where there is nothing sensible to target. + HotKeyManager::setExecutingHotKey( TRUE ); TheWindowManager->winSendSystemMsg( win->winGetParent(), GBM_SELECTED, (WindowMsgData)win, win->winGetWindowId() ); + HotKeyManager::setExecutingHotKey( FALSE ); // here we make the same click sound that the GUI uses when you click a button AudioEventRTS buttonClick("GUIClick"); diff --git a/Core/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp b/Core/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp index 321423df5b1..3d081db2ef0 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp @@ -1094,6 +1094,19 @@ GameMessageDisposition SelectionTranslator::onRawMouseRightButtonUp(MAYBE_UNUSED { //Added support to cancel the GUI command without deselecting the unit(s) involved //when you right click. + // TheSuperHackers @feature Right click also cancels a queued quick cast, so a cast + // waiting on a cooldown can be called off the same way an armed command is. +#if RTS_ZEROHOUR + if( TheInGameUI->hasQueuedQuickCast() ) + { + TheInGameUI->cancelQueuedQuickCast(); + TheInGameUI->setScrolling( FALSE ); + + //With a queued cast cancel, we want no other behavior. + return DESTROY_MESSAGE; + } +#endif + if( TheInGameUI->getGUICommand() ) { //Cancel GUI command mode... don't deselect units. diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 89a5fa08f9d..134371e8228 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -142,6 +142,9 @@ class GlobalData : public SubsystemInterface Bool m_useAlternateMouse; Bool m_useRightMouseScrollWithAlternateMouse; // TheSuperHackers @feature User option for RMB scroll in Alternate Mouse mode. Bool m_clientRetaliationModeEnabled; + // TheSuperHackers @feature Hotkey activation behaviour for targeted commands. + // Holds a CastMode; stored as Int to avoid pulling OptionPreferences.h in here. + Int m_castMode; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; Int m_jpegQuality; // TheSuperHackers @feature Quality for JPEG screenshots. diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h index ecc1c4fdaf5..8baf9cff1fb 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h @@ -604,6 +604,15 @@ friend class Drawable; // for selection/deselection transactions void unregisterWindowLayout(WindowLayout *layout); // stop updates for this layout void triggerDoubleClickAttackMoveGuardHint(); + // TheSuperHackers @feature Flash the targeting decal where a quick cast landed. + void triggerQuickCastHint( const CommandButton *command, const ICoord2D &screenPos ); + + // TheSuperHackers @feature Queued quick cast -- remember a cast requested while the ability + // was still recharging, and fire it once the logic side says it is ready. + void queueQuickCast( const CommandButton *command, const ICoord2D &screenPos ); + void cancelQueuedQuickCast( void ); + Bool hasQueuedQuickCast( void ) const { return m_queuedCastCommandName.isNotEmpty(); } + void updateQueuedQuickCast( void ); public: @@ -748,6 +757,21 @@ friend class Drawable; // for selection/deselection transactions Int m_duringDoubleClickAttackMoveGuardHintTimer; ///< Frames left to draw the doubleClickFeedbackTimer Coord3D m_duringDoubleClickAttackMoveGuardHintStashedPosition; + // TheSuperHackers @feature Quick cast indicator, same shape as the hint above. + Int m_quickCastHintTimer; + Coord3D m_quickCastHintPosition; + RadiusCursorType m_quickCastHintCursorType; + // Remembered so the fading redraw recreates the cursor with the same radius inputs. + const SpecialPowerTemplate *m_quickCastHintTemplate; + WeaponSlotType m_quickCastHintWeaponSlot; + // TheSuperHackers @feature Queued quick cast state. + // The command is remembered by name and re-resolved at fire time: the control bar owns + // the button objects and recreates them on a resolution change, so a retained pointer + // could dangle while the queue waits. + AsciiString m_queuedCastCommandName; + Coord3D m_queuedCastWorldPos; + ObjectID m_queuedCastSourceID; + UnsignedInt m_queuedCastExpiryFrame; // Video playback data VideoBuffer* m_videoBuffer; ///< video playback buffer diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index e862cd149d5..867ca574210 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1062,6 +1062,7 @@ GlobalData::GlobalData() m_useRightMouseScrollWithAlternateMouse = TRUE; #endif m_clientRetaliationModeEnabled = TRUE; //On by default. + m_castMode = CastMode_Default; m_doubleClickAttackMove = FALSE; } @@ -1206,6 +1207,7 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_useRightMouseScrollWithAlternateMouse = optionPref.getRightMouseScrollWithAlternateMouseEnabled(); TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled(); TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled(); + TheWritableGlobalData->m_castMode = optionPref.getCastMode(); TheWritableGlobalData->m_jpegQuality = optionPref.getJpegQuality(); TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor(); TheWritableGlobalData->m_drawScrollAnchor = optionPref.getDrawScrollAnchor(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index e323d96d34d..3615b854a1b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -1057,6 +1057,16 @@ InGameUI::InGameUI() m_selectCount = 0; m_frameSelectionChanged = 0; m_duringDoubleClickAttackMoveGuardHintTimer = 0; + // TheSuperHackers @feature quick cast indicator starts idle + m_quickCastHintTimer = 0; + m_quickCastHintPosition.zero(); + m_quickCastHintCursorType = RADIUSCURSOR_NONE; + m_quickCastHintTemplate = nullptr; + m_quickCastHintWeaponSlot = PRIMARY_WEAPON; + m_queuedCastCommandName.clear(); + m_queuedCastWorldPos.zero(); + m_queuedCastSourceID = INVALID_ID; + m_queuedCastExpiryFrame = 0; m_duringDoubleClickAttackMoveGuardHintStashedPosition.zero(); m_maxSelectCount = -1; m_isScrolling = FALSE; @@ -1513,7 +1523,20 @@ void InGameUI::handleRadiusCursor() { if (!m_curRadiusCursor.isEmpty()) { - if ( TheGlobalData->m_doubleClickAttackMove && m_duringDoubleClickAttackMoveGuardHintTimer > 0 ) + // TheSuperHackers @feature While a cast is queued waiting on its cooldown, pin the decal + // to the spot it will land on, so the player can see what is pending. + if ( hasQueuedQuickCast() ) + { + m_curRadiusCursor.setOpacity( 0.5f ); + m_curRadiusCursor.setPosition( m_queuedCastWorldPos ); + } + // TheSuperHackers @feature Quick cast indicator fades out where the command landed. + else if ( m_quickCastHintTimer > 0 ) + { + m_curRadiusCursor.setOpacity( m_quickCastHintTimer * 0.1f ); + m_curRadiusCursor.setPosition( m_quickCastHintPosition ); + } + else if ( TheGlobalData->m_doubleClickAttackMove && m_duringDoubleClickAttackMoveGuardHintTimer > 0 ) { m_curRadiusCursor.setOpacity( m_duringDoubleClickAttackMoveGuardHintTimer * 0.1f ); m_curRadiusCursor.setPosition( m_duringDoubleClickAttackMoveGuardHintStashedPosition ); //world space position of center of decal @@ -1552,6 +1575,142 @@ void InGameUI::handleRadiusCursor() } +// TheSuperHackers @feature Quick cast indicator. +/** Flash the command's own targeting decal where the quick cast landed, so the player gets + * the same visual confirmation they would have had from aiming manually. */ +void InGameUI::triggerQuickCastHint( const CommandButton *command, const ICoord2D &screenPos ) +{ + if( command == nullptr ) + return; + + m_quickCastHintTimer = 11; + m_quickCastHintCursorType = command->getRadiusCursorType(); + m_quickCastHintTemplate = command->getSpecialPowerTemplate(); + m_quickCastHintWeaponSlot = command->getWeaponSlot(); + + if( !rts::localPlayerHasRadar() || (TheRadar->screenPixelToWorld( &screenPos, &m_quickCastHintPosition ) == FALSE) ) + TheTacticalView->screenToTerrain( &screenPos, &m_quickCastHintPosition ); + + setRadiusCursor( m_quickCastHintCursorType, m_quickCastHintTemplate, + m_quickCastHintWeaponSlot ); +} + +// TheSuperHackers @feature Queued quick cast. +/** Remember a cast the player asked for while the ability was still recharging. */ +void InGameUI::queueQuickCast( const CommandButton *command, const ICoord2D &screenPos ) +{ + if( command == nullptr ) + return; + + m_queuedCastCommandName = command->getName(); + m_queuedCastSourceID = INVALID_ID; + + // Remember where in the world it was aimed, so the cast still lands on the chosen spot + // even if the camera has scrolled by the time it fires. + if( !rts::localPlayerHasRadar() || (TheRadar->screenPixelToWorld( &screenPos, &m_queuedCastWorldPos ) == FALSE) ) + TheTacticalView->screenToTerrain( &screenPos, &m_queuedCastWorldPos ); + + Drawable *draw = getFirstSelectedDrawable(); + if( draw && draw->getObject() ) + m_queuedCastSourceID = draw->getObject()->getID(); + + // Give up eventually rather than firing minutes later out of nowhere. + m_queuedCastExpiryFrame = TheGameLogic->getFrame() + (LOGICFRAMES_PER_SECOND * 60); +} + +//------------------------------------------------------------------------------------------------- +void InGameUI::cancelQueuedQuickCast( void ) +{ + if( !hasQueuedQuickCast() ) + return; + + m_queuedCastCommandName.clear(); + m_queuedCastSourceID = INVALID_ID; + m_queuedCastExpiryFrame = 0; + setRadiusCursorNone(); +} + +//------------------------------------------------------------------------------------------------- +/** Fire a queued cast once the ability is ready. Readiness is asked of the logic side every + * frame rather than predicted, so this never fires earlier than a manual cast could. */ +void InGameUI::updateQueuedQuickCast( void ) +{ + if( !hasQueuedQuickCast() ) + return; + + if( TheGameLogic == nullptr || TheGameLogic->getFrame() > m_queuedCastExpiryFrame ) + { + cancelQueuedQuickCast(); + return; + } + + // Re-resolve the button from its name: the control bar owns the buttons and recreates + // them on a resolution change, so a pointer retained across frames could dangle. + const CommandButton *command = + TheControlBar ? TheControlBar->findCommandButton( m_queuedCastCommandName ) : nullptr; + if( command == nullptr ) + { + cancelQueuedQuickCast(); + return; + } + + // the caster has to still exist, still be ours, and still be selected + Object *source = TheGameLogic->findObjectByID( m_queuedCastSourceID ); + if( source == nullptr || source->isEffectivelyDead() || + source->getControllingPlayer() != ThePlayerList->getLocalPlayer() ) + { + cancelQueuedQuickCast(); + return; + } + + // The cast fires through the current selection, so the source has to still be part of + // it - otherwise a selection change while waiting would redirect the queued command to + // whatever is selected now. + if( source->getDrawable() == nullptr || !source->getDrawable()->isSelected() ) + { + cancelQueuedQuickCast(); + return; + } + + const SpecialPowerTemplate *spTemplate = command->getSpecialPowerTemplate(); + if( spTemplate ) + { + SpecialPowerModuleInterface *mod = source->getSpecialPowerModule( spTemplate ); + if( mod == nullptr ) + { + cancelQueuedQuickCast(); + return; + } + + if( !mod->isReady() ) + return; // still charging, check again next frame + } + + // Ready. Fire it the same way a manual cast would, by arming and clicking, so all the + // engine's validation and cleanup applies. + // + // The click has to land on the remembered world position. If the camera has scrolled it + // out of the frustum the projection fails, and reusing the originally captured pixel + // would resolve against the current camera and fire at different terrain - so hold the + // cast until the spot is back on screen, or let it expire. + ICoord2D screenPos; + if( !TheTacticalView->worldToScreen( &m_queuedCastWorldPos, &screenPos ) ) + return; + + cancelQueuedQuickCast(); + + setGUICommand( command ); + + IRegion2D clickRegion; + clickRegion.lo = screenPos; + clickRegion.hi = screenPos; + + GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_MOUSE_LEFT_CLICK ); + msg->appendPixelRegionArgument( clickRegion ); + + triggerQuickCastHint( command, screenPos ); +} + void InGameUI::triggerDoubleClickAttackMoveGuardHint() { const MouseIO* mouseIO = TheMouse->getMouseStatus(); @@ -1846,6 +2005,9 @@ void InGameUI::update() //USE_PERF_TIMER(InGameUI_update) Int i; + // TheSuperHackers @feature Fire a queued quick cast as soon as its ability comes up. + updateQueuedQuickCast(); + /// @todo make sure this code gets called even when the UI is not being drawn if ( m_videoStream && m_videoBuffer ) { @@ -2862,6 +3024,20 @@ void InGameUI::createCommandHint( const GameMessage *msg ) setRadiusCursorNone(); + + // TheSuperHackers @feature Hold the quick cast decal for a few frames after firing, then + // let the normal cursor logic take back over. + if ( m_quickCastHintTimer > 0 ) + { + if ( --m_quickCastHintTimer > 0 ) + { + // recreate with the same inputs it was made with, or a special power hint would lose + // its template and a non primary weapon hint its radius + setRadiusCursor( m_quickCastHintCursorType, m_quickCastHintTemplate, m_quickCastHintWeaponSlot ); + return; + } + } + if ( TheGlobalData->m_doubleClickAttackMove ) { if ( --m_duringDoubleClickAttackMoveGuardHintTimer > 0 )