diff --git a/Core/GameEngine/Include/Common/MessageStream.h b/Core/GameEngine/Include/Common/MessageStream.h index e5dc1fc0896..a26ef34c01b 100644 --- a/Core/GameEngine/Include/Common/MessageStream.h +++ b/Core/GameEngine/Include/Common/MessageStream.h @@ -244,6 +244,7 @@ class GameMessage : public MemoryPoolObject MSG_META_TOGGLE_LOWER_DETAILS, ///< toggles graphics options to crappy mode instantly MSG_META_TOGGLE_CONTROL_BAR, ///< show/hide controlbar MSG_META_TOGGLE_PLAYER_OBSERVER, ///< TheSuperHackers @feature Toggle the player observer view in game + MSG_META_CYCLE_HEALTH_BAR_MODE, ///< TheSuperHackers @feature Cycle HealthBarDisplayMode in game MSG_META_BEGIN_PATH_BUILD, ///< enter path-building mode MSG_META_END_PATH_BUILD, ///< exit path-building mode diff --git a/Core/GameEngine/Include/Common/OptionPreferences.h b/Core/GameEngine/Include/Common/OptionPreferences.h index 85aba4228be..5bd50c1c325 100644 --- a/Core/GameEngine/Include/Common/OptionPreferences.h +++ b/Core/GameEngine/Include/Common/OptionPreferences.h @@ -38,6 +38,18 @@ typedef UnsignedInt CursorCaptureMode; typedef UnsignedInt ScreenEdgeScrollMode; +// TheSuperHackers @feature When health bars are shown above objects. Purely a client side +// display preference -- it never affects game logic, so it is safe in multiplayer and replays. +enum HealthBarDisplayMode CPP_11(: Int) +{ + HealthBarDisplayMode_Classic = 0, ///< selected and moused over objects only (retail behavior) + HealthBarDisplayMode_Damaged, ///< the above, plus anything that is not at full health + HealthBarDisplayMode_Always, ///< the above, plus every undamaged unit and structure + + HealthBarDisplayMode_Count, + HealthBarDisplayMode_Default = HealthBarDisplayMode_Classic +}; + //----------------------------------------------------------------------------- // OptionsPreferences options menu class //----------------------------------------------------------------------------- @@ -71,6 +83,9 @@ class OptionPreferences : public UserPreferences Bool getAlternateMouseModeEnabled(); Bool getRightMouseScrollWithAlternateMouseEnabled() const; Bool getRetaliationModeEnabled(); + HealthBarDisplayMode getHealthBarDisplayMode() const; + Bool getNumericalHealthEnabled() const; + Bool getSmartPipsEnabled() const; Bool getDoubleClickAttackMoveEnabled(); Int getJpegQuality() const; Real getScrollFactor(); diff --git a/Core/GameEngine/Source/Common/MessageStream.cpp b/Core/GameEngine/Source/Common/MessageStream.cpp index 9980021c6ee..c471db5b267 100644 --- a/Core/GameEngine/Source/Common/MessageStream.cpp +++ b/Core/GameEngine/Source/Common/MessageStream.cpp @@ -326,6 +326,7 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t) CASE_LABEL(MSG_META_TOGGLE_LOWER_DETAILS) CASE_LABEL(MSG_META_TOGGLE_CONTROL_BAR) CASE_LABEL(MSG_META_TOGGLE_PLAYER_OBSERVER) + CASE_LABEL(MSG_META_CYCLE_HEALTH_BAR_MODE) CASE_LABEL(MSG_META_BEGIN_PATH_BUILD) CASE_LABEL(MSG_META_END_PATH_BUILD) CASE_LABEL(MSG_META_BEGIN_FORCEATTACK) diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp index e681ef8b192..1258f420c4b 100644 --- a/Core/GameEngine/Source/Common/OptionPreferences.cpp +++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp @@ -216,6 +216,59 @@ Bool OptionPreferences::getRightMouseScrollWithAlternateMouseEnabled() const return FALSE; } +// TheSuperHackers @feature Health bar display mode, read from Options.ini as +// HealthBarDisplayMode = Classic | Damaged | Always (a plain index also works). +HealthBarDisplayMode OptionPreferences::getHealthBarDisplayMode() const +{ + OptionPreferences::const_iterator it = find("HealthBarDisplayMode"); + if (it == end()) + return HealthBarDisplayMode_Default; + + if (stricmp(it->second.str(), "Always") == 0) + return HealthBarDisplayMode_Always; + if (stricmp(it->second.str(), "Damaged") == 0) + return HealthBarDisplayMode_Damaged; + if (stricmp(it->second.str(), "Classic") == 0) + return HealthBarDisplayMode_Classic; + + // also accept the raw index, so the value round trips if it is ever written numerically + Int mode = atoi(it->second.str()); + if (mode >= 0 && mode < HealthBarDisplayMode_Count) + return (HealthBarDisplayMode)mode; + + return HealthBarDisplayMode_Default; +} + +// TheSuperHackers @feature Options.ini: NumericalHealth = Yes prints the hit points beside the +// health bar. Follows HealthBarDisplayMode, so the number appears exactly where a bar does. +Bool OptionPreferences::getNumericalHealthEnabled() const +{ + OptionPreferences::const_iterator it = find("NumericalHealth"); + if (it == end()) + return FALSE; + + if (stricmp(it->second.str(), "yes") == 0) { + return TRUE; + } + return FALSE; +} + +// TheSuperHackers @feature Options.ini: SmartPips = Yes keeps ammo and passenger pips on screen +// instead of showing them only while the unit is selected or moused over. Own units only -- not +// allies, not enemies. Nothing is drawn when there is nothing to report: no shots left, or no +// one aboard. So the pips read as "still loaded" and "carrying someone" at a glance. +Bool OptionPreferences::getSmartPipsEnabled() const +{ + OptionPreferences::const_iterator it = find("SmartPips"); + if (it == end()) + return FALSE; + + if (stricmp(it->second.str(), "yes") == 0) { + return TRUE; + } + return FALSE; +} + Bool OptionPreferences::getRetaliationModeEnabled() { OptionPreferences::const_iterator it = find("Retaliation"); diff --git a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index c2835253d30..169e7c191b8 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -59,6 +59,8 @@ #include "GameClient/GameClient.h" #include "GameClient/GameWindowManager.h" #include "GameClient/GameText.h" +// TheSuperHackers @feature for the health bar display mode cycle +#include "Common/OptionPreferences.h" #include "GameClient/ParticleSys.h" #include "GameClient/GUICallbacks.h" #include "GameClient/Shell.h" @@ -3455,6 +3457,44 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage break; } + //----------------------------------------------------------------------------------------- +#if RTS_ZEROHOUR + // TheSuperHackers @feature Cycle the health bar display mode in game, so the player can + // switch between Classic, Damaged and Always without leaving to edit Options.ini. Purely a + // client side display setting -- nothing here reaches the simulation, so it is safe in + // multiplayer and replays. The change is not written back to Options.ini, so it lasts for + // the session only and the configured mode returns next launch. + case GameMessage::MSG_META_CYCLE_HEALTH_BAR_MODE: + { + if( TheWritableGlobalData ) + { + Int mode = TheGlobalData->m_healthBarDisplayMode + 1; + if( mode >= HealthBarDisplayMode_Count || mode < 0 ) + mode = HealthBarDisplayMode_Classic; + + TheWritableGlobalData->m_healthBarDisplayMode = mode; + + switch( mode ) + { + case HealthBarDisplayMode_Damaged: + TheInGameUI->messageNoFormat( TheGameText->FETCH_OR_SUBSTITUTE( + "GUI:HealthBarModeDamaged", L"Health bars: damaged only" ) ); + break; + case HealthBarDisplayMode_Always: + TheInGameUI->messageNoFormat( TheGameText->FETCH_OR_SUBSTITUTE( + "GUI:HealthBarModeAlways", L"Health bars: always" ) ); + break; + default: + TheInGameUI->messageNoFormat( TheGameText->FETCH_OR_SUBSTITUTE( + "GUI:HealthBarModeClassic", L"Health bars: classic" ) ); + break; + } + } + disp = DESTROY_MESSAGE; + break; + } +#endif // RTS_ZEROHOUR + //----------------------------------------------------------------------------------------- case GameMessage::MSG_META_TOGGLE_ATTACKMOVE: TheInGameUI->toggleAttackMoveToMode(); diff --git a/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index 31a954e67b5..491fca02db0 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -159,6 +159,7 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "TOGGLE_LOWER_DETAILS", GameMessage::MSG_META_TOGGLE_LOWER_DETAILS }, { "TOGGLE_CONTROL_BAR", GameMessage::MSG_META_TOGGLE_CONTROL_BAR }, { "TOGGLE_PLAYER_OBSERVER", GameMessage::MSG_META_TOGGLE_PLAYER_OBSERVER }, + { "CYCLE_HEALTH_BAR_MODE", GameMessage::MSG_META_CYCLE_HEALTH_BAR_MODE }, { "BEGIN_PATH_BUILD", GameMessage::MSG_META_BEGIN_PATH_BUILD }, { "END_PATH_BUILD", GameMessage::MSG_META_END_PATH_BUILD }, { "BEGIN_FORCEATTACK", GameMessage::MSG_META_BEGIN_FORCEATTACK }, @@ -860,6 +861,23 @@ void MetaMap::generateMetaMap() map->m_usableIn = COMMANDUSABLE_OBSERVER; } } +#if RTS_ZEROHOUR + { + // TheSuperHackers @feature Cycle the health bar display mode. Ctrl+` is unbound in + // retail and in the debug and demo command maps, so this takes a key nothing else wants. + // Note the tick key is layout dependent -- it is ` on a US keyboard but prints something + // else elsewhere -- however the binding is by scancode, so the same physical key works. + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_CYCLE_HEALTH_BAR_MODE); + if (map->m_key == MK_NONE) + { + map->m_key = MK_TICK; + map->m_transition = DOWN; + map->m_modState = CTRL; + // in game and while observing, but not in the menus, where health bars mean nothing + map->m_usableIn = (CommandUsableInType)(COMMANDUSABLE_GAME | COMMANDUSABLE_OBSERVER); + } + } +#endif // RTS_ZEROHOUR { // Is mostly useful for Generals. MetaMapRec *map = getMetaMapRec(GameMessage::MSG_META_TOGGLE_FAST_FORWARD_REPLAY); diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 89a5fa08f9d..b159618ce3d 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -142,6 +142,15 @@ 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 Client side health bar display preference, from Options.ini. + // Holds a HealthBarDisplayMode; stored as Int so this widely included header does not + // have to pull in OptionPreferences.h. + Int m_healthBarDisplayMode; + // TheSuperHackers @feature Print hit points beside the health bar. + Bool m_numericalHealth; + // TheSuperHackers @feature Keep ammo and passenger pips on screen when there is something + // to report, rather than only on selection or hover. + Bool m_smartPips; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; Int m_jpegQuality; // TheSuperHackers @feature Quality for JPEG screenshots. diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h index 7169b8cb51f..e775a497172 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h @@ -739,6 +739,8 @@ class Drawable : public Thing, //Perhaps we can move this out of Drawable??? public: static void killStaticImages(); + // TheSuperHackers @feature Free shared display strings before the manager is destroyed. + static void killStaticDisplayStrings(); #ifdef DIRTY_CONDITION_FLAGS // only for StDrawableDirtyStuffLocker! @@ -755,6 +757,9 @@ class Drawable : public Thing, void drawConstructPercent( const IRegion2D *healthBarRegion ); ///< display % construction complete void drawCaption( const IRegion2D *healthBarRegion ); ///< draw caption void drawAmmo( const IRegion2D *healthBarRegion ); ///< draw icons + // TheSuperHackers @feature hit points beside the bar (Options.ini: NumericalHealth) + void drawNumericalHealth( const IRegion2D *healthBarRegion, Real health, Real maxHealth, + Color color ); void drawContained( const IRegion2D *healthBarRegion ); ///< draw icons void drawVeterancy( const IRegion2D *healthBarRegion ); ///< draw veterency information diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index e862cd149d5..bf99c515eca 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1063,6 +1063,10 @@ GlobalData::GlobalData() #endif m_clientRetaliationModeEnabled = TRUE; //On by default. m_doubleClickAttackMove = FALSE; + // TheSuperHackers @feature Health bars behave exactly as they always have unless Options.ini says otherwise. + m_healthBarDisplayMode = HealthBarDisplayMode_Default; + m_numericalHealth = FALSE; + m_smartPips = FALSE; } @@ -1205,6 +1209,9 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_useAlternateMouse = optionPref.getAlternateMouseModeEnabled(); TheWritableGlobalData->m_useRightMouseScrollWithAlternateMouse = optionPref.getRightMouseScrollWithAlternateMouseEnabled(); TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled(); + TheWritableGlobalData->m_healthBarDisplayMode = optionPref.getHealthBarDisplayMode(); + TheWritableGlobalData->m_numericalHealth = optionPref.getNumericalHealthEnabled(); + TheWritableGlobalData->m_smartPips = optionPref.getSmartPipsEnabled(); TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled(); TheWritableGlobalData->m_jpegQuality = optionPref.getJpegQuality(); TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index 54dff3325f2..cbce0468878 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -42,6 +42,7 @@ #include "Common/GameState.h" #include "Common/GameUtility.h" #include "Common/GlobalData.h" +#include "Common/OptionPreferences.h" #include "Common/ModuleFactory.h" #include "Common/PerfTimer.h" #include "Common/Player.h" @@ -304,6 +305,26 @@ const Int MAX_ENABLED_MODULES = 16; s_animationTemplates = nullptr; } +// TheSuperHackers @feature One shared string for the numerical health text, rebuilt whenever the +// text changes. Unlike the command bar overlays there is no small fixed set of values to cache +// per object, and many bars draw per frame, so it is rebuilt as often as it is reused. It stays +// static purely to avoid allocating every call, and is returned to the manager before the manager +// itself is torn down -- see Drawable::killStaticDisplayStrings. +static DisplayString *s_healthString = nullptr; + +//------------------------------------------------------------------------------------------------- +/** Return the shared display strings to the manager. Must run before TheDisplayStringManager is + * destroyed, which asserts on any string still registered -- GameClient's destructor calls this + * right before deleting the manager, unlike killStaticImages, which runs after. */ +//------------------------------------------------------------------------------------------------- +/*static*/ void Drawable::killStaticDisplayStrings() +{ + if( s_healthString != nullptr && TheDisplayStringManager != nullptr ) + TheDisplayStringManager->freeDisplayString( s_healthString ); + + s_healthString = nullptr; +} + //------------------------------------------------------------------------------------------------- void Drawable::saturateRGB(RGBColor& color, Real factor) { @@ -2846,18 +2867,48 @@ void Drawable::drawAmmo( const IRegion2D *healthBarRegion ) { const Object *obj = getObject(); - if (!( - TheGlobalData->m_showObjectHealth && - (isSelected() || (TheInGameUI && (TheInGameUI->getMousedOverDrawableID() == getID()))) && - obj->getControllingPlayer() == rts::getObservedOrLocalPlayer() - )) + // The pips are left justified against the health bar, so without that region there is nowhere + // to put them. It is null when the object is off screen or has no health box. The selected or + // moused over test used to make a valid region implicit; SmartPips draws without that test, so + // check it outright rather than dereference null further down. + if (!healthBarRegion) return; + // TheSuperHackers @feature SmartPips shows a unit's ammo pips all the time rather than only + // while it is selected or moused over, so remaining ammo is readable at a glance. Restricted + // to the player's own units -- not allies, not enemies -- since standing ammo counts on units + // you do not control would be information the game does not otherwise give away. A unit with + // no shots left draws nothing at all, so the pips read as "this one is still loaded". + const Bool smartPips = TheGlobalData->m_smartPips; + + if (!smartPips) + { + if (!( + TheGlobalData->m_showObjectHealth && + (isSelected() || (TheInGameUI && (TheInGameUI->getMousedOverDrawableID() == getID()))) && + obj->getControllingPlayer() == rts::getObservedOrLocalPlayer() + )) + return; + } + else + { + if (obj->getControllingPlayer() != rts::getObservedOrLocalPlayer()) + return; + } + Int numTotal; Int numFull; if (!obj->getAmmoPipShowingInfo(numTotal, numFull)) return; + // TheSuperHackers @feature With SmartPips the pips are on screen the whole time, so an empty + // unit would otherwise sit under a permanent row of empty boxes. Draw nothing at all instead, + // which also makes an out of ammo unit obvious by the pips vanishing. Without SmartPips the + // pips only appear on selection or hover, where showing the empties is the informative thing + // to do, so this does not apply. + if (smartPips && numFull <= 0) + return; + if (!s_fullAmmo || !s_emptyAmmo) return; @@ -2904,13 +2955,35 @@ void Drawable::drawContained( const IRegion2D *healthBarRegion ) if (!container) return; - if (!( - TheGlobalData->m_showObjectHealth && - (isSelected() || (TheInGameUI && (TheInGameUI->getMousedOverDrawableID() == getID()))) && - obj->getControllingPlayer() == rts::getObservedOrLocalPlayer() - )) + // The pips are left justified against the health bar, so without that region there is nowhere + // to put them. It is null when the object is off screen or has no health box. The selected or + // moused over test used to make a valid region implicit; SmartPips draws without that test, so + // check it outright rather than dereference null further down. + if (!healthBarRegion) return; + // TheSuperHackers @feature SmartPips keeps the passenger pips on screen rather than showing + // them only on selection or hover, matching what it does for ammo pips. Own units only -- not + // allies, not enemies -- so this never reveals cargo the player could not already see. An + // empty transport draws nothing either way, thanks to the numFull check below, so the pips + // read as "this one is carrying someone". + const Bool smartPips = TheGlobalData->m_smartPips; + + if (!smartPips) + { + if (!( + TheGlobalData->m_showObjectHealth && + (isSelected() || (TheInGameUI && (TheInGameUI->getMousedOverDrawableID() == getID()))) && + obj->getControllingPlayer() == rts::getObservedOrLocalPlayer() + )) + return; + } + else + { + if (obj->getControllingPlayer() != rts::getObservedOrLocalPlayer()) + return; + } + Int numTotal; Int numFull; if (!container->getContainerPipsToShow(numTotal, numFull)) @@ -2920,22 +2993,40 @@ void Drawable::drawContained( const IRegion2D *healthBarRegion ) if (numFull == 0) return; + // TheSuperHackers @fix SmartPips draws these every frame for every owned container rather than + // only for a selected one, which exposed two assumptions this loop was quietly making. + // + // The list can hold a null, and an entry can be mid removal while a transport is unloading, so + // each one is checked before use rather than dereferenced outright. And numFull comes from + // getContainerPipsToShow, which counts getContainCount() plus getExtraSlotsInUse() and may be + // redirected to a different module entirely -- OverlordContain forwards it to its sub container + // -- while this list is always the outer container's own. The two therefore need not agree, so + // numInfantry is clamped to numFull rather than assumed to be within it. Int numInfantry = 0; const ContainedItemsList* contained = container->getContainedItemsList(); if (contained) { for (ContainedItemsList::const_iterator it = contained->begin(); it != contained->end(); ++it) { - if ((*it)->isKindOf(KINDOF_INFANTRY)) + const Object* item = *it; + if (item && item->isKindOf(KINDOF_INFANTRY)) ++numInfantry; } } + if (numInfantry > numFull) + numInfantry = numFull; + #ifdef SCALE_ICONS_WITH_ZOOM_ML Real scale = TheGlobalData->m_ammoPipScaleFactor / CLAMP_ICON_ZOOM_FACTOR( TheTacticalView->getZoom() ); #else Real scale = 1.0f; #endif + // drawAmmo guards its images the same way. Previously a missing pip image would only crash on + // selecting a transport; with SmartPips it would crash on sight of one. + if (!s_fullContainer || !s_emptyContainer) + return; + Int boxWidth = REAL_TO_INT(s_emptyContainer->getImageWidth() * scale); Int boxHeight = REAL_TO_INT(s_emptyContainer->getImageHeight() * scale); const Int SPACING = 1; @@ -3764,6 +3855,90 @@ void Drawable::drawVeterancy( const IRegion2D *healthBarRegion ) // ------------------------------------------------------------------------------------------------ /** Draw health bar information for drawable */ // ------------------------------------------------------------------------------------------------ +// TheSuperHackers @feature Health bar display modes (Options.ini: HealthBarDisplayMode). +/** + * Should this object show a health bar even though it is neither selected nor moused over? + * + * Purely a client side display question -- it reads state but never changes any, so it cannot + * affect game logic, multiplayer sync or replays. + */ +static Bool isHealthBarAlwaysVisible( const Object *obj, Real healthRatio ) +{ + const Int mode = TheGlobalData->m_healthBarDisplayMode; + + if( mode == HealthBarDisplayMode_Classic ) + return FALSE; + + // never label a corpse or a piece of scenery, in any mode + if( obj->isEffectivelyDead() ) + return FALSE; + + if( obj->isKindOf( KINDOF_PROJECTILE ) + || obj->isKindOf( KINDOF_SHRUBBERY ) + || obj->isKindOf( KINDOF_OPTIMIZED_TREE ) + || obj->isKindOf( KINDOF_DRAWABLE_ONLY ) + || obj->isKindOf( KINDOF_INERT ) + || obj->isKindOf( KINDOF_UNATTACKABLE ) + || obj->isKindOf( KINDOF_MINE ) + || obj->isKindOf( KINDOF_NO_SELECT ) ) + return FALSE; + + if( mode == HealthBarDisplayMode_Damaged ) + return healthRatio < 1.0f; + + // HealthBarDisplayMode_Always -- real combatants and buildings only, so the map does not + // fill up with bars over rocks, crates and civilian props. + return obj->isKindOf( KINDOF_STRUCTURE ) || obj->getAI() != nullptr; +} + +//------------------------------------------------------------------------------------------------- +// TheSuperHackers @feature Print the hit points beside the health bar (Options.ini: +// NumericalHealth). Called from drawHealthBar once the bar itself is down, so every rule that +// decides whether a bar is drawn -- selection, mouseover, HealthBarDisplayMode -- already applies. +//------------------------------------------------------------------------------------------------- +void Drawable::drawNumericalHealth( const IRegion2D *healthBarRegion, Real health, Real maxHealth, + Color color ) +{ + if( healthBarRegion == nullptr || TheDisplayStringManager == nullptr ) + return; + + if( s_healthString == nullptr ) + { + s_healthString = TheDisplayStringManager->newDisplayString(); + if( s_healthString == nullptr ) + return; + + // Small on purpose: in Always mode this is drawn over every unit on screen at once, so it + // has to annotate the bar rather than compete with it. + Int pointSize = 6; + if( TheGlobalLanguageData ) + pointSize = TheGlobalLanguageData->adjustFontSize( pointSize ); + s_healthString->setFont( TheFontLibrary->getFont( AsciiString( "Arial" ), pointSize, FALSE ) ); + } + + // Round rather than truncate, so a sliver of health left does not read as 0 next to a unit + // that is plainly still alive. + const Int shownHealth = REAL_TO_INT( health + 0.5f ); + const Int shownMax = REAL_TO_INT( maxHealth + 0.5f ); + + UnicodeString text; + text.format( L"%d/%d", shownHealth, shownMax ); + s_healthString->setText( text ); + + Int width, height; + s_healthString->getSize( &width, &height ); + + // just past the right end of the bar, vertically centred on it + const Int healthBoxHeight = max( 3, healthBarRegion->hi.y - healthBarRegion->lo.y ); + const Int textX = healthBarRegion->hi.x + 2; + const Int textY = healthBarRegion->lo.y + ( healthBoxHeight / 2 ) - ( height / 2 ); + + // Black drop shadow rather than a backdrop plate: the number sits over the battlefield rather + // than over a cameo, so a filled box would be far more intrusive than the bar it annotates. + s_healthString->draw( textX, textY, color, GameMakeColor( 0, 0, 0, 255 ) ); +} + +//------------------------------------------------------------------------------------------------- void Drawable::drawHealthBar(const IRegion2D* healthBarRegion) { if (!healthBarRegion) @@ -3771,11 +3946,14 @@ void Drawable::drawHealthBar(const IRegion2D* healthBarRegion) // // only draw health for selected drawables and drawables that have been moused over - // by the cursor + // by the cursor. TheSuperHackers @feature Options.ini HealthBarDisplayMode can widen this + // to also cover damaged objects, or every unit and structure. // - if( TheGlobalData->m_showObjectHealth && - (isSelected() || (TheInGameUI && (TheInGameUI->getMousedOverDrawableID() == getID()))) ) + if( TheGlobalData->m_showObjectHealth ) { + const Bool classicallyVisible = isSelected() + || (TheInGameUI && (TheInGameUI->getMousedOverDrawableID() == getID())); + Object *obj = getObject(); // if no object, nothing to do @@ -3805,6 +3983,12 @@ void Drawable::drawHealthBar(const IRegion2D* healthBarRegion) // what is our health ratio Real healthRatio = health / maxHealth; + // TheSuperHackers @feature Decide whether this object gets a bar it would not classically get. + // Objects hidden, stealthed or shrouded never reach here at all -- drawablePostDraw filters + // those out before drawIconUI is ever called. + if( !classicallyVisible && !isHealthBarAlwaysVisible( obj, healthRatio ) ) + return; + // // what color will we use for the health bar based on our ratio, this makes it // slowly go from green to red, (or from blue to cyan if under construction, or disabled) @@ -3879,6 +4063,13 @@ void Drawable::drawHealthBar(const IRegion2D* healthBarRegion) TheDisplay->drawFillRect( healthBarRegion->lo.x + 1, healthBarRegion->lo.y + 1, (healthBoxWidth - 2) * healthRatio, healthBoxHeight - 2, color ); + + // TheSuperHackers @feature NumericalHealth prints the hit points just past the end of the + // bar. It reuses the bar's own colour, so the number carries the same red to green reading + // at a glance, and it hangs off every path that got this far -- which means it follows + // HealthBarDisplayMode for free and appears exactly where a bar appears. + if( TheGlobalData->m_numericalHealth ) + drawNumericalHealth( healthBarRegion, health, maxHealth, color ); } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp index af5f407209b..32de65faa39 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp @@ -228,6 +228,10 @@ GameClient::~GameClient() delete TheKeyboard; TheKeyboard = nullptr; + // TheSuperHackers @feature The shared numerical health string must go back to the manager + // while the manager is still alive; its destructor asserts on any string left registered. + Drawable::killStaticDisplayStrings(); + delete TheDisplayStringManager; TheDisplayStringManager = nullptr;