diff --git a/Development/Src/TempestMp/Classes/Siege.uc b/Development/Src/TempestMp/Classes/Siege.uc index e5603a94..057edc86 100644 --- a/Development/Src/TempestMp/Classes/Siege.uc +++ b/Development/Src/TempestMp/Classes/Siege.uc @@ -360,4 +360,6 @@ function SetupSpectator(TmProxyActor ProxyActor, TgPlayerController PC, Incoming SPC.SpectatorSetViewTarget(TargetPawn); ProxyActor.ClientConsoleCommand("setreadytoplay"); + ProxyActor.ClientConsoleCommand("spectogglefirstperson"); + } \ No newline at end of file diff --git a/Development/Src/TmCore/Classes/TmCameraModule_SpectatorFirstPerson.uc b/Development/Src/TmCore/Classes/TmCameraModule_SpectatorFirstPerson.uc new file mode 100644 index 00000000..a377fc54 --- /dev/null +++ b/Development/Src/TmCore/Classes/TmCameraModule_SpectatorFirstPerson.uc @@ -0,0 +1,146 @@ +class TmCameraModule_SpectatorFirstPerson extends TgCameraModule + config(Game); + +// True while the native gate reports a third person tick (ability, mount...). +// We keep this module active and pose the camera ourselves. +var bool m_bForced3P; + +// Pose transition state, mirroring TgCameraModule_SpectatorThirdPerson. +var transient TPOV OldViewpoint; +var transient float RemainingBlendTime; +var transient float TotalBlendTime; + +// Called by the spectator controller whenever the native gate flips between +// first and third person; kicks off a smooth pose blend instead of snapping. +function SetForced3P(bool bNewForced3P) +{ + if (m_bForced3P == bNewForced3P) + return; + + m_bForced3P = bNewForced3P; + if (PlayerCamera != none) + { + OldViewpoint = PlayerCamera.CameraCache.POV; + RemainingBlendTime = 0.25; + TotalBlendTime = 0.25; + } +} + +function UpdateCamera(Pawn P, TgPlayerCamera CameraActor, float DeltaTime, out TViewTarget OutVT) +{ + local Vector camOrigin, boneLoc, Offset; + local Rotator CamRot; + local TPOV TargetPOV; + local float DurationPct, BlendPct; + local TgPawn TgP; + local TgWeaponMeshActor WMA; + + if (CameraActor == none || CameraActor.PlayerOwner == none) + return; + + TgP = TgPawn(OutVT.Target); + if (TgP != none) + WMA = TgP.m_WeaponMesh; + + CamRot = CameraActor.PlayerOwner.Rotation; + if (TgP != none) + { + CamRot = TgP.Rotation; + CamRot.Pitch = TgP.m_nSmoothedRemoteViewPitch; + } + + TargetPOV.Rotation = CamRot; + + if (m_bForced3P && TgP != none) + { + // Behind-view pose mirroring the stock spectator third person module, + // anchored to a stable location instead of animating bones: skeleton + // anchors make the camera bob violently with mount/riding animations. + camOrigin = TgP.Location; + if (int(TgP.c_LocalPolymorph) == 0) + camOrigin.Z += TgP.EyeHeight; + else + camOrigin.Z += 20.0; + Offset.X = -142.0; + Offset.Z = 25.0; + TargetPOV.Location = camOrigin + (Offset >> CamRot); + } + else if (TgP != none) + { + // Prefer the rig's camera bone (same source as the native first person + // path), but only trust it when the rig was built near the pawn. + boneLoc = vect(0.0, 0.0, 0.0); + if (WMA != none && WMA.m_bUseSocketForCameraAttach && WMA.CanUseCameraBone() && WMA.m_WeaponMesh1P != none) + boneLoc = WMA.m_WeaponMesh1P.GetBoneLocation('Camera_bn'); + + if (VSizeSq(boneLoc - TgP.Location) < 90000.0) + { + TargetPOV.Location = boneLoc; + TargetPOV.Rotation = OrthoRotation( + WMA.m_WeaponMesh1P.GetBoneAxis('Camera_bn', 4) * -1.0, + WMA.m_WeaponMesh1P.GetBoneAxis('Camera_bn', 1), + WMA.m_WeaponMesh1P.GetBoneAxis('Camera_bn', 2) * -1.0); + } + else + { + TargetPOV.Location = GetBodyAttachPoint(TgP) + TgP.WalkBob; + } + } + else if (OutVT.Target != none) + { + TargetPOV.Location = OutVT.Target.Location; + } + + // Keep the pipeline's FOV; only pose blends below. + TargetPOV.FOV = OutVT.POV.FOV; + + // Smooth pose transition, same math as TgCameraModule_SpectatorThirdPerson: + // blend from the frozen previous POV toward the moving target with a cubic + // ease while a flip is in flight, then track the target directly. + if (RemainingBlendTime > 0.0) + { + RemainingBlendTime -= DeltaTime; + if (RemainingBlendTime < 0.0) + RemainingBlendTime = 0.0; + DurationPct = (TotalBlendTime - RemainingBlendTime) / TotalBlendTime; + BlendPct = FCubicInterp(0.0, 0.0, 1.0, 0.0, DurationPct); + OutVT.POV.Location = VLerp(OldViewpoint.Location, TargetPOV.Location, BlendPct); + OutVT.POV.Rotation = RLerp(OldViewpoint.Rotation, TargetPOV.Rotation, BlendPct, true); + OutVT.POV.FOV = Lerp(OldViewpoint.FOV, TargetPOV.FOV, BlendPct); + } + else + { + OutVT.POV = TargetPOV; + } +} + +// Same anchor math as TgCameraModule_ThirdPerson.GetCamAttachPoint's fallback. +simulated function Vector GetBodyAttachPoint(TgPawn TgP) +{ + local Vector NewCameraLoc; + + NewCameraLoc = TgP.Location; + if (TgP.Mesh != none) + { + if (TgP.Mesh.MatchRefBone('EffectsSocket') != -1) + { + NewCameraLoc = TgP.Mesh.GetBoneLocation('EffectsSocket'); + NewCameraLoc.Z += TgP.GetCollisionHeight(); + } + else if (TgP.Mesh.MatchRefBone('Root') != -1) + { + NewCameraLoc = TgP.Mesh.GetBoneLocation('Root'); + NewCameraLoc.Z += TgP.GetCollisionHeight(); + } + else + { + NewCameraLoc = TgP.Mesh.GetBoneLocation(TgP.Mesh.GetBoneName(0)); + NewCameraLoc.Z += TgP.GetCollisionHeight(); + } + } + if (int(TgP.c_LocalPolymorph) == 0) + NewCameraLoc.Z += TgP.EyeHeight; + else + NewCameraLoc.Z += 20.0; + return NewCameraLoc; +} diff --git a/Development/Src/TmCore/Classes/TmGameViewportClient.uc b/Development/Src/TmCore/Classes/TmGameViewportClient.uc index a1302477..f94dc01e 100644 --- a/Development/Src/TmCore/Classes/TmGameViewportClient.uc +++ b/Development/Src/TmCore/Classes/TmGameViewportClient.uc @@ -36,6 +36,7 @@ function PostRender(Canvas Canvas) if (TmSpectatorController(PC) != none) { + TmSpectatorController(PC).UpdateFirstPersonNudge(); TmSpectatorController(PC).TickSpectatorPlayerHUD(); TmSpectatorController(PC).TickSpectatorTeamHUD(); TmSpectatorController(PC).TickBurnsHud(); diff --git a/Development/Src/TmCore/Classes/TmSpectatorController.uc b/Development/Src/TmCore/Classes/TmSpectatorController.uc index 2506282c..7e32757b 100644 --- a/Development/Src/TmCore/Classes/TmSpectatorController.uc +++ b/Development/Src/TmCore/Classes/TmSpectatorController.uc @@ -13,6 +13,17 @@ var transient bool bClonedHUD; var transient int LastClonedTickCount; var transient int bBorderBuilt; +// Pawn currently forced into the client-side first person rig, if any. +var transient TgPawn m_NudgedFpPawn; +// Last known mounted state of the nudged pawn (edge detection for the manual mount presentation). +var transient bool m_bWasMounted; +// True while inside a mount/emote third person window. +var transient bool m_bWasWindow3P; +// If false, disable the server-side relevancy viewpoint override. +var config bool c_bTrackViewTargetForRelevancy; + +const ABILITY_SLOTS = 5; + simulated function ForwardToSpectatingMatch() { super.ForwardToSpectatingMatch(); @@ -26,11 +37,371 @@ public function Class GetHudClass(Class pNewHudType) return Class'TgClient.TgGameHUD'; } +exec function SpecToggleFirstPerson() +{ + if (int(m_CameraMode) == int(SpectatorCameraMode.SpecCam_FollowFirstPerson)) + SetSpectatorCameraMode(SpectatorCameraMode.SpecCam_FollowThirdPerson); + else + SetSpectatorCameraMode(SpectatorCameraMode.SpecCam_FollowFirstPerson); +} + +// Same bookkeeping as the stock mode 3 handler, but swaps in our own camera +// module so the POV is driven from script instead of the native gate. +exec function SetSpectatorCameraMode(TgSpectatorController.SpectatorCameraMode Mode, optional bool bCameraTween = false) +{ + if (int(Mode) != int(SpectatorCameraMode.SpecCam_FollowFirstPerson)) + { + super.SetSpectatorCameraMode(Mode, bCameraTween); + ClearFirstPersonNudge(); + return; + } + + if (int(Mode) == int(m_CameraMode)) + return; + + m_CameraMode = Mode; + m_bIsMapSquashed = false; + if (bCameraTween) + TgPlayerCamera(PlayerCamera).SwitchCameras(class'TmCore.TmCameraModule_SpectatorFirstPerson', 0.2); + else + TgPlayerCamera(PlayerCamera).SwitchCameras(class'TmCore.TmCameraModule_SpectatorFirstPerson'); +} + +// Opens the native first person gate (ATgPawn::ShouldBeFirstPersonThisTick, +// live path) for the followed pawn by assigning the local spectator +// controller to its Controller reference client-side. Pawn.Controller is +// owner-only replicated so the assignment never leaves this machine and the +// real owner keeps authoritative control server-side. Attaching a LOCAL +// controller is deliberate: many effect playback paths branch on +// IsLocallyControlled(), and the local branches are the ones that reliably +// play tracers/impacts/explosions for the spectated pawn (non-local stand-in +// controllers suppress them). +// Wants3P() returns false for us because our FP camera module does not derive +// from TgCameraModule_ThirdPerson and no 3P camera posture is latched, which +// opens the gate and makes the engine build the entire native first person +// rig (viewmodel, Camera_bn eye position) exactly like the followed player +// sees. +simulated function UpdateFirstPersonNudge() +{ + local TgPawn ViewPawn; + local TgRepInfo_Player FollowedPRI; + local bool bNativeFP; + + if (m_CameraMode != SpectatorCameraMode.SpecCam_FollowFirstPerson) + { + ClearFirstPersonNudge(); + return; + } + + ViewPawn = TgPawn(GetViewTarget()); + // Never touch pawns we have authority over (listen server): the fake + // Controller assignment is only safe on simulated proxies. + if (ViewPawn == none || !ViewPawn.IsAliveAndWell() || ViewPawn.Role >= ROLE_Authority) + { + ClearFirstPersonNudge(); + return; + } + + if (m_NudgedFpPawn != none && m_NudgedFpPawn != ViewPawn) + ClearFirstPersonNudge(); + + m_NudgedFpPawn = ViewPawn; + ViewPawn.Controller = self; + m_bBehindView = false; + + // Mounts have no native signal in a spectator context (their posture is + // pushed on the owning player's controller only, and the gate's device + // loop cannot see them), so detect them from replicated pawn state. The + // native r_bIsMounted replication handler also refuses to build the horse + // while the pawn looks locally controlled, so the mount presentation is + // driven manually - strictly on transitions, otherwise the dismount + // presentation would replay every frame. + if (ViewPawn.r_bIsMounted != m_bWasMounted) + { + m_bWasMounted = ViewPawn.r_bIsMounted; + if (ViewPawn.r_bIsMounted) + ViewPawn.PlayMountingEffects(false, ViewPawn.r_bUseMountPosture); + else + ViewPawn.StopMountingEffects(true, ViewPawn.r_bUseMountPosture); + } + + + bNativeFP = !ViewPawn.r_bIsMounted + && !IsForced3PAbilityDevice(ViewPawn) + && ViewPawn.ShouldBeFirstPersonThisTick(); + m_bBehindView = !bNativeFP; + + SetRealModelHidden(ViewPawn, bNativeFP); + SetRigVisible(ViewPawn, bNativeFP); + SetForced3PPose(!bNativeFP); +} + +//TODO Cinnamon: Find a better way to do this + +// Some movement abilities never close the native gate, and DEVICE OBJECTS do +// not replicate to spectators at all (GetDeviceByEqPoint resolves to none on +// this connection - verified by diagnostics). What does replicate is the form +// STATE NAME per equip point plus the pawn class itself, so third person +// windows are driven by a per-champion slot table. +// +// Champion internal class names: Cassie=TgPawn_Cassie, Lian=TgPawn_Princess, +// Ash=TgPawn_Juggernaut(unverified), Barik=TgPawn_Barik, +// Fernando=TgPawn_Fernando, Makoa=TgPawn_Makoa, Androxus=TgPawn_Androxus, +// Evie=TgPawn_Evie, Zhin=TgPawn_Darklord, Grohk=TgPawn_Grohk, +// Maeve=TgPawn_Blades, Willo=TgPawn_Fairy, Sha Lin=TgPawn_Longbow, +// Seris/Torvald=TgPawn_Oracle(?). +simulated function bool IsForced3PAbilityDevice(TgPawn ViewPawn) +{ + local int Mask; + + //Cinnamon: 7 = emote. Handle emotes before anything else. + if (ViewPawn.c_EquipFormState[7] == 'DeviceFiring' || ViewPawn.c_EquipFormState[7] == 'DeviceBuilding') + return true; + + // Bit 16 = RMB ability, bit 4 = F ability, bit 3 = Q ability, bit 2 = E ability. + Mask = GetForce3PSlotMask(ViewPawn) | GetForce3PUltSlotMask(ViewPawn); + if (Mask == 0) + return false; + + if ((Mask & (1 << 16)) != 0 && (ViewPawn.c_EquipFormState[16] == 'DeviceFiring' || ViewPawn.c_EquipFormState[16] == 'DeviceBuilding')) + return true; + if ((Mask & (1 << 4)) != 0 && (ViewPawn.c_EquipFormState[4] == 'DeviceFiring' || ViewPawn.c_EquipFormState[4] == 'DeviceBuilding')) + return true; + if ((Mask & (1 << 3)) != 0 && (ViewPawn.c_EquipFormState[3] == 'DeviceFiring' || ViewPawn.c_EquipFormState[3] == 'DeviceBuilding')) + return true; + if ((Mask & (1 << 2)) != 0 && (ViewPawn.c_EquipFormState[2] == 'DeviceFiring' || ViewPawn.c_EquipFormState[2] == 'DeviceBuilding')) + return true; + return false; +} + +//TODO Cinnamon: Find a better way to do this + +// Ultimates (equip point 2) that play out in third person. Jenos' Through +// Time and Space already closes the gate natively and needs no entry. +simulated function int GetForce3PUltSlotMask(TgPawn ViewPawn) +{ + if (ViewPawn.IsA('TgPawn_Princess') // Lian + || ViewPawn.IsA('TgPawn_Flak') // Ash + || ViewPawn.IsA('TgPawn_Knight') // Fernando + || ViewPawn.IsA('TgPawn_Darklord') // Zhin + || ViewPawn.IsA('TgPawn_Shaman') // Grohk + || ViewPawn.IsA('TgPawn_BombKing') // Bomb King + || ViewPawn.IsA('TgPawn_Drogoz') // Drogoz + || ViewPawn.IsA('TgPawn_BarrierTank') // Inara + || ViewPawn.IsA('TgPawn_Druid')) // Grover + return 1 << 2; + + return 0; +} + + +//TODO Cinnamon: Find a better way to do this +simulated function int GetForce3PSlotMask(TgPawn ViewPawn) +{ + // Champion internal class names (runtime): Cassie=Huntress, + // Fernando=Knight, Barik=Engineer, Grohk=Shaman, Evie=Mage, + // Lian=Princess, Ash=Flak, Makoa=Makoa, Androxus=Androxus, + // Zhin=Darklord, Maeve=Blades, Willo=Fairy, Sha Lin=Longbow, + // Seris=Oracle, Strix=Owl, Inara=BarrierTank, Terminus=Lazarus, + // Tyra=Salty, Viktor=BRmale/Robosarge, Pip=Alchemist, Vivian=Churchill, + // Moji=Rider, Lex=Lawman, Grover=Druid, Atlas=TimeTraveler, + // Koga=Ninja, Furia=Angel, Dredge=Pirate, Ying=Illusionist, + // Khan=Vanguard. Old export aliases kept as fallbacks where they exist. + + // Zhin forces third person on both F and Q. + if (ViewPawn.IsA('TgPawn_Darklord')) + return (1 << 4) | (1 << 3); + + // Fernando forces it on F and RMB + if(ViewPawn.IsA('TgPawn_Fernando') || ViewPawn.IsA('TgPawn_Knight')) + return (1 << 4) | (1 << 16); + + // Androxus forces it on Q only. + if (ViewPawn.IsA('TgPawn_Androxus')) + return (1 << 3); + + // Everyone else with a forcing ability uses F only. + if (ViewPawn.IsA('TgPawn_Huntress') // Cassie dodge roll + || ViewPawn.IsA('TgPawn_Cassie') // (old alias) + || ViewPawn.IsA('TgPawn_Princess') // Lian + || ViewPawn.IsA('TgPawn_Flak') // Ash + || ViewPawn.IsA('TgPawn_Engineer') // Barik + || ViewPawn.IsA('TgPawn_Barik') // (old alias) + || ViewPawn.IsA('TgPawn_Makoa') // Makoa + || ViewPawn.IsA('TgPawn_Shaman') // Grohk + || ViewPawn.IsA('TgPawn_Grohk') // (old alias) + || ViewPawn.IsA('TgPawn_Mage') // Evie + || ViewPawn.IsA('TgPawn_Evie') // (old alias) + || ViewPawn.IsA('TgPawn_Fairy')) // Willo + return (1 << 4); + + // Tyra, Viktor, Sha Lin, Bomb King, Drogoz, Kinessa, Inara, Ruckus, + // Torvald, Buck, Lex, Maeve, Skye, Grover, Seris, Ying, Strix: none. + return 0; +} +simulated function SetForced3PPose(bool bForced3P) +{ + local TmCameraModule_SpectatorFirstPerson CamMod; + + if (PlayerCamera == none) + return; + CamMod = TmCameraModule_SpectatorFirstPerson(TgPlayerCamera(PlayerCamera).CurrentCameraMod); + if (CamMod != none) + CamMod.SetForced3P(bForced3P); +} + +// Hiding must be absolute and survive ATgPawn::TickSpecial, which re-asserts +// mesh visibility every tick via RecursiveSetVisibility (derived from +// bOwnerNoSee-style flags that cannot work here: Owner is not replicated to +// spectators, so the engine never considers the pawn's meshes owned by us). +// Setting the actor-level bHidden wins that race: rendering honors the owning +// actor's hidden flag regardless of per-component flips. The mount mesh is +// deliberately skipped: PlayMountingEffects/StopMountingEffects own its +// lifecycle, and blanket-unhiding would resurrect a stale horse during later +// third person ticks. +simulated function SetRealModelHidden(TgPawn ViewPawn, bool bHidden) +{ + local PrimitiveComponent Prim; + + ViewPawn.SetHidden(bHidden); + foreach ViewPawn.ComponentList(class'PrimitiveComponent', Prim) + { + if (!bHidden && Prim == ViewPawn.m_MountMesh) + continue; + Prim.SetHidden(bHidden); + } +} + +// Park or restore the first person viewmodel for third person ticks. The +// native gate stays open there, so its per-tick toggles (OwnerNoSee based) +// keep flipping state; absolute SetHidden sticks instead. Overlay duplicates +// and particles attached to the rig are handled too, otherwise weapon glow +// effects keep rendering over a hidden mesh. +simulated function SetRigVisible(TgPawn ViewPawn, bool bVisible) +{ + local TgWeaponMeshActor WMA; + local PrimitiveComponent Prim; + local int i; + + WMA = ViewPawn.m_WeaponMesh; + if (WMA == none) + return; + + if (bVisible) + { + // Restore the first person pieces plus everything owned by the rig + // (weapon glow particles live here); the 3P weapon is left to the + // native owner-based visibility, which now applies to us. + foreach WMA.AllOwnedComponents(class'PrimitiveComponent', Prim) + Prim.SetHidden(false); + if (WMA.m_WeaponMesh1P != none) + WMA.m_WeaponMesh1P.FxActivateGroup('AlwaysOn', 0); + if (WMA.m_HandsMesh != none) + WMA.m_HandsMesh.FxActivateGroup('AlwaysOn', 0); + if (WMA.m_HeadMesh1P != none) + WMA.m_HeadMesh1P.FxActivateGroup('AlwaysOn', 0); + SetRigOverlaysVisible(ViewPawn, true); + return; + } + + // Park everything weapon related for third person ticks. + WMA.Set1PAttachState(2); + if (WMA.m_WeaponMesh1P != none) + { + WMA.m_WeaponMesh1P.SetHidden(true); + WMA.m_WeaponMesh1P.FxDeactivateGroup('AlwaysOn', 0); + } + if (WMA.m_HandsMesh != none) + { + WMA.m_HandsMesh.SetHidden(true); + WMA.m_HandsMesh.FxDeactivateGroup('AlwaysOn', 0); + } + if (WMA.m_HeadMesh1P != none) + { + WMA.m_HeadMesh1P.SetHidden(true); + WMA.m_HeadMesh1P.FxDeactivateGroup('AlwaysOn', 0); + } + if (WMA.m_WeaponMesh3P != none) + WMA.m_WeaponMesh3P.SetHidden(true); + + // Particles parented to the rig render independently of their parent's + // hidden flag, and they can live anywhere in the ownership chain (the + // weapon glows do), so sweep everything the rig owns. + foreach WMA.AllOwnedComponents(class'PrimitiveComponent', Prim) + Prim.SetHidden(true); + + SetRigOverlaysVisible(ViewPawn, false); +} + +// Hides/shows the overlay duplicate meshes parented to the first person +// pieces (skin/shield overlays render independently of their parent's flag). +simulated function SetRigOverlaysVisible(TgPawn ViewPawn, bool bVisible) +{ + local int i; + local TgWeaponMeshActor WMA; + + WMA = ViewPawn.m_WeaponMesh; + if (WMA == none) + return; + + for (i = 0; i < ViewPawn.m_OverlayInfosBody.Length; i++) + { + if (ViewPawn.m_OverlayInfosBody[i].ParentMesh == WMA.m_HandsMesh + || ViewPawn.m_OverlayInfosBody[i].ParentMesh == WMA.m_HeadMesh1P) + ViewPawn.m_OverlayInfosBody[i].OverlayMesh.SetHidden(!bVisible); + } + for (i = 0; i < ViewPawn.m_OverlayInfosWeapon.Length; i++) + { + if (ViewPawn.m_OverlayInfosWeapon[i].ParentMesh == WMA.m_WeaponMesh1P) + ViewPawn.m_OverlayInfosWeapon[i].OverlayMesh.SetHidden(!bVisible); + } +} + +// Server-side actor relevancy (projectiles, deployables, sounds...) is computed +// from each controller's GetPlayerViewPoint. Flag-gated override that tracks +// the view target; kept off the hot path when disabled. +simulated event GetPlayerViewPoint(out Vector out_Location, out Rotator out_Rotation) +{ + local Actor VT; + + if (c_bTrackViewTargetForRelevancy && Role == ROLE_Authority && WorldInfo.NetMode != NM_Client) + { + VT = GetViewTarget(); + if (VT != none) + { + out_Location = VT.Location; + out_Rotation = VT.Rotation; + return; + } + } + super.GetPlayerViewPoint(out_Location, out_Rotation); +} + function SpectatorSetViewTarget(Actor VT, optional ViewTargetTransitionParams TransitionParams) { + ClearFirstPersonNudge(); super.SpectatorSetViewTarget(VT, TransitionParams); } +simulated function ClearFirstPersonNudge() +{ + local TgPawn Nudged; + + if (m_NudgedFpPawn == none) + return; + + Nudged = m_NudgedFpPawn; + m_NudgedFpPawn = none; + m_bWasMounted = false; + + SetRealModelHidden(Nudged, false); + SetRigVisible(Nudged, true); + SetForced3PPose(false); + if (Nudged.Controller == self) + Nudged.Controller = none; + m_bBehindView = true; +} + simulated function TickBurnsHud() { local UIHudCards CardsHUD; @@ -180,3 +551,8 @@ simulated function TickSpectatorTeamHUD() HealthTip.SetAlpha(100.0); } } + +defaultproperties +{ + c_bTrackViewTargetForRelevancy=true +} \ No newline at end of file