diff options
author | Respawn <respawn@northstar.tf> | 2024-11-21 23:37:35 +0100 |
---|---|---|
committer | Respawn <respawn@northstar.tf> | 2024-11-21 23:37:35 +0100 |
commit | c1649d1e1d719b2859041afa1cbff32ee090a885 (patch) | |
tree | 5c37c4555b079e94837e9a96e83fdc7c3663284a /Northstar.Client/mod/scripts/vscripts | |
parent | d941fc3f0b062b3485ad2f94c4d6edbdc7f45aab (diff) | |
download | NorthstarMods-c1649d1e1d719b2859041afa1cbff32ee090a885.tar.gz NorthstarMods-c1649d1e1d719b2859041afa1cbff32ee090a885.zip |
Add cl_damage_indicator.gnut from englishclient_mp_common
Diffstat (limited to 'Northstar.Client/mod/scripts/vscripts')
-rw-r--r-- | Northstar.Client/mod/scripts/vscripts/client/cl_damage_indicator.gnut | 951 |
1 files changed, 951 insertions, 0 deletions
diff --git a/Northstar.Client/mod/scripts/vscripts/client/cl_damage_indicator.gnut b/Northstar.Client/mod/scripts/vscripts/client/cl_damage_indicator.gnut new file mode 100644 index 00000000..4857c109 --- /dev/null +++ b/Northstar.Client/mod/scripts/vscripts/client/cl_damage_indicator.gnut @@ -0,0 +1,951 @@ +untyped + +global function ClDamageIndicator_Init +global function Create_DamageIndicatorHUD +global function DamageIndicators +global function GrenadeArrowThink +global function RumbleForTitanDamage + +global function ServerCallback_TitanTookDamage +global function ServerCallback_PilotTookDamage +//global function ClientCodeCallback_OnMissileCreation +global function ClientCodeCallback_CreateGrenadeIndicator + +global function DamageIndicatorRui + +global function ShowGrenadeArrow + +global function SCB_AddGrenadeIndicatorForEntity + +const DAMAGEARROW_FADEANIM = "damage_fade" +const DAMAGEARROW_DURATION = 2.5 +const DAMAGEARROW_SMALL = 0 +const DAMAGEARROW_MEDIUM = 1 +const DAMAGEARROW_LARGE = 2 + +const float DAMAGEHUD_GRENADE_DEBOUNCE_TIME = 0.4 +const float DAMAGEHUD_GRENADE_DEBOUNCE_TIME_LOWSPEED = 0.15 +const float DAMAGEHUD_GRENADE_DEBOUNCE_TIME_LOWSPEED_VELOCITYCUTOFF = 500.0 + + +struct { + array<table> damageArrows + int currentDamageArrow = 0 + int numDamageArrows = 16 + float damageArrowFadeDuration = 1.0 + float damageArrowTime = 0.0 + vector damageArrowAngles = < 0.0, 0.0, 0.0 > + vector damageArrowPointCenter = < 0.0, 0.0, 0.0 > + + table whizByFX = { + small = null, + large = null, + titan = null, + } + + array<table> arrowIncomingAnims = [ + { anim = "damage_incoming_small", duration = 1.5 }, + { anim = "damage_incoming", duration = 1.75 }, + { anim = "damage_incoming_large", duration = 2.00 }, + ] + + int damageIndicatorCount = 0 +} file + +function ClDamageIndicator_Init() +{ + RegisterSignal( "CriticalHitReceived" ) + + AddCreateCallback( "titan_cockpit", DamageArrow_CockpitInit ) + + PrecacheParticleSystem( $"P_wpn_grenade_frag_icon" ) + PrecacheParticleSystem( $"P_wpn_grenade_frag_blue_icon" ) + PrecacheParticleSystem( $"P_wpn_grenade_smoke_icon" ) + + if ( !IsLobby() ) + AddCallback_EntitiesDidLoad( InitDamageArrows ) +} + +function ServerCallback_TitanTookDamage( damage, x, y, z, damageType, damageSourceId, attackerEHandle, eModId, doomedNow, doomedDamage ) +{ + expect float( damage ) + expect int( damageType ) + expect int( damageSourceId ) + expect bool( doomedNow ) + expect int( doomedDamage ) + + if ( IsWatchingThirdPersonKillReplay() ) + return + + if ( DebugVictimClientDamageFeedbackIsEnabled() && (damage > 0.0) ) + { + entity attacker = attackerEHandle ? GetHeavyWeightEntityFromEncodedEHandle( attackerEHandle ) : null + entity localViewPlayer = GetLocalViewPlayer() + bool isHeadShot = (damageType & DF_HEADSHOT) ? true : false + bool isKillShot = (damageType & DF_KILLSHOT) ? true : false + bool isCritical = (damageType & DF_CRITICAL) ? true : false + bool isDoomProtected = (damageType & DF_DOOM_PROTECTED) ? true : false + bool isDoomFatality = (damageType & DF_DOOM_FATALITY) ? true : false + + local weaponMods = [] + if ( eModId != null && eModId in modNameStrings ) + weaponMods.append( modNameStrings[eModId] ) + + string modDesc = ((eModId != null && eModId in modNameStrings) ? (expect string( modNameStrings[eModId] )) : "") + DebugTookDamagePrint( localViewPlayer, attacker, damage, damageSourceId, modDesc, isHeadShot, isKillShot, isCritical, doomedNow, doomedDamage, isDoomProtected, isDoomFatality ) + } + + // It appears to be faster here to create a new thread so other functions called can wait until the frame ends before running. + thread TitanTookDamageThread( damage, x, y, z, damageType, damageSourceId, attackerEHandle, eModId, doomedNow, doomedDamage ) + + vector damageOrigin = < x, y, z > + entity attacker = attackerEHandle ? GetHeavyWeightEntityFromEncodedEHandle( attackerEHandle ) : null + + if ( damageSourceId in clGlobal.onLocalPlayerTookDamageCallback ) + { + foreach ( callback in clGlobal.onLocalPlayerTookDamageCallback[ damageSourceId ] ) + callback( damage, damageOrigin, damageType, damageSourceId, attacker ) + } +} + +function TitanTookDamageThread( float damage, x, y, z, int damageType, int damageSourceId, attackerEHandle, eModId, bool doomedNow, int doomedDamage ) +{ + WaitEndFrame() + + entity attacker = attackerEHandle ? GetHeavyWeightEntityFromEncodedEHandle( attackerEHandle ) : null + entity localViewPlayer = GetLocalViewPlayer() + entity cockpit = localViewPlayer.GetCockpit() + + if ( cockpit && IsTitanCockpitModelName( cockpit.GetModelName() ) ) + TitanCockpit_DamageFeedback( localViewPlayer, cockpit, damage, damageType, < x, y, z >, damageSourceId, doomedNow, doomedDamage ) + + if ( damage >= DAMAGE_BREAK_MELEE_ASSIST ) + localViewPlayer.Lunge_ClearTarget() + + if ( damageSourceId != eDamageSourceId.bubble_shield ) //Don't play Betty OS dialogue if we took damage by bubble shield. We don't have appropriate dialogue for it. + Tracker_PlayerAttackedByTarget( localViewPlayer, attacker ) + + array<string> weaponMods + if ( eModId != null && eModId in modNameStrings ) + weaponMods.append( expect string( modNameStrings[ eModId ] ) ) + + if ( (damage > 0.0) || doomedDamage ) + { + vector damageOrigin = < x, y, z > + DamageHistoryStruct damageHistory = StoreDamageHistoryAndUpdate( localViewPlayer, MAX_DAMAGE_HISTORY_TIME, damage, damageOrigin, damageType, damageSourceId, attacker, weaponMods ) + DamageIndicators( damageHistory, true ) + } + + entity soul = localViewPlayer.GetTitanSoul() + if ( PlayerHasPassive( localViewPlayer, ePassives.PAS_AUTO_EJECT ) ) //TODO: Handle nuclear eject if we ever allow nuclear + auto eject combo again + { + if ( ShouldPlayAutoEjectAnim( localViewPlayer, soul, doomedNow ) ) + thread PlayerEjects( localViewPlayer, cockpit ) + + } + + if ( damageType & DF_CRITICAL ) + { + localViewPlayer.Signal( "CriticalHitReceived" ) + EmitSoundOnEntity( localViewPlayer, "titan_damage_crit_3p_vs_1p" ) + } +} + +bool function ShouldPlayAutoEjectAnim( entity player, entity titanSoul, bool doomedNow ) +{ + if ( !titanSoul.IsDoomed() ) + return false + + if ( player.ContextAction_IsActive() && !player.ContextAction_IsBusy() ) //Some other context action, e.g. melee instead of eject. Then again + return false + + return true +} + +string function DevBuildAttackerDesc( entity localViewPlayer, entity ent ) +{ + if ( ent == null ) + return "<null>" + + if ( localViewPlayer == ent ) + return ("<self>") + + if ( ent.IsPlayer() ) + return ("'" + ent.GetPlayerName() + "' " + ent.GetPlayerSettings()) + + entity bossPlayer = ent.GetBossPlayer() + string ownerString = ((bossPlayer != null) ? (bossPlayer.GetPlayerName() + "'s ") : "") + + var sigName = ent.GetSignifierName() + string debugName = (sigName != null) ? expect string( sigName ) : ent.GetClassName() + return (ownerString + debugName) +} + +void function DebugTookDamagePrint( entity ornull localViewPlayer, entity attacker, float damage, int damageSourceId, string modDesc, bool isHeadShot, bool isKillShot, bool isCritical, bool isDoomShot, int doomShotDamage, bool isDoomProtected, bool isDoomFatality ) +{ + Assert( localViewPlayer ) + string attackerDesc = DevBuildAttackerDesc( expect entity( localViewPlayer ), attacker ) + string timePrint = format( "%d:%.2f", FrameCount(), PlatformTime() ) + printt( + "{"+timePrint+"} TOOK DAMAGE: " + damage + + (isHeadShot ? " (headshot)" : "") + + (isCritical ? " (critical)" : "") + + (isKillShot ? " (KILLED)" : "") + + (isDoomShot ? " (DOOMED dmg:" + doomShotDamage + ")" : "") + + (isDoomProtected ? " (DOOM PROTECTION)" : "") + + (isDoomFatality ? " (DOOM FATALITY)" : "") + + " " + attackerDesc + + " w/ " + GetObitFromDamageSourceID( damageSourceId ) + modDesc + ) +} + +void function PlayVictimHeadshotConfirmation( bool isKillShot ) +{ + entity localViewPlayer = GetLocalViewPlayer() + if ( localViewPlayer == null ) + return + + if ( isKillShot ) + EmitSoundOnEntity( localViewPlayer, "Player.Hitbeep_headshot.Kill.Human_3P_vs_1P" ) + else + EmitSoundOnEntity( localViewPlayer, "Player.Hitbeep_headshot.Human_3P_vs_1P" ) +} + +void function RumbleForPilotDamage( float damageAmount ) +{ + Rumble_Play( "rumble_pilot_hurt", {} ) +} + +void function RumbleForTitanDamage( float damageAmount ) +{ + string rumbleName; + if ( damageAmount < 500 ) + rumbleName = "titan_damaged_small" + else if ( damageAmount < 1000 ) + rumbleName = "titan_damaged_med" + else + rumbleName = "titan_damaged_big" + + Rumble_Play( rumbleName, {} ) +} + +function ServerCallback_PilotTookDamage( damage, x, y, z, damageType, damageSourceId, attackerEHandle, eModId ) +{ + expect float( damage ) + expect int( damageType ) + expect int( damageSourceId ) + + if ( IsWatchingThirdPersonKillReplay() ) + return + + entity attacker = attackerEHandle ? GetHeavyWeightEntityFromEncodedEHandle( attackerEHandle ) : null + entity localViewPlayer = GetLocalViewPlayer() + vector damageOrigin = < x, y, z > + + bool isHeadShot = (damageType & DF_HEADSHOT) ? true : false + bool isKillShot = (damageType & DF_KILLSHOT) ? true : false + + if ( isHeadShot ) + PlayVictimHeadshotConfirmation( isKillShot ); + + //Jolt view if player is getting meleed + if ( damageSourceId == eDamageSourceId.human_melee ) + { + vector joltDir = Normalize( localViewPlayer.CameraPosition() - damageOrigin ) + //clear melee assist when you get meleed + localViewPlayer.Lunge_ClearTarget() + } + + array<string> weaponMods + if ( eModId != null && eModId in modNameStrings ) + weaponMods.append( expect string( modNameStrings[ eModId ] ) ) + + if ( DebugVictimClientDamageFeedbackIsEnabled() && !IsWatchingReplay() ) + { + string modDesc = (weaponMods.len() > 0 ? (" +" + weaponMods[0]) : "") + bool isCritical = (damageType & DF_CRITICAL) ? true : false + + DebugTookDamagePrint( localViewPlayer, attacker, damage, damageSourceId, modDesc, isHeadShot, isKillShot, isCritical, false, 0, false, false ) + } + + RumbleForPilotDamage( damage ) + + DamageHistoryStruct damageTable = StoreDamageHistoryAndUpdate( localViewPlayer, MAX_DAMAGE_HISTORY_TIME, damage, damageOrigin, damageType, damageSourceId, attacker, weaponMods ) + + DamageIndicators( damageTable, false ) + + if ( damageSourceId in clGlobal.onLocalPlayerTookDamageCallback ) + { + foreach ( callback in clGlobal.onLocalPlayerTookDamageCallback[ damageSourceId ] ) + callback( damage, damageOrigin, damageType, damageSourceId, attacker ) + } +} + +/* +void function ClientCodeCallback_OnMissileCreation( entity missileEnt, string weaponName, bool firstTime ) +{ + +} +*/ + +void function ClientCodeCallback_CreateGrenadeIndicator( entity missileEnt, string weaponName ) +{ + if ( !IsValid( missileEnt ) ) + return + + //Called for all projectiles, not just missiles. + TryAddGrenadeIndicator( missileEnt, weaponName ) +} + + +void function DamageIndicators( DamageHistoryStruct damageHistory, bool playerIsTitan ) +{ + if ( damageHistory.damageType & DF_NO_INDICATOR ) + return + if ( !level.clientScriptInitialized ) + return + if ( IsWatchingThirdPersonKillReplay() ) + return + + entity localViewPlayer = GetLocalViewPlayer() + + int arrowType = DAMAGEARROW_MEDIUM + + if ( IsValid( damageHistory.attacker ) ) + { + if ( damageHistory.attacker == localViewPlayer ) + return + + if ( damageHistory.attacker.IsTitan() ) + arrowType = DAMAGEARROW_MEDIUM + else if ( damageHistory.attacker.IsPlayer() ) + arrowType = DAMAGEARROW_SMALL + else + arrowType = DAMAGEARROW_SMALL + + //if ( damageHistory.attacker.IsTitan() ) + // arrowType = DAMAGEARROW_LARGE + //else if ( damageHistory.attacker.IsPlayer() ) + // arrowType = DAMAGEARROW_MEDIUM + //else + // arrowType = DAMAGEARROW_SMALL + } + + if ( playerIsTitan ) + { + entity cockpit = localViewPlayer.GetCockpit() + + if ( !cockpit ) + return + + vector dirToDamage = damageHistory.origin - localViewPlayer.GetOrigin() + dirToDamage.z = 0 + dirToDamage = Normalize( dirToDamage ) + + vector playerViewForward = localViewPlayer.GetViewVector() + playerViewForward.z = 0.0 + playerViewForward = Normalize( playerViewForward ) + + float damageFrontDot = DotProduct( dirToDamage, playerViewForward ) + + if ( damageFrontDot >= 0.707107 ) + cockpit.AddToTitanHudDamageHistory( COCKPIT_PANEL_TOP, damageHistory.damage ) + else if ( damageFrontDot <= -0.707107 ) + cockpit.AddToTitanHudDamageHistory( COCKPIT_PANEL_BOTTOM, damageHistory.damage ) + else + { + vector playerViewRight = localViewPlayer.GetViewRight() + playerViewRight.z = 0.0 + playerViewRight = Normalize( playerViewRight ) + + float damageRightDot = DotProduct( dirToDamage, playerViewRight ) + + if ( damageRightDot >= 0.707107 ) + cockpit.AddToTitanHudDamageHistory( COCKPIT_PANEL_RIGHT, damageHistory.damage ) + else + cockpit.AddToTitanHudDamageHistory( COCKPIT_PANEL_LEFT, damageHistory.damage ) + } + + if ( damageHistory.attacker && damageHistory.attacker.GetParent() == localViewPlayer ) + { + damageHistory.rodeoDamage = true + return + } + } + + #if SP + if ( IsValid( damageHistory.attacker ) && damageHistory.attacker.IsTitan() ) + arrowType = DAMAGEARROW_LARGE + else if ( playerIsTitan && damageHistory.damage < 50 ) + return + else if ( !playerIsTitan && damageHistory.damage < 15 ) + arrowType = DAMAGEARROW_SMALL + else + arrowType = DAMAGEARROW_MEDIUM + + thread DamageIndicatorRui( damageHistory.origin, arrowType, playerIsTitan ) + #else + bool show2DIndicator = true + bool show3DIndicator = false + + const int DAMAGE_INDICATOR_STYLE_2D_ONLY = 0 + const int DAMAGE_INDICATOR_STYLE_BOTH = 1 + const int DAMAGE_INDICATOR_STYLE_3D_ONLY = 2 + + if ( playerIsTitan ) + { + show2DIndicator = GetConVarInt( "damage_indicator_style_titan" ) != DAMAGE_INDICATOR_STYLE_3D_ONLY + show3DIndicator = GetConVarInt( "damage_indicator_style_titan" ) != DAMAGE_INDICATOR_STYLE_2D_ONLY + } + else + { + show2DIndicator = GetConVarInt( "damage_indicator_style_pilot" ) != DAMAGE_INDICATOR_STYLE_3D_ONLY + show3DIndicator = GetConVarInt( "damage_indicator_style_pilot" ) != DAMAGE_INDICATOR_STYLE_2D_ONLY + } + + if ( show2DIndicator ) + thread DamageIndicatorRui( damageHistory.origin, arrowType, playerIsTitan ) + + if ( show3DIndicator ) + ShowDamageArrow( localViewPlayer, damageHistory.origin, arrowType, playerIsTitan, damageHistory.attacker ) + #endif +} + +const float DAMAGE_INDICATOR_DURATION = 4.0 + +void function DamageIndicatorRui( vector damageOrigin, int arrowType, bool playerIsTitan ) +{ + clGlobal.levelEnt.EndSignal( "KillReplayStarted" ) + clGlobal.levelEnt.EndSignal( "KillReplayEnded" ) + + // slop + float distance = Length( damageOrigin - GetLocalViewPlayer().CameraPosition() ) + float randomRange = GraphCapped( distance, 0.0, 2048, 0.0, 256.0 ) + damageOrigin = <damageOrigin.x + RandomFloatRange( randomRange, -randomRange ), damageOrigin.y + RandomFloatRange( randomRange, -randomRange ), damageOrigin.z> + + float startTime = Time() + + var rui = RuiCreate( $"ui/damage_indicator.rpak", clGlobal.topoFullScreen, RUI_DRAW_HUD, 0 ) + RuiSetResolutionToScreenSize( rui ) + RuiSetGameTime( rui, "startTime", startTime ) + RuiSetFloat( rui, "duration", DAMAGE_INDICATOR_DURATION ) + RuiSetInt( rui, "attackerType", arrowType ) + + file.damageIndicatorCount++ + int damageIndicatorThreshold = file.damageIndicatorCount + 8 + + OnThreadEnd( + function() : ( rui ) + { + RuiDestroy( rui ) + } + ) + + while ( Time() - startTime < DAMAGE_INDICATOR_DURATION && file.damageIndicatorCount < damageIndicatorThreshold ) + { + vector vecToDamage = damageOrigin - GetLocalViewPlayer().CameraPosition() + vecToDamage.z = 0 + vecToDamage = Normalize( vecToDamage ) + RuiSetFloat3( rui, "vecToDamage2D", vecToDamage ) + RuiSetFloat3( rui, "camVec2D", Normalize( AnglesToForward( < 0, GetLocalViewPlayer().CameraAngles().y, 0 > ) ) ) + RuiSetFloat( rui, "sideDot", vecToDamage.Dot( CrossProduct( <0, 0, 1>, Normalize( AnglesToForward( < 0, GetLocalViewPlayer().CameraAngles().y, 0 > ) ) ) ) ) + WaitFrame() + } +} + +void function ShowGrenadeArrow( entity player, entity grenade, float damageRadius, float startDelay, bool requireLos = true ) +{ + thread GrenadeArrowThink( player, grenade, damageRadius, startDelay, requireLos ) +} + +vector function GetRandomOriginWithinBounds( entity ent ) +{ + vector boundingMins = ent.GetBoundingMins() + vector boundingMaxs = ent.GetBoundingMaxs() + + vector randomOffset = < RandomFloatRange( boundingMins.x, boundingMaxs.x ), RandomFloatRange( boundingMins.y, boundingMaxs.y ), RandomFloatRange( boundingMins.z, boundingMaxs.z ) > + + return ent.GetOrigin() + randomOffset +} + +void function GrenadeArrowThink( entity player, entity grenade, float damageRadius, float startDelay, bool requireLos, string requiredPlayerState = "any" ) +{ + EndSignal( grenade, "OnDeath" ) //On death added primarily for frag_drones + EndSignal( grenade, "OnDestroy" ) + EndSignal( player, "OnDeath" ) + + wait startDelay + + asset grenadeModel = GRENADE_INDICATOR_FRAG_MODEL + vector grenadeOffset = < -5, 0, 0 > + if ( grenade instanceof C_Projectile ) + { + if ( grenade.ProjectileGetWeaponClassName() == "mp_weapon_grenade_sonar" ) + { + grenadeModel = GRENADE_INDICATOR_SONAR_MODEL + grenadeOffset = < -5, 0, 0 > + requireLos = false + } + } + else if ( grenade.IsNPC() ) + { + switch ( grenade.GetSignifierName() ) + { + #if MP + case "npc_stalker": + grenadeModel = GRENADE_INDICATOR_STALKER_MODEL + break + #endif + + case "npc_frag_drone": + grenadeModel = GRENADE_INDICATOR_TICK_MODEL + break + } + } + + entity arrow = CreateClientSidePropDynamic( < 0, 0, 0 >, < 0, 0, 0 >, GRENADE_INDICATOR_ARROW_MODEL ) + entity mdl = CreateClientSidePropDynamic( < 0, 0, 0 >, < 0, 0, 0 >, grenadeModel ) + + OnThreadEnd( + function() : ( arrow, mdl ) + { + if ( IsValid( arrow ) ) + arrow.Destroy() + if ( IsValid( mdl ) ) + mdl.Destroy() + } + ) + + entity cockpit = player.GetCockpit() + if ( !cockpit ) + return + + EndSignal( cockpit, "OnDestroy" ) + + arrow.SetParent( cockpit, "CAMERA_BASE" ) + arrow.SetAttachOffsetOrigin( < 25.0, 0.0, -4.0 > ) + + mdl.SetParent( arrow, "BACK" ) + mdl.SetAttachOffsetOrigin( grenadeOffset ) + + float lastVisibleTime = 0 + bool shouldBeVisible = true + + while ( true ) + { + cockpit = player.GetCockpit() + + switch ( requiredPlayerState ) + { + case "any": + shouldBeVisible = true + break + case "pilot": + shouldBeVisible = !player.IsTitan() + break + case "titan": + shouldBeVisible = player.IsTitan() + break + default: + Assert( false, "Invalid player state! Allower states: 'any' 'pilot' 'titan'" ) + + } + + if ( shouldBeVisible ) + { + if ( Distance( player.GetOrigin(), grenade.GetOrigin() ) > damageRadius || !cockpit ) + { + shouldBeVisible = false + } + else + { + bool tracePassed = false + + if ( requireLos ) + { + TraceResults result = TraceLine( grenade.GetOrigin(), GetRandomOriginWithinBounds( player ), grenade, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE ) + + if ( result.fraction == 1.0 ) + tracePassed = true + } + + if ( requireLos && !tracePassed ) + { + shouldBeVisible = false + } + else + { + shouldBeVisible = true + lastVisibleTime = Time() + } + } + } + + if ( shouldBeVisible || Time() - lastVisibleTime < 0.25 ) + { + arrow.EnableDraw() + mdl.EnableDraw() + + arrow.DisableRenderWithViewModelsNoZoom() + arrow.EnableRenderWithCockpit() + arrow.EnableRenderWithHud() + mdl.DisableRenderWithViewModelsNoZoom() + mdl.EnableRenderWithCockpit() + mdl.EnableRenderWithHud() + + vector damageArrowAngles = AnglesInverse( player.EyeAngles() ) + vector vecToDamage = grenade.GetOrigin() - (player.EyePosition() + (player.GetViewVector() * 20.0)) + + // reparent for embark/disembark + if ( arrow.GetParent() == null ) + arrow.SetParent( cockpit, "CAMERA_BASE", true ) + + arrow.SetAttachOffsetAngles( damageArrowAngles.AnglesCompose( vecToDamage.VectorToAngles() ) ) + } + else + { + mdl.DisableDraw() + arrow.DisableDraw() + } + + WaitFrame() + } + +} + + +function Create_DamageIndicatorHUD() +{ +} + + +void function SCB_AddGrenadeIndicatorForEntity( int team, int ownerHandle, int eHandle, float damageRadius ) +{ + if ( !level.grenadeIndicatorEnabled ) + return + + #if DEV + if ( !level.clientScriptInitialized ) + return + #endif + + entity player = GetLocalViewPlayer() + entity owner = GetEntityFromEncodedEHandle( ownerHandle ) + + entity ent = GetEntityFromEncodedEHandle( eHandle ) + if ( !IsValid( ent ) ) + return + + if ( team == player.GetTeam() && owner != player ) + return + + //TryAddGrenadeIndicator( ent, "" ) // TODO: make function handle non-grenade ents +} + + +function TryAddGrenadeIndicator( grenade, weaponName ) +{ + #if DEV + if ( !level.clientScriptInitialized ) + return + #endif + + if ( !level.grenadeIndicatorEnabled ) + return + + expect entity( grenade ) + entity player = GetLocalViewPlayer() + + // view player may be null when dead + if ( !IsValid( player ) ) + return + + var className = grenade.GetClassName() + float damageRadius = 0.0 + + if ( className == "grenade" ) + { + damageRadius = grenade.GetDamageRadius() + } + else if ( grenade.ProjectileGetWeaponClassName() == "mp_titanweapon_arc_ball" ) + { + // arc ball doesn't arc to pilots so no need to show the warning + if ( !player.IsTitan() ) + return + + damageRadius = BALL_LIGHTNING_ZAP_RADIUS + } + else + { + return + } + + float radius = grenade.GetExplosionRadius() + + if ( player.IsPhaseShifted() ) + return + + + float startDelay = 0.0 + if ( grenade.GetOwner() == player ) + { + if ( !grenade.GetProjectileWeaponSettingBool( eWeaponVar.projectile_damages_owner ) && !grenade.GetProjectileWeaponSettingBool( eWeaponVar.explosion_damages_owner ) ) + return + + float relVelocity = Length( grenade.GetVelocity() - player.GetVelocity() ) + if ( relVelocity < DAMAGEHUD_GRENADE_DEBOUNCE_TIME_LOWSPEED_VELOCITYCUTOFF ) + startDelay = DAMAGEHUD_GRENADE_DEBOUNCE_TIME_LOWSPEED + else + startDelay = DAMAGEHUD_GRENADE_DEBOUNCE_TIME + } + else if ( grenade.GetTeam() == player.GetTeam() ) + { + return + } + + float padding = player.IsTitan() ? 204.0 : 65.0 + + //AddGrenadeIndicator( grenade, radius + padding, startDelay, false ) + ShowGrenadeArrow( player, grenade, radius + padding, startDelay ) + + //thread ShowRuiGrenadeThreatIndicator( grenade, float( radius ) + padding ) +} + +void function ShowRuiGrenadeThreatIndicator( entity grenade, float radius ) +{ + var rui = RuiCreate( $"ui/grenade_threat_indicator.rpak", clGlobal.topoCockpitHudPermanent, RUI_DRAW_COCKPIT, 0 ) + //var rui = CreateCockpitRui( $"ui/grenade_threat_indicator.rpak", 0 ) + RuiSetGameTime( rui, "startTime", Time() ) + RuiSetFloat( rui, "damageRadius", radius ) + //RuiTrackFloat3( rui, "pos", grenade, RUI_TRACK_ABSORIGIN_FOLLOW )` + RuiTrackFloat3( rui, "pos", grenade, RUI_TRACK_POINT_FOLLOW, grenade.LookupAttachment( "BACK" ) ) + + OnThreadEnd( + function() : ( rui ) + { + RuiDestroy( rui ) + } + ) + + grenade.WaitSignal( "OnDestroy" ) +} + + + +void function InitDamageArrows() +{ + for ( int i = 0; i < file.numDamageArrows; i++ ) + { + table arrowData = { + grenade = null + grenadeRadius = 0.0 + damageOrigin = < 0.0, 0.0, 0.0 >, + damageDirection = < 0.0, 0.0, 0.0 >, + endTime = -99.0 + DAMAGEARROW_DURATION, + startTime = -99.0, + isDying = false, + isVisible = false, + whizby = false, // hack until we get a new model/shader for the whizby indicator - Roger + attacker = null, + randomAngle = 0 // Repeated shots from the same attacker randomize the angle of the arrow. + } + + entity arrow = CreateClientSidePropDynamic( < 0, 0, 0 >, < 0, 0, 0 >, DAMAGEARROW_MODEL ) + arrow.SetCanCloak( false ) + arrow.SetVisibleForLocalPlayer( 0 ) + arrow.DisableDraw() + + arrowData.arrow <- arrow + arrow.s.arrowData <- arrowData + + file.damageArrows.append( arrowData ) + } + + entity arrow = CreateClientSidePropDynamic( < 0, 0, 0 >, < 0, 0, 0 >, DAMAGEARROW_MODEL ) + file.damageArrowFadeDuration = arrow.GetSequenceDuration( DAMAGEARROW_FADEANIM ) // 0.266 + arrow.Destroy() +} + + +void function DamageArrow_CockpitInit( entity cockpit ) +{ + entity localViewPlayer = GetLocalViewPlayer() + thread UpdateDamageArrows( localViewPlayer, cockpit ) +} + +function RefreshExistingDamageArrow( entity player, arrowData, int arrowType, damageOrigin ) +{ + //Hack - 10 tick rate is making damage feedback bunch up. If we improve that then shouldn't be threaded. + player.EndSignal( "OnDestroy" ) + entity cockpit = player.GetCockpit() + if ( IsValid( cockpit ) ) + cockpit.EndSignal( "OnDestroy" ) + + float time = Time() + + if ( arrowData.startTime == time ) + wait 0.05 + + if ( !arrowData.isVisible || arrowData.isDying ) + return + + time = Time() + arrowData.endTime = time + file.arrowIncomingAnims[ arrowType ].duration + arrowData.startTime = time + arrowData.damageOrigin = damageOrigin + arrowData.randomAngle = RandomIntRange( -3, 3 ) + PulseDamageArrow( expect entity( arrowData.arrow ), arrowType ) + UpdateDamageArrowVars( player ) + UpdateDamageArrowAngle( arrowData ) +} + +function ShowDamageArrow( entity player, damageOrigin, int arrowType, playerIsTitan, attacker ) +{ + if ( file.damageArrows.len() == 0 ) // not yet initialized + return + + table arrowData = file.damageArrows[file.currentDamageArrow] + entity arrow = expect entity( arrowData.arrow ) + + file.currentDamageArrow++ + if ( file.currentDamageArrow >= file.numDamageArrows ) + file.currentDamageArrow = 0 + + float time = Time() + + arrow.s.arrowData.damageOrigin = damageOrigin + arrow.s.arrowData.grenade = null + arrow.s.arrowData.grenadeRadius = 0.0 + arrow.s.arrowData.endTime = time + file.arrowIncomingAnims[ arrowType ].duration + arrow.s.arrowData.startTime = time + arrow.s.arrowData.isDying = false + arrow.s.arrowData.whizby = false // hack until we get a new model/shader for the whizby indicator + arrow.s.arrowData.attacker = attacker + + if ( !arrow.s.arrowData.isVisible ) + { + entity cockpit = player.GetCockpit() + + if ( !cockpit ) + return + + arrow.s.arrowData.isVisible = true + arrow.EnableDraw() + + arrow.DisableRenderWithViewModelsNoZoom() + arrow.EnableRenderWithCockpit() + + arrow.EnableRenderWithHud() + + arrow.SetParent( cockpit, "CAMERA_BASE" ) + arrow.SetAttachOffsetOrigin( < 20.0, 0.0, -2.0 > ) + } + + + PulseDamageArrow( arrow, arrowType ) + UpdateDamageArrowVars( player ) + UpdateDamageArrowAngle( arrowData ) +} + + +function PulseDamageArrow( entity arrow, int arrowType ) +{ + arrow.Anim_NonScriptedPlay( file.arrowIncomingAnims[ arrowType ].anim ) +} + +function UpdateDamageArrowVars( entity localViewPlayer ) +{ + file.damageArrowTime = Time() + file.damageArrowAngles = AnglesInverse( localViewPlayer.EyeAngles() ) + file.damageArrowPointCenter = localViewPlayer.EyePosition() + ( localViewPlayer.GetViewVector() * 20.0 ) +} + +function UpdateDamageArrowAngle( arrowData ) +{ + if ( IsValid( arrowData.grenade ) ) + arrowData.damageOrigin = arrowData.grenade.GetOrigin() + + vector vecToDamage = expect vector( arrowData.damageOrigin ) - file.damageArrowPointCenter + vector anglesToDamage = VectorToAngles( vecToDamage ) + vector eyeAngles = GetLocalViewPlayer().EyeAngles() + + float roll = sin( DegToRad( eyeAngles.y - anglesToDamage.y ) ) + + arrowData.arrow.SetAttachOffsetAngles( AnglesCompose( file.damageArrowAngles, anglesToDamage ) + < arrowData.randomAngle, 0, roll * 90.0 > ) + arrowData.damageDirection = Normalize( vecToDamage ) +} + +function UpdateDamageArrows( entity localViewPlayer, entity cockpit ) +{ + localViewPlayer.EndSignal( "OnDestroy" ) + cockpit.EndSignal( "OnDestroy" ) + + OnThreadEnd( + function() : ( localViewPlayer ) + { + foreach ( arrowData in file.damageArrows ) + { + if ( IsValid( arrowData.arrow ) ) + { + arrowData.arrow.DisableDraw() + arrowData.arrow.ClearParent() + arrowData.attacker = null + arrowData.isVisible = false + arrowData.randomAngle = 0 + } + } + } + ) + + bool varsUpdated = false + + while ( true ) + { + WaitEndFrame() + + vector playerOrigin = localViewPlayer.GetOrigin() + + varsUpdated = false + bool inPhaseShift = localViewPlayer.IsPhaseShifted() + + foreach ( arrowData in file.damageArrows ) + { + if ( !arrowData.isVisible ) + { + continue + } + + if ( arrowData.grenade != null ) + { + if ( !IsValid( arrowData.grenade ) ) + arrowData.endTime = 0.0 + } + + if ( (file.damageArrowTime >= arrowData.endTime) || inPhaseShift ) + { + arrowData.arrow.DisableDraw() + arrowData.arrow.ClearParent() + arrowData.attacker = null + arrowData.isVisible = false + arrowData.randomAngle = 0 + continue + } + + if ( !varsUpdated ) // only call UpdateDamageArrowVars if one or more of the file.damageArrows is visible + { + varsUpdated = true + UpdateDamageArrowVars( localViewPlayer ) + } + + UpdateDamageArrowAngle( arrowData ) + + if ( !arrowData.isDying && ( ( arrowData.endTime - file.damageArrowTime ) <= file.damageArrowFadeDuration ) ) + { + arrowData.isDying = true + arrowData.arrow.Anim_NonScriptedPlay( DAMAGEARROW_FADEANIM ) + } + } + + wait( 0.0 ) + } +} |