// Copyright IHY. Universal Bars plugin. #include "UniversalBarWidget.h" #include "Components/Image.h" #include "Materials/MaterialInstanceDynamic.h" UUniversalBarWidget::UUniversalBarWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { } void UUniversalBarWidget::NativeConstruct() { Super::NativeConstruct(); EnsureMID(); if (BarMID) { BarMID->SetScalarParameterValue(TEXT("FillPercent"), DisplayFill); BarMID->SetScalarParameterValue(TEXT("DelayedFillPercent"), DelayedFill); } PushZones(true); } UMaterialInstanceDynamic* UUniversalBarWidget::EnsureMID() { if (BarMID || !BarImage) { return BarMID; } BarMID = BarImage->GetDynamicMaterial(); return BarMID; } void UUniversalBarWidget::SetFillPercent(float NewValue, bool bAnimate) { EnsureMID(); NewValue = FMath::Clamp(NewValue, 0.f, 1.f); if (!bAnimate) { TargetFill = DisplayFill = DelayedFill = NewValue; if (BarMID) { BarMID->SetScalarParameterValue(TEXT("FillPercent"), DisplayFill); BarMID->SetScalarParameterValue(TEXT("DelayedFillPercent"), DelayedFill); } return; } const bool bDown = NewValue < TargetFill - KINDA_SMALL_NUMBER; const bool bUp = NewValue > TargetFill + KINDA_SMALL_NUMBER; TargetFill = NewValue; if (bDown) { // instant bright drop; the lighter tail holds, then drains (damage lag) DisplayFill = NewValue; DelayTimer = DelayedDelay; if (BarMID) { BarMID->SetScalarParameterValue(TEXT("FillPercent"), DisplayFill); } if (bAutoFlashOnChange) { PlayDamageFlash(); } } else if (bUp) { // show the incoming amount immediately as the lighter band; bright fill eases up into it DelayedFill = FMath::Max(DelayedFill, NewValue); if (BarMID) { BarMID->SetScalarParameterValue(TEXT("DelayedFillPercent"), DelayedFill); } if (bAutoFlashOnChange) { PlayHealFlash(); } } } void UUniversalBarWidget::SetShieldPercent(float NewValue) { EnsureMID(); if (BarMID) { BarMID->SetScalarParameterValue(TEXT("ShieldPercent"), FMath::Clamp(NewValue, 0.f, 1.f)); } } void UUniversalBarWidget::PlayDamageFlash() { PlayFlash(DamageFlashColor); } void UUniversalBarWidget::PlayHealFlash() { PlayFlash(HealFlashColor); } void UUniversalBarWidget::PlayFlash(FLinearColor Color) { EnsureMID(); PendingFlashColor = Color; if (BarMID) { BarMID->SetVectorParameterValue(TEXT("FlashColor"), Color); } FlashAmount = 1.f; bFlashing = true; } void UUniversalBarWidget::SetBarColors(FLinearColor Fill, FLinearColor Empty, FLinearColor Border, FLinearColor Delayed) { EnsureMID(); if (!BarMID) { return; } BarMID->SetVectorParameterValue(TEXT("FillColor"), Fill); BarMID->SetVectorParameterValue(TEXT("EmptyColor"), Empty); BarMID->SetVectorParameterValue(TEXT("BorderColor"), Border); BarMID->SetVectorParameterValue(TEXT("DelayedFillColor"), Delayed); } void UUniversalBarWidget::SetSegmentCount(int32 Count) { EnsureMID(); if (!BarMID) { return; } BarMID->SetScalarParameterValue(TEXT("SegmentCount"), static_cast(FMath::Max(0, Count))); BarMID->SetScalarParameterValue(TEXT("SegmentDividerOpacity"), Count > 0 ? 0.85f : 0.f); } void UUniversalBarWidget::SetPenaltyZone(FName Id, float Width, FLinearColor Color) { Width = FMath::Clamp(Width, 0.f, 1.f); for (FBarZone& Zone : PenaltyZones) { if (Zone.Id == Id) { Zone.Width = Width; Zone.Color = Color; return; // tick eases DisplayWidth toward the new Width } } FBarZone& NewZone = PenaltyZones[PenaltyZones.AddDefaulted()]; NewZone.Id = Id; NewZone.Width = Width; NewZone.Color = Color; } void UUniversalBarWidget::SetPenaltyZoneWidth(FName Id, float Width) { Width = FMath::Clamp(Width, 0.f, 1.f); for (FBarZone& Zone : PenaltyZones) { if (Zone.Id == Id) { Zone.Width = Width; return; } } } void UUniversalBarWidget::RemovePenaltyZone(FName Id) { // Ease to zero, then drop on next tick when fully collapsed. for (FBarZone& Zone : PenaltyZones) { if (Zone.Id == Id) { Zone.Width = 0.f; return; } } } void UUniversalBarWidget::ClearPenaltyZones() { for (FBarZone& Zone : PenaltyZones) { Zone.Width = 0.f; } } void UUniversalBarWidget::PushZones(bool bImmediate) { EnsureMID(); if (!BarMID) { return; } const int32 Count = FMath::Min(PenaltyZones.Num(), MaxMaterialZones); float Total = 0.f; for (int32 i = 0; i < Count; ++i) { if (bImmediate) { PenaltyZones[i].DisplayWidth = PenaltyZones[i].Width; } Total += FMath::Clamp(PenaltyZones[i].DisplayWidth, 0.f, 1.f); } Total = FMath::Clamp(Total, 0.f, 1.f); const float MaxFill = 1.f - Total; BarMID->SetScalarParameterValue(TEXT("MaxFillPercent"), MaxFill); BarMID->SetScalarParameterValue(TEXT("PenaltyCount"), static_cast(Count)); float Bound = MaxFill; static const TCHAR* EndNames[MaxMaterialZones] = { TEXT("PenaltyEnd1"), TEXT("PenaltyEnd2"), TEXT("PenaltyEnd3"), TEXT("PenaltyEnd4") }; static const TCHAR* ColorNames[MaxMaterialZones] = { TEXT("PenaltyColor1"), TEXT("PenaltyColor2"), TEXT("PenaltyColor3"), TEXT("PenaltyColor4") }; for (int32 i = 0; i < MaxMaterialZones; ++i) { if (i < Count) { Bound = FMath::Min(1.f, Bound + FMath::Clamp(PenaltyZones[i].DisplayWidth, 0.f, 1.f)); BarMID->SetScalarParameterValue(EndNames[i], Bound); BarMID->SetVectorParameterValue(ColorNames[i], PenaltyZones[i].Color); } else { BarMID->SetScalarParameterValue(EndNames[i], 1.f); } } } float UUniversalBarWidget::GetUsableEnd() const { float Total = 0.f; const int32 N = FMath::Min(PenaltyZones.Num(), MaxMaterialZones); for (int32 i = 0; i < N; ++i) { Total += FMath::Clamp(PenaltyZones[i].DisplayWidth, 0.f, 1.f); } return FMath::Clamp(1.f - Total, 0.f, 1.f); } float UUniversalBarWidget::GetZoneCenter(FName Id) const { float Edge = GetUsableEnd(); const int32 N = FMath::Min(PenaltyZones.Num(), MaxMaterialZones); for (int32 i = 0; i < N; ++i) { const float W = FMath::Clamp(PenaltyZones[i].DisplayWidth, 0.f, 1.f); if (PenaltyZones[i].Id == Id) { return Edge + W * 0.5f; } Edge += W; } return -1.f; } void UUniversalBarWidget::SetScalar(FName Param, float Value) { EnsureMID(); if (BarMID) { BarMID->SetScalarParameterValue(Param, Value); } } void UUniversalBarWidget::SetVector(FName Param, FLinearColor Value) { EnsureMID(); if (BarMID) { BarMID->SetVectorParameterValue(Param, Value); } } void UUniversalBarWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime) { Super::NativeTick(MyGeometry, InDeltaTime); if (!BarMID) { return; } // bright fill eases toward target (smooth rise on heal; already snapped on damage) if (!FMath::IsNearlyEqual(DisplayFill, TargetFill, 1e-3f)) { DisplayFill = FMath::FInterpTo(DisplayFill, TargetFill, InDeltaTime, FillRiseSpeed); BarMID->SetScalarParameterValue(TEXT("FillPercent"), DisplayFill); } // lighter "lag" tail: holds, then drains toward target (damage). Never below the bright fill. const float DelayedTarget = FMath::Max(TargetFill, DisplayFill); if (!FMath::IsNearlyEqual(DelayedFill, DelayedTarget, 1e-3f)) { if (DelayTimer > 0.f) { DelayTimer -= InDeltaTime; } else { DelayedFill = FMath::FInterpTo(DelayedFill, DelayedTarget, InDeltaTime, DelayedFallSpeed); BarMID->SetScalarParameterValue(TEXT("DelayedFillPercent"), DelayedFill); } } // flash decay if (bFlashing) { FlashAmount = FMath::FInterpTo(FlashAmount, 0.f, InDeltaTime, FlashSpeed); BarMID->SetScalarParameterValue(TEXT("FlashAmount"), FlashAmount); if (FlashAmount < 0.01f) { FlashAmount = 0.f; bFlashing = false; BarMID->SetScalarParameterValue(TEXT("FlashAmount"), 0.f); } } // low-value pulse const float PulseTarget = (TargetFill < LowThreshold) ? 1.f : 0.f; if (!FMath::IsNearlyEqual(PulseAmount, PulseTarget, 1e-3f)) { PulseAmount = FMath::FInterpTo(PulseAmount, PulseTarget, InDeltaTime, 6.f); BarMID->SetScalarParameterValue(TEXT("LowHPPulseAmount"), PulseAmount); } // penalty zone widths ease toward target; drop fully-collapsed removed zones bool bZonesDirty = false; bool bNeedCompact = false; for (FBarZone& Zone : PenaltyZones) { if (!FMath::IsNearlyEqual(Zone.DisplayWidth, Zone.Width, 1e-4f)) { Zone.DisplayWidth = FMath::FInterpTo(Zone.DisplayWidth, Zone.Width, InDeltaTime, ZoneInterpSpeed); bZonesDirty = true; } if (Zone.Width <= 0.f && Zone.DisplayWidth <= 1e-3f) { bNeedCompact = true; } } if (bZonesDirty) { PushZones(false); } if (bNeedCompact) { PenaltyZones.RemoveAll([](const FBarZone& Z) { return Z.Width <= 0.f && Z.DisplayWidth <= 1e-3f; }); PushZones(false); } }