From 0f71d94b6c968ddb34ec611d898cceea02638f99 Mon Sep 17 00:00:00 2001 From: BobTheBob <32057864+BobTheBob9@users.noreply.github.com> Date: Sun, 17 Oct 2021 22:29:19 +0100 Subject: finish burncard impl, add forced 1p fp sequences and classic rodeo --- .../gamemodes/_gamemode_fastball_intro.gnut | 5 +- .../scripts/vscripts/gamemodes/_gamemode_inf.gnut | 4 + .../vscripts/melee/_melee_synced_human.gnut | 606 +++++ .../vscripts/melee/_melee_synced_titan.gnut | 1610 +++++++++++++ .../mod/scripts/vscripts/rodeo/_rodeo_titan.gnut | 2491 ++++++++++++++++++++ .../scripts/vscripts/rodeo/sh_classic_rodeo.gnut | 61 + .../vscripts/sh_3psequence_to_1p_hacks.gnut | 178 ++ .../vscripts/titan/sh_first_person_embark.gnut | 47 - .../scripts/vscripts/titan/sh_titan_embark.gnut | 25 +- 9 files changed, 4955 insertions(+), 72 deletions(-) create mode 100644 Northstar.Custom/mod/scripts/vscripts/melee/_melee_synced_human.gnut create mode 100644 Northstar.Custom/mod/scripts/vscripts/melee/_melee_synced_titan.gnut create mode 100644 Northstar.Custom/mod/scripts/vscripts/rodeo/_rodeo_titan.gnut create mode 100644 Northstar.Custom/mod/scripts/vscripts/rodeo/sh_classic_rodeo.gnut create mode 100644 Northstar.Custom/mod/scripts/vscripts/sh_3psequence_to_1p_hacks.gnut delete mode 100644 Northstar.Custom/mod/scripts/vscripts/titan/sh_first_person_embark.gnut (limited to 'Northstar.Custom/mod') diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fastball_intro.gnut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fastball_intro.gnut index 75482558..b26016f3 100644 --- a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fastball_intro.gnut +++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fastball_intro.gnut @@ -154,7 +154,8 @@ void function FastballPlayer( entity player ) // respawn the player player.SetOrigin( buddy.GetOrigin() ) player.RespawnPlayer( null ) - player.Hide() + var oldVisibility = player.kv.VisibilityFlags // better than .Hide(), hides weapons and such + player.kv.VisibilityFlags = 0 player.HolsterWeapon() // hide hud, fade screen out from black @@ -184,7 +185,7 @@ void function FastballPlayer( entity player ) // have to correct this manually here since due to no 3p animation our position isn't set right during this sequence player.SetOrigin( buddy.GetAttachmentOrigin( buddy.LookupAttachment( "FASTBALL_R" ) ) ) - player.Show() + player.kv.VisibilityFlags = oldVisibility // restore visibility player.SetVelocity( throwVel ) TryGameModeAnnouncement( player ) diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut index b2e76aaf..cdf03edd 100644 --- a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut +++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut @@ -99,6 +99,10 @@ void function RespawnInfected( entity player ) player.SetSkin( 1 ) player.SetCamo( 110 ) + // if human, remove helmet bodygroup, human models have some weird bloody white thing underneath their helmet that works well for this, imo + if ( !player.IsMechanical() ) + player.SetBodygroup( player.FindBodyGroup( "head" ), 1 ) + // stats for infected StimPlayer( player, 9999.9 ) // can't do endless since we don't get the visual effect in endless player.kv.airAcceleration = 2500 diff --git a/Northstar.Custom/mod/scripts/vscripts/melee/_melee_synced_human.gnut b/Northstar.Custom/mod/scripts/vscripts/melee/_melee_synced_human.gnut new file mode 100644 index 00000000..068ffcfd --- /dev/null +++ b/Northstar.Custom/mod/scripts/vscripts/melee/_melee_synced_human.gnut @@ -0,0 +1,606 @@ +untyped + +global function MeleeThread_PilotVsEnemy +global function MeleeSyncedServer_Init + +void function MeleeSyncedServer_Init() +{ + RegisterSignal( "NpcDealsExecutionDamage" ) +} + +bool function MeleeThread_PilotVsEnemy( SyncedMelee action, entity attacker, entity target ) +{ + // function off for reload scripts + return MeleeThread_PilotVsEnemyInternal( action, attacker, target ) +} + +bool function MeleeThread_PilotVsEnemyInternal( SyncedMelee action, entity attacker, entity target ) +{ + Assert( IsHumanSized( target ), target + " is not human sized melee target" ) + Assert( attacker.IsPlayer() && IsHumanSized( attacker ), attacker + " is not human sized player attacker" ) + Assert( IsAlive( attacker ) ) + Assert( IsAlive( target ) ) + + bool isAttackerRef = IsAttackerRef( action, target ) + + vector attackerOrigin = attacker.GetOrigin() + vector targetOrigin = target.GetOrigin() + + attacker.EndSignal( "OnDestroy" ) + target.EndSignal( "OnDestroy" ) + + if ( IsSingleplayer() ) + { + if ( attacker.IsPlayer() ) + { + if ( IsCloaked( attacker ) ) + { + UnlockAchievement( attacker, achievements.CLOAK_TAKEDOWN ) + } + } + } + + OnThreadEnd( + function() : ( attacker, target, attackerOrigin, targetOrigin, action, isAttackerRef ) + { + if ( IsValid( attacker ) ) + attacker.ClearParent() + + if ( IsValid( target ) ) + target.ClearParent() + + + if ( IsValid( attacker ) ) + { + attacker.PlayerMelee_SetState( PLAYER_MELEE_STATE_NONE ) + } + + // Note that the original attacker/target origins are not guarranteed to be a safe spot now because we have moving geo in the game. + // Whoever is the 'ref' will be in a safe position though, so we can always use the origin of the person who has been designated as the 'ref'. + if ( IsAlive( attacker ) ) + { + if ( !isAttackerRef && IsValid( target ) ) + { + PutEntityInSafeSpot( attacker, target, null, target.GetOrigin(), attacker.GetOrigin() ) + } + else + { + PutEntityInSafeSpot( attacker, target, null, attacker.GetOrigin(), attacker.GetOrigin() ) + } + + } + + if ( IsValid( target ) ) + { + target.ClearParent() + + if ( IsAlive( target ) ) + { + // Note that the original target origin is not guarranteed to be a safe spot now because we have moving geo in the game now. + if ( isAttackerRef && IsValid( attacker ) ) + { + + PutEntityInSafeSpot( target, attacker, null, attacker.GetOrigin(), target.GetOrigin() ) + } + else + { + PutEntityInSafeSpot( target, attacker, null, target.GetOrigin(), target.GetOrigin() ) + } + } + } + } + ) + + thread MeleeThread_PilotVsEnemy_Attacker( action, attacker, target, isAttackerRef ) + // target's sequence is longer + waitthread MeleeThread_PilotVsEnemy_Target( action, attacker, target, isAttackerRef ) + + attacker.Signal( "SyncedMeleeComplete" ) + return true +} + +struct PilotVsEnemyStruct +{ + bool clearInvulnerable = false + bool wasCloaked = false + float cloakEndTime = 0.0 +} + +void function DisableCloakBeforeMelee( entity player, PilotVsEnemyStruct dataStruct ) +{ + if ( IsCloaked( player ) ) + { + dataStruct.wasCloaked = true + dataStruct.cloakEndTime = player.GetCloakEndTime() + DisableCloak( player, 0.0 ) + } +} + +void function RestoreCloakAfterMelee( entity player, PilotVsEnemyStruct dataStruct ) +{ + if ( !IsAlive( player ) ) + return + + if ( !dataStruct.wasCloaked ) + return + + float remainingCloakDuration = max( 0.0, dataStruct.cloakEndTime - Time() ) + if ( remainingCloakDuration > CLOAK_FADE_IN ) //Has to be higher than fade in duration, otherwise will cloak forever + EnableCloak( player, remainingCloakDuration, CLOAK_FADE_IN ) +} + +void function HandleCloakExecutionWithCloakedAttacker( entity player, PilotVsEnemyStruct dataStruct, SyncedMelee action ) +{ + if ( !IsCloaked( player ) ) + return //No need to run DisableCloakBeforeMelee() either + + float attackerSequenceEndTime = Time() + player.GetSequenceDuration( action.attackerAnimation3p ) + float scheduledCloakEndTime = player.GetCloakEndTime() + + //printt( "attackerSequenceEndTime: " + attackerSequenceEndTime + ", scheduledCloakEndTime: " + scheduledCloakEndTime ) + + if ( scheduledCloakEndTime > attackerSequenceEndTime ) + { + //printt( "Cloak ability lasts longer than execution sequence, just doing DisableCloakBeforeMelee" ) + player.SetCloakFlicker( 0.0, 0.0 ) //Turn off flicker; this is normally not a problem for other executions since cloak is turned off for the entirety of those executions + DisableCloakBeforeMelee( player, dataStruct ) + } + else + { + //Cloak would normally run out during the animation of this execution, which is disruptive to the presentation of cloak animation, so just stop cloak now for good and prevent it from coming back. + //printt( "Cloak ability is shorter than execution sequence, DisableCloak now and stop it from coming back" ) + dataStruct.wasCloaked = true //Have to do this to mark player was cloaked during start of execution, so we can track the stat correctly + dataStruct.cloakEndTime = Time() + DisableCloak( player, 0.0 ) + player.Signal( "KillHandleCloakEnd" ) + } +} + + +void function MeleeThread_PilotVsEnemy_Attacker( SyncedMelee action, entity attacker, entity target, bool isAttackerRef ) +{ + attacker.EndSignal( "OnAnimationDone" ) + attacker.EndSignal( "OnAnimationInterrupted" ) + attacker.EndSignal( "OnDeath" ) + attacker.EndSignal( "ScriptAnimStop" ) + + attacker.EndSignal( "OnDestroy" ) + Assert( IsValid( target ) ) + target.EndSignal( "OnDestroy" ) + + + foreach ( AnimEventData animEventData in action.attacker3pAnimEvents ) + { + AddAnimEvent( attacker, animEventData.eventName, animEventData.callback, animEventData.optionalVar ) + } + AddAnimEvent( attacker, "synced_melee_enable_planting", EnablePlantingOnEntity ) + + PilotVsEnemyStruct dataStruct + OnThreadEnd( + function() : ( attacker, target, action, dataStruct ) + { + if ( IsValid( attacker ) ) + { + DeleteAnimEvent( attacker, "synced_melee_enable_planting" ) + + if ( dataStruct.clearInvulnerable ) + { + attacker.ClearInvulnerable() + } + + if ( attacker.IsPlayer() ) + { + attacker.PlayerMelee_ExecutionEndAttacker() + ClearPlayerAnimViewEntity( attacker ) + DeployAndEnableWeapons( attacker ) + + RestoreCloakAfterMelee( attacker, dataStruct ) + #if MP + IncrementStatForPilotExecutionWhileCloaked( attacker, target, dataStruct ) + #endif + } + + foreach ( AnimEventData animEventData in action.attacker3pAnimEvents ) + { + DeleteAnimEvent( attacker, animEventData.eventName ) + } + } + + if ( !IsAlive( attacker ) ) + attacker.Anim_Stop() + } + ) + + FirstPersonSequenceStruct attackerSequence + attackerSequence.blendTime = 0.4 + attackerSequence.attachment = "ref" + attackerSequence.thirdPersonAnim = action.attackerAnimation3p + attackerSequence.firstPersonAnim = action.attackerAnimation1p + attackerSequence.thirdPersonCameraAttachments = [action.thirdPersonCameraAttachment] + attackerSequence.thirdPersonCameraVisibilityChecks = true + + if ( isAttackerRef ) + { + attackerSequence.noParent = true + attackerSequence.playerPushable = true + attackerSequence.enablePlanting = true + } + else + { + attackerSequence.useAnimatedRefAttachment = true + } + + float duration = attacker.GetSequenceDuration( attackerSequence.thirdPersonAnim ) + + if ( attacker.IsPlayer() ) + { + float executionEndTime = Time() + duration + attacker.PlayerMelee_ExecutionStartAttacker( executionEndTime ) + attacker.Lunge_ClearTarget() + HolsterViewModelAndDisableWeapons( attacker ) + + if ( action.ref == "execution_cloak" ) //Special case for cloak execution + { + HandleCloakExecutionWithCloakedAttacker( attacker, dataStruct, action ) + } + else + { + DisableCloakBeforeMelee( attacker, dataStruct ) + } + + if ( IsSingleplayer() ) + { + dataStruct.clearInvulnerable = true + attacker.SetInvulnerable() + thread LowerEnemyAccuracy( attacker, duration ) + } + } + + if ( isAttackerRef ) + { + if ( GetCurrentPlaylistVarInt( "fp_embark_enabled", 0 ) == 1 ) + FirstPersonSequenceForce1P( attackerSequence, attacker ) + thread FirstPersonSequence( attackerSequence, attacker ) + } + else + { + if ( GetCurrentPlaylistVarInt( "fp_embark_enabled", 0 ) == 1 ) + FirstPersonSequenceForce1P( attackerSequence, attacker, target ) + thread FirstPersonSequence( attackerSequence, attacker, target ) + } + + wait duration +} + + +void function MeleeThread_PilotVsEnemy_Target( SyncedMelee action, entity attacker, entity target, bool isAttackerRef ) +{ + attacker.EndSignal( "OnAnimationDone" ) + attacker.EndSignal( "OnAnimationInterrupted" ) + attacker.EndSignal( "OnDeath" ) + attacker.EndSignal( "ScriptAnimStop" ) + + attacker.EndSignal( "OnDestroy" ) + Assert( IsValid( target ) ) + target.EndSignal( "OnDestroy" ) + + foreach ( AnimEventData animEventData in action.target3pAnimEvents ) + { + AddAnimEvent( target, animEventData.eventName, animEventData.callback, animEventData.optionalVar ) + } + AddAnimEvent( target, "synced_melee_enable_planting", EnablePlantingOnEntity ) + + PilotVsEnemyStruct dataStruct + + OnThreadEnd( + function() : ( attacker, target, action, dataStruct ) + { + if ( IsValid( target ) ) + { + if ( target.IsNPC() && IsMultiplayer() ) + { + SetForceDrawWhileParented( target, false ) + } + + TargetClearedExecuted( target ) + DeleteAnimEvent( target, "mark_for_death" ) + DeleteAnimEvent( target, "phase_gib" ) + + foreach ( AnimEventData animEventData in action.target3pAnimEvents ) + { + DeleteAnimEvent( target, animEventData.eventName ) + } + DeleteAnimEvent( target, "synced_melee_enable_planting" ) + + bool isAlive = IsAlive( target ) + + if ( target.IsPlayer() ) + { + EnableOffhandWeapons( target ) + if ( isAlive ) + target.DeployWeapon() + } + + if ( isAlive ) + { + if ( target.e.markedForExecutionDeath ) //Kill off target if he already reached blackout part of melee + { + entity killCreditAttacker = null //If the attacker disconnected, we don't have a player to give credit to, that's fine. Script will not error + if ( IsValid( target.e.syncedMeleeAttacker ) ) + killCreditAttacker = target.e.syncedMeleeAttacker + //printt( "Killing off target " + target + " because he already reached blackout part of execution!" ) + + int damageAmount = target.GetMaxHealth() + 1 + target.TakeDamage( damageAmount, killCreditAttacker, killCreditAttacker, { forceKill = true, damageType = DMG_MELEE_EXECUTION, damageSourceId = eDamageSourceId.human_execution } ) + //markedForExecutionDeath will be cleared in MarkForDeath() which sets it in the first place + } + + if ( target.IsPlayer() ) + RestoreCloakAfterMelee( target, dataStruct ) + } + + if ( IsValid( target.e.syncedMeleeAttacker ) ) + { + if ( IsValid( target.e.lastSyncedMeleeAttacker ) ) + { + target.e.lastSyncedMeleeAttacker = null + } + + target.e.lastSyncedMeleeAttacker = target.e.syncedMeleeAttacker + target.e.syncedMeleeAttacker = null + } + } + } + ) + + TargetSetExecutedBy( target, attacker ) + + AddAnimEvent( target, "mark_for_death", MarkForDeath ) + AddAnimEvent( target, "phase_gib", PhaseGib ) + + FirstPersonSequenceStruct targetSequence + targetSequence.blendTime = 0.25 + targetSequence.attachment = "ref" + targetSequence.thirdPersonAnim = action.targetAnimation3p + targetSequence.thirdPersonCameraAttachments = [action.thirdPersonCameraAttachment] + targetSequence.thirdPersonCameraVisibilityChecks = true + + if ( isAttackerRef ) + { + targetSequence.useAnimatedRefAttachment = true + if ( target.IsNPC() && IsMultiplayer() ) + { + SetForceDrawWhileParented( target, true ) + } + } + else + { + targetSequence.noParent = true + targetSequence.playerPushable = true + targetSequence.enablePlanting = true + } + + + if ( target.IsPlayer() ) + { + HolsterViewModelAndDisableWeapons( target ) + targetSequence.firstPersonAnim = action.targetAnimation1p + DisableCloakBeforeMelee( target, dataStruct ) + } + + if ( attacker.IsPlayer() ) + { + if ( MeleeTargetrequiresDataKnife( target ) ) + { + string tag = GetTagForKnifeMeleeTarget( target ) + thread AttachPlayerModelForDuration( attacker, DATA_KNIFE_MODEL, tag, 2.2 ) + } + else if ( action.attachTag1p != "" && action.attachModel1p != $"" ) + { + thread AttachPlayerModelForDuration( attacker, action.attachModel1p, action.attachTag1p, 2.2 ) + } + } + + if ( isAttackerRef ) + { + if ( GetCurrentPlaylistVarInt( "fp_embark_enabled", 0 ) == 1 ) + FirstPersonSequenceForce1P( targetSequence, target, attacker ) + + waitthread FirstPersonSequence( targetSequence, target, attacker ) + } + else + { + if ( GetCurrentPlaylistVarInt( "fp_embark_enabled", 0 ) == 1 ) + FirstPersonSequenceForce1P( targetSequence, target ) + + waitthread FirstPersonSequence( targetSequence, target ) + } +} + +#if MP +void function IncrementStatForPilotExecutionWhileCloaked( entity attacker, entity target, PilotVsEnemyStruct dataStruct ) +{ + if ( !IsAlive( attacker ) ) + return + + if ( IsAlive( target ) ) + return + + if ( !target.IsPlayer() ) + return + + if ( !dataStruct.wasCloaked ) + return + + IncrementPlayerDidPilotExecutionWhileCloaked( attacker ) //Kinda clumsy we have to do it here instead of where all the other kill stats are incremented. Mainly because we turn cloak off at the start of execution so you can't do it where all the other kill stats are incremented +} +#endif + +void function TargetClearedExecuted( entity target ) +{ + target.ClearParent() + target.Solid() + if ( target.ContextAction_IsMeleeExecution() ) + target.PlayerMelee_ExecutionEndTarget() + if ( target.IsPlayer() ) + ClearPlayerAnimViewEntity( target ) +} + +void function TargetSetExecutedBy( entity target, entity attacker ) +{ + //Break out of context actions like hacking control panel etc + if ( target.ContextAction_IsActive() ) + target.Anim_Stop() + + target.PlayerMelee_ExecutionStartTarget( attacker ) + target.e.syncedMeleeAttacker = attacker + target.NotSolid() +} + +bool function MeleeTargetrequiresDataKnife( entity target ) +{ + if ( IsProwler( target ) ) + return true + + if ( IsPilotElite( target ) ) + return true + + return false +} + +string function GetTagForKnifeMeleeTarget( entity target ) +{ + Assert( MeleeTargetrequiresDataKnife( target ) ) + + if ( IsProwler( target ) ) + return "PROPGUN" + + if ( IsPilotElite( target ) ) + return "KNIFE" + + unreachable +} + +function AttachPlayerModelForDuration( var player, asset modelName, var tag, var time ) +{ + expect entity( player ) + + if ( !IsValid( player ) ) + return + + Assert( IsValid( tag ), "No tag specified for player" ) + + entity viewModel = player.GetFirstPersonProxy() //JFS: Defensive fix for player not having view models sometimes + if ( !IsValid( viewModel ) ) + return + + if ( !EntHasModelSet( viewModel ) ) + return + + entity model = CreatePropDynamic( modelName ) + model.SetParent( viewModel, tag, false, 0.0 ) + + OnThreadEnd( + function() : ( model ) + { + if ( IsValid( model ) ) + model.Destroy() + } + ) + + player.EndSignal( "OnDeath" ) + + wait time +} + +void function MarkForDeath( entity target ) +{ + if ( target.IsNPC() ) + { + //printt("Killing marked for death npc " + target ) + //Just kill off NPC now, otherwise it will play pain animations on death + CodeCallback_OnMeleeKilled( target ) + return + } + + //printt("marking player " + target + " for death") + target.e.markedForExecutionDeath = true //This will kill off the player even if the execution animation is interruped from this point forward + + target.EndSignal( "OnDeath" ) + + OnThreadEnd( + function() : ( target ) + { + target.e.markedForExecutionDeath = false + } + ) + + WaitForever() + +} + +void function PhaseGib( entity target ) +{ + if ( !IsAlive( target ) ) + return + + target.ClearInvulnerable() + + entity attacker + if ( IsValid( target.e.syncedMeleeAttacker ) ) + { + attacker = target.e.syncedMeleeAttacker + } + else if ( IsValid( target.e.lastSyncedMeleeAttacker ) ) + { + attacker = target.e.lastSyncedMeleeAttacker + } + else + { + attacker = null + } + + int damageAmount = target.GetMaxHealth() + 1 + target.TakeDamage( damageAmount , attacker, attacker, { forceKill = true, damageType = DMG_MELEE_EXECUTION, damageSourceId = eDamageSourceId.human_execution, scriptType = DF_NO_INDICATOR | DF_GIB } ) +} + + +entity function CreateSyncedMeleeRef( entity attacker, entity target, SyncedMelee action ) +{ + entity ref = CreateMeleeScriptMoverBetweenEnts( attacker, target ) + + vector angles = target.GetAngles() + angles.x = ref.GetAngles().x + + ref.SetAngles( angles ) + if ( action.animRefPos == "attacker" ) + ref.SetOrigin( attacker.GetOrigin() ) + else + ref.SetOrigin( target.GetOrigin() ) + return ref +} + +void function ApplyGruntExecutionDamage( entity ref, entity attacker, entity target, float damageDealt ) +{ + ref.EndSignal( "OnDestroy" ) + attacker.EndSignal( "OnDeath" ) + target.EndSignal( "OnDeath" ) + + for ( ;; ) + { + table results = attacker.WaitSignal( "NpcDealsExecutionDamage" ) + float damage + switch ( results.parm ) + { + case "lethal": + damage = float( target.GetMaxHealth() ) + break + + case "nonlethal": + damage = min( target.GetHealth() - 10, target.GetMaxHealth() * damageDealt ) + break + } + + target.TakeDamage( damage, attacker, attacker, { damageSourceId=eDamageSourceId.human_execution, scriptType = DF_RAGDOLL } ) + } +} diff --git a/Northstar.Custom/mod/scripts/vscripts/melee/_melee_synced_titan.gnut b/Northstar.Custom/mod/scripts/vscripts/melee/_melee_synced_titan.gnut new file mode 100644 index 00000000..be2e512e --- /dev/null +++ b/Northstar.Custom/mod/scripts/vscripts/melee/_melee_synced_titan.gnut @@ -0,0 +1,1610 @@ +untyped + +global function MeleeSyncedTitan_Init + +const TITANARMMODEL = $"models/weapons/arms/atlaspov.mdl" +const TEAM_JUMPJET_DBL = $"P_team_jump_jet_DBL" + +enum eTitanExecutionType +{ + fistThroughCockpit + dummy //not used yet +} + +struct TitanExcutionData +{ + string attackerAnimation3p + string attackerAnimation3p_vsAutoTitan + table attackerAnimation3pPilot + table targetAnimation3p + table targetAnimation3pPilot + string sound_1p + string sound_3p + array thirdPersonCameraAttachments + array linkedExecutions +} + +struct +{ + table executionData_3p +} file + +int RAGDOLL_IMPACT_TABLE_IDX = -1 + +function MeleeSyncedTitan_Init() +{ + RAGDOLL_IMPACT_TABLE_IDX = PrecacheImpactEffectTable( "ragdoll_human" ) + AddSyncedMeleeServerThink( GetSyncedMeleeChooser( "titan", "titan" ), MeleeThread_TitanVsTitan ) + + if ( GetBugReproNum() == 129802 ) + { + AddDeathCallback( "npc_titan", OnNPCTitanDeath ) + } + + PrecacheWeapon( "mp_titanweapon_salvo_rockets" ) + PrecacheParticleSystem( TEAM_JUMPJET_DBL ) + + Init3pExecutions() +} + +void function Init3pExecutions() +{ + var dataTable = GetDataTable( $"datatable/titan_executions.rpak" ) + int numRows = GetDatatableRowCount( dataTable ) + for ( int row=0; row camAttachments = split( camAttach, " " ) + + array linkedExecutionArray = SplitAndStripStringArray( GetDataTableString( dataTable, row, GetDataTableColumnByName( dataTable, "linkedExecutions" ) ) ) + + TitanExcutionData data + data.attackerAnimation3p = attackerAnimation3p + data.attackerAnimation3p_vsAutoTitan = attackerAnimation3p_vsAutoTitan + data.targetAnimation3p[ "stryder" ] <- targetAnimation3p_lt + data.targetAnimation3p[ "atlas" ] <- targetAnimation3p_md + data.targetAnimation3p[ "ogre" ] <- targetAnimation3p_hv + data.targetAnimation3pPilot[ "stryder" ] <- targetAnimation3pPilot_lt + data.targetAnimation3pPilot[ "atlas" ] <- targetAnimation3pPilot_md + data.targetAnimation3pPilot[ "ogre" ] <- targetAnimation3pPilot_hv + data.attackerAnimation3pPilot[ "stryder" ] <- attackerAnimation3pPilot_lt + data.attackerAnimation3pPilot[ "atlas" ] <- attackerAnimation3pPilot_md + data.attackerAnimation3pPilot[ "ogre" ] <- attackerAnimation3pPilot_hv + data.sound_1p = sound_1p + data.sound_3p = sound_3p + data.thirdPersonCameraAttachments = camAttachments + data.linkedExecutions = linkedExecutionArray + return data +} + +array function SplitAndStripStringArray( string combinedString ) +{ + array stringArray = split( combinedString, "," ) + + foreach ( i, value in stringArray ) + { + stringArray[ i ] = strip( value ) + } + + return stringArray +} + + +struct MeleeThread_TitanVsTitanDataStruct +{ + bool setAttackerInvulnerable = false + bool setAttackerDemigod = false +} + +bool function MeleeThread_TitanVsTitan( SyncedMelee action, entity attacker, entity target ) +{ + // function off for reload scripts + return MeleeThread_TitanVsTitan_Internal( action, attacker, target ) +} + +bool function MeleeThread_TitanVsTitan_Internal( SyncedMelee action, entity attacker, entity target ) +{ + Assert( target.IsTitan(), target + " is not Titan target" ) + Assert( attacker.IsPlayer() && attacker.IsTitan(), attacker + " is not Titan attacker" ) + + #if SERVER + printt( "Player", attacker, "attempting to melee", target, "TitanVsTitanMelee" ) + #endif + + if ( attacker.ContextAction_IsActive() || target.ContextAction_IsActive() ) + { + printt("Either attacker or target already in ContextAction! Exiting Titan Vs Titan melee attempt") + return false + } + + if ( !IsAlive( attacker ) ) + return false + + if ( !IsAlive( target ) ) + return false + + void functionref( SyncedMelee action, entity attacker, entity target ) func + func = GetTitanSyncedMeleeFunc( attacker, target ) + if ( func == null ) + return false + + attacker.GetTitanSoul().Signal( "OnSyncedMelee" ) //Need the signal on the soul to clean-up tether traps during synced executions. + + // JFS: signals can kill things mid frame: R2DLC-311 SCRIPT ERROR: PHONE_HOME: [SERVER] Entity is null + if ( !IsAlive( attacker ) ) + return false + + if ( !IsAlive( target ) ) + return false + + target.GetTitanSoul().Signal( "OnSyncedMelee" ) + + // JFS: signals can kill things mid frame: R2DLC-311 SCRIPT ERROR: PHONE_HOME: [SERVER] Entity is null + if ( !IsAlive( attacker ) ) + return false + + if ( !IsAlive( target ) ) + return false + //attacker.Signal( "OnSyncedMelee" ) + //target.Signal( "OnSyncedMelee" ) + + MeleeThread_TitanVsTitanDataStruct dataStruct + + OnThreadEnd( + function() : ( attacker, target, dataStruct ) + { + if ( IsValid( attacker ) ) + { + if ( dataStruct.setAttackerInvulnerable ) + attacker.ClearInvulnerable() + + if ( dataStruct.setAttackerDemigod ) + DisableDemigod( attacker ) + + attacker.PlayerMelee_SetState( PLAYER_MELEE_STATE_NONE ) + } + } + ) + + string titanSubClass = GetSoulTitanSubClass( attacker.GetTitanSoul() ) + + entity burnCardTarget + entity bossPlayer = target.GetBossPlayer() + if ( target.IsNPC() ) + { + if ( IsValid( bossPlayer ) ) + burnCardTarget = bossPlayer + } + else + { + burnCardTarget = target + } + + attacker.PlayerMelee_ExecutionStartAttacker( 0 ) + target.PlayerMelee_ExecutionStartTarget( attacker ) + + attacker.Lunge_ClearTarget() + + ForceTitanSustainedDischargeEnd( target ) + + #if TITAN_EXECUTION_ATTACKER_IS_INVULNERABLE + dataStruct.setAttackerInvulnerable = true + attacker.SetInvulnerable() + #else + dataStruct.setAttackerDemigod = true + EnableDemigod( attacker ) + #endif + + waitthread func( action, attacker, target ) + + if ( !IsValid( attacker ) ) + return true + + attacker.Signal( "SyncedMeleeComplete" ) + #if MP + if ( attacker.IsPlayer() ) + AddPlayerScore( attacker, "Execution" ) + #endif + return true +} + +void functionref( SyncedMelee action, entity attacker, entity target ) function GetTitanSyncedMeleeFunc( entity attacker, entity target ) +{ + if ( GetCurrentPlaylistVarInt( "titan_executions_always_short", 0 ) != 0 ) + return MeleeThread_AtlasVsTitanShort + + entity soul = attacker.GetTitanSoul() + #if SP + TitanLoadoutDef loadout = GetTitanLoadoutForCurrentMap() + #else + TitanLoadoutDef loadout = soul.soul.titanLoadout // GetActiveTitanLoadout( attacker ) + #endif + string executionRef = loadout.titanExecution + if ( SoulHasPassive( soul, ePassives.PAS_VANGUARD_COREMETER ) ) + executionRef = "execution_vanguard_kit" + + if ( executionRef in file.executionData_3p ) + return TitanVsTitan_3p + + if ( target.IsNPC() ) + { + entity bossPlayer = target.GetBossPlayer() + if ( IsValid( bossPlayer ) || !IsVDUTitan( target ) ) + return MeleeThread_AtlasVsTitanShort + } + + string attackerType = GetSoulTitanSubClass( soul ) + + switch ( attackerType ) + { + case "stryder": + return MeleeThread_StyderVsTitan + + case "ogre": + return MeleeThread_OgreVsTitan + + case "atlas": + case "buddy": + return MeleeThread_AtlasVsTitan + } + + return null +} + +void function MeleeThread_AtlasVsTitanShort( SyncedMelee action, entity attacker, entity target ) +{ + if ( !IsAlive( attacker ) ) + return + + if ( !IsAlive( target ) ) + return + + string attackerAnimation1p = "atpov_melee_sync_frontkill_autotitan" + string attackerAnimation3p = "at_melee_sync_frontkill_autotitan" + string targetAnimation3p = "at_melee_sync_frontdeath_autotitan" + + target.Signal( "TitanStopsThinking" ) // in future, need to make titan scripted anims co-exist better and not require gotcha stuff like this -Mackey + + local e = {} + e.attackerViewBody <- null + + e.attackerStartOrg <- attacker.GetOrigin() + + entity ref = CreateMeleeScriptMoverBetweenEnts( attacker, target ) + + FirstPersonSequenceStruct attackerSequence + attackerSequence.blendTime = 0.25 + attackerSequence.attachment = "ref" + + FirstPersonSequenceStruct targetSequence = clone attackerSequence + + attackerSequence.thirdPersonAnim = attackerAnimation3p + // attackerSequence.thirdPersonAnimIdle = "at_melee_sync_frontkill_end_idle" + + attackerSequence.firstPersonAnim = attackerAnimation1p + targetSequence.thirdPersonAnim = targetAnimation3p + targetSequence.blendTime = 0.25 + + target.e.syncedMeleeAttacker = attacker + + // attacker.SetInvulnerable() + target.SetInvulnerable() //HACK: Have to SetInvulnerable first before attacker holsters weapon, because if the attacker is vortexing, holster will release bullets caught and kill off the victim if low enough health + + //HACK! This function was originally for NPCs only, but now that it is being used for players, we need to holster their weapon + if ( target.IsPlayer() ) + HolsterAndDisableWeapons( target ) + + if ( ShouldHolsterWeaponForSyncedMelee( attacker ) ) + HolsterAndDisableWeapons( attacker ) + + local attackerViewBody + + // needs shortened verions + EmitDifferentSoundsOnEntityForPlayerAndWorld( "Titan_1p_Sync_Melee_vs_AutoTitan", "Titan_3p_Sync_Melee_vs_AutoTitan", attacker, attacker ) + + local soul = target.GetTitanSoul() + soul.SetInvalidHealthBarEnt( true ) + + AddAnimEvent( target, "rider_rodeo_over", ForceTitanRodeoToEnd ) + + target.SetInvulnerable() //Setting target of execution as invulnerable to prevent them dying mid-way + + OnThreadEnd( + function() : ( ref, attacker, target, e ) + { + if ( IsValid( ref ) ) + { + if ( IsValid( attacker ) ) + attacker.ClearParent() + + if ( IsValid( target ) ) + target.ClearParent() + + AssertNoPlayerChildren( ref ) + ref.Destroy() + } + + if ( IsValid( attacker ) ) + { + //attacker.ClearInvulnerable() + attacker.UnforceStand() + attacker.ClearParent() + ClearPlayerAnimViewEntity( attacker ) + DeployAndEnableWeapons( attacker ) + attacker.PlayerMelee_ExecutionEndAttacker() + + if ( IsAlive( attacker ) ) + { + // if we got into solid, teleport back to safe place + if ( !PutEntityInSafeSpot( attacker, null, null, expect vector( e.attackerStartOrg ), attacker.GetOrigin() ) ) + { + printt( "PutEntityInSafeSpot failed, putting him back at the start origin" ) + attacker.SetOrigin( expect vector( e.attackerStartOrg ) ) + } + + } + } + + if ( IsValid( target ) ) + { + if ( !target.IsNPC() ) + { + target.PlayerMelee_ExecutionEndTarget() + ClearPlayerAnimViewEntity( target ) + DeployAndEnableWeapons( target ) + } + + + if ( IsAlive( target ) ) + { + local attack = attacker + if ( !IsValid( attack ) ) + attack = null + + target.Die( attack, attack, { scriptType = 0, damageSourceId = eDamageSourceId.titan_execution } ) + } + + target.e.syncedMeleeAttacker = null + + if ( HasAnimEvent( target, "rider_rodeo_over" ) ) + DeleteAnimEvent( target, "rider_rodeo_over" ) + } + } + ) + + thread FirstPersonSequence( targetSequence, target, ref ) + waitthread FirstPersonSequence( attackerSequence, attacker, ref ) + + //wait ( 50.0 / 30.0 ) // 37 frames in +} + + +void function MeleeThread_StyderVsTitan( SyncedMelee action, entity attacker, entity target ) +{ + table e + e.gib <- true + e.attackerAnimation1p <- "strypov_melee_sync_frontkill" + e.attackerAnimation3p <- "stry_melee_sync_frontkill" + e.targetAnimation3p <- "stry_melee_sync_frontdeath" + e.targetPilotAnimationForAttacker <- "pt_stry_melee_sync_front_pilotkill_1st" + e.targetPilotAnimationForObserver <- "pt_stry_melee_sync_front_pilotkill_3rd" + e.targetPilotAnimationForObserver1st <- "ptpov_stry_tvtmelee_targetdeath" + e.TitanSpecific1pSyncMeleeSound <- "Stryder_1p_Sync_Melee" + e.TitanSpecific3pSyncMeleeSound <- "Stryder_3p_Sync_Melee" + + MeleeThread_TitanRipsPilot( e, action, attacker, target ) +} + +void function MeleeThread_AtlasVsTitan( SyncedMelee action, entity attacker, entity target ) +{ + table e + e.gib <- false + e.attackerAnimation1p <- "atpov_melee_sync_frontkill" + e.attackerAnimation3p <- "at_melee_sync_frontkill" + e.targetAnimation3p <- "at_melee_sync_frontdeath" + e.targetPilotAnimationForAttacker <- "pt_melee_sync_front_pilotkill_1st" + e.targetPilotAnimationForObserver <- "pt_melee_sync_front_pilotkill_3rd" + e.targetPilotAnimationForObserver1st <- "ptpov_tvtmelee_targetdeath" + e.TitanSpecific1pSyncMeleeSound <- "Atlas_1p_Sync_Melee" + e.TitanSpecific3pSyncMeleeSound <- "Atlas_3p_Sync_Melee" + + MeleeThread_TitanRipsPilot( e, action, attacker, target ) +} + +function MeleeThread_TitanRipsPilot( table e, SyncedMelee action, entity attacker, entity target ) +{ + e.attackerViewBody <- null + e.attacker <- attacker + e.attackerStartOrg <- attacker.GetOrigin() + + entity ref = CreateMeleeScriptMoverBetweenEnts( attacker, target ) + + FirstPersonSequenceStruct attackerSequence + attackerSequence.blendTime = 0.25 + attackerSequence.attachment = "ref" + + FirstPersonSequenceStruct targetSequence = clone attackerSequence + + attackerSequence.thirdPersonAnim = expect string ( e.attackerAnimation3p ) + // attackerSequence.thirdPersonAnimIdle = "at_melee_sync_frontkill_end_idle" + + attackerSequence.firstPersonAnim = expect string( e.attackerAnimation1p ) + targetSequence.thirdPersonAnim = expect string ( e.targetAnimation3p ) + targetSequence.blendTime = 0.25 + + target.e.syncedMeleeAttacker = attacker + + // attacker.SetInvulnerable() + target.SetInvulnerable() //HACK: Have to SetInvulnerable first before attacker holsters weapon, because if the attacker is vortexing, holster will release bullets caught and kill off the victim if low enough health + if ( ShouldHolsterWeaponForSyncedMelee( attacker ) ) + HolsterAndDisableWeapons( attacker ) + + if ( !target.IsNPC() ) + HolsterAndDisableWeapons( target ) + + EmitDifferentSoundsOnEntityForPlayerAndWorld( expect string ( e.TitanSpecific1pSyncMeleeSound ), expect string ( e.TitanSpecific3pSyncMeleeSound ), attacker, attacker ) + + entity attackerViewBody + bool targetIsPlayer = target.IsPlayer() + + if ( targetIsPlayer ) + { + attackerViewBody = Wallrun_CreateCopyOfPilotModel( target ) //attackerViewBody is the model of the pilot getting ripped out of the cockpit + } + else + { + attackerViewBody = CreateNpcTitanPilotModel( target ) + } + + attackerViewBody.SetOrigin( ref.GetOrigin() ) + e.attackerViewBody = attackerViewBody + attackerViewBody.SetOwner( attacker ) + attackerViewBody.kv.VisibilityFlags = ENTITY_VISIBLE_TO_OWNER + attackerViewBody.SetRagdollImpactFX( RAGDOLL_IMPACT_TABLE_IDX ) + attackerViewBody.SetContinueAnimatingAfterRagdoll( true ) + + FirstPersonSequenceStruct attackerBodySequence + attackerBodySequence.attachment = "ref" + attackerBodySequence.teleport = true + attackerBodySequence.thirdPersonAnim = expect string ( e.targetPilotAnimationForAttacker ) + + FirstPersonSequenceStruct targetBodySequence + targetBodySequence.attachment = "ref" + targetBodySequence.blendTime = 0.25 + targetBodySequence.thirdPersonAnim = expect string ( e.targetPilotAnimationForObserver ) + targetBodySequence.firstPersonAnim = expect string ( e.targetPilotAnimationForObserver1st ) + + + entity targetSoul = target.GetTitanSoul() + targetSoul.SetInvalidHealthBarEnt( true ) + + entity targetTitan + if ( targetIsPlayer ) + { + e.oldPlayerSettings <- target.s.storedPlayerSettings + //target.s.storedPlayerSettings = "pilot_titan_cockpit" // Makes player have titan cockpit temporarily. Turned off to avoid having extra checks all over in script + targetTitan = CreateAutoTitanForPlayer_ForTitanBecomesPilot( target ) //TargetTitan is the NPC Titan that is created temporarily during execution + DispatchSpawn( targetTitan ) + + TitanBecomesPilot( target, targetTitan ) + DisableTitanRodeo( targetTitan ) + targetTitan.SetOwner( target ) + targetTitan.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) //owner cant see + targetTitan.PlayerMelee_ExecutionStartTarget( attacker ) + e.target <- target + } + else + { + targetTitan = target + + // target is now a random dude + target = CreateSoldier( target.GetTeam(), Vector(0,0,0), Vector(0,0,0) ) + DispatchSpawn( target ) + e.target <- target + } + + + AddAnimEvent( targetTitan, "rider_rodeo_over", ForceTitanRodeoToEnd ) + AddAnimEvent( targetTitan, "melee_killed_ragdoll", MeleeKilledRagdoll, attacker ) + + targetTitan.SetInvulnerable() //Setting target of execution as invulnerable to prevent them dying mid-way + + target.SetOwner( attacker ) + target.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) //owner cant see + e.targetTitan <- targetTitan + + if ( GetBugReproNum() == 129802 ) + thread OnNPCTitanSignalDeath( targetTitan ) + + OnThreadEnd( + function() : ( ref, attacker, target, targetTitan, e ) + { + if ( IsValid( ref ) ) + { + if ( IsValid( attacker ) ) + { + attacker.ClearParent() + } + else + { + TryClearParent( attacker ) + } + + if ( IsValid( target ) ) + { + target.ClearParent() + } + else + { + TryClearParent( target ) + } + + AssertNoPlayerChildren( ref ) + ref.Kill_Deprecated_UseDestroyInstead() + } + + if ( IsValid( attacker ) ) + { + attacker.UnforceStand() + attacker.ClearParent() + ClearPlayerAnimViewEntity( attacker ) + DeployAndEnableWeapons( attacker ) + attacker.PlayerMelee_ExecutionEndAttacker() + + if ( IsAlive( attacker ) ) + { + // if we got into solid, teleport back to safe place + PutEntityInSafeSpot( attacker, null, null, expect vector( e.attackerStartOrg ), attacker.GetOrigin() ) + } + } + + if ( IsValid( target ) ) + { + if ( !target.IsNPC() ) + { + target.PlayerMelee_ExecutionEndTarget() + ClearPlayerAnimViewEntity( target ) + DeployAndEnableWeapons( target ) + } + + if ( HasAnimEvent( target, "pink_mist" ) ) + DeleteAnimEvent( target, "pink_mist" ) + + if ( IsAlive( expect entity( e.target ) ) ) + MeleePinkMist( e ) + + target.e.syncedMeleeAttacker = null + } + + if ( IsValid( e.attackerViewBody ) ) + e.attackerViewBody.Kill_Deprecated_UseDestroyInstead() + + if ( GetBugReproNum() != 129802 && IsAlive( targetTitan ) ) + { + if ( IsValid( attacker ) ) + targetTitan.Die( attacker, attacker, { scriptType = DF_MELEE, damageSourceId = eDamageSourceId.titan_execution } ) + else + targetTitan.Die() + + if ( GetBugReproNum() == 129815 ) + { + targetTitan.SetContinueAnimatingAfterRagdoll( true ) + targetTitan.BecomeRagdoll( Vector(0,0,0), false ) + } + } + } + ) + + target.EndSignal( "OnRespawnPlayer" ) + + waitthread TitanSyncedMeleeAnimationsPlay( attackerBodySequence, attackerViewBody, ref, targetBodySequence, target, attackerSequence, attacker, targetSequence, targetTitan, e ) +} + +entity function CreateNpcTitanPilotModel( entity titan ) +{ + asset modelName = GetNpcTitanPilotModel( titan ) + return CreatePropDynamic( modelName ) +} + + + +asset function GetNpcTitanPilotModel( entity titan ) +{ + asset modelName = TEAM_IMC_GRUNT_MODEL + + #if HAS_BOSS_AI + if ( IsBossTitan( titan ) ) + { + modelName = GetBossTitanCharacterModel( titan ) + } + #endif + + return modelName +} + +function TitanSyncedMeleeAnimationsPlay( FirstPersonSequenceStruct attackerBodySequence, entity attackerViewBody, entity ref, FirstPersonSequenceStruct targetBodySequence, entity target, FirstPersonSequenceStruct attackerSequence, entity attacker, FirstPersonSequenceStruct targetSequence, entity targetTitan, table e ) +{ + e.thrown <- false + OnThreadEnd ( + function () : ( targetTitan, target, attacker, e ) + { + // insure visibility + if ( IsValid( targetTitan ) ) + targetTitan.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE + + if ( !IsAlive( attacker ) ) + { + attacker.Anim_Stop() + + if ( !e.thrown && IsAlive( target ) ) + { + target.Anim_Stop() + target.SetOwner( null ) + target.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE + if ( target.IsPlayer() ) + { + ClearPlayerAnimViewEntity( target ) + target.GetFirstPersonProxy().Anim_Stop() + target.SetPlayerSettings( e.oldPlayerSettings ) + } + + } + } + } + ) + + attacker.EndSignal( "OnDeath" ) + target.EndSignal( "OnDestroy" ) + target.EndSignal( "OnRespawnPlayer" ) + + thread FirstPersonSequence( attackerBodySequence, attackerViewBody, ref ) + if ( !target.IsPlayer() ) + { + // don't do first person anims if we're not a player + targetBodySequence.firstPersonAnim = "" + targetBodySequence.firstPersonAnimIdle = "" + } + + thread FirstPersonSequence( targetBodySequence, target, ref ) + thread FirstPersonSequence( attackerSequence, attacker, ref ) + thread FirstPersonSequence( targetSequence, targetTitan, ref ) + targetTitan.Anim_AdvanceCycleEveryFrame( true ) + local duration = attacker.GetSequenceDuration( attackerSequence.thirdPersonAnim ) + + if ( e.targetAnimation3p == "at_melee_sync_frontdeath" ) + { + thread MeleeThrowIntoWallSplat( attacker, target, e ) + } + else + { + AddAnimEvent( target, "pink_mist", MeleePinkMistAnimEvent, e ) + } + + float timer + string titanType = GetSoulTitanSubClass( attacker.GetTitanSoul() ) + switch ( titanType ) + { + case "stryder": + timer = 0.9 + break + case "atlas": + case "buddy": + timer = 0.45 + break + default: + Assert( 0, "Unknown titan type " + titanType ) + } + + wait timer + + // first the victim cant see his titan, as a pilot, and then he can + targetTitan.SetNextThinkNow() + targetTitan.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE + targetTitan.SetNextThinkNow() + wait duration - timer +} + +void function MeleePinkMistAnimEvent( entity target ) //parameter isn't used, but function signature is like this because it's being called from an anim event +{ + table e = expect table( GetOptionalAnimEventVar( target, "pink_mist" ) ) + + MeleePinkMist( e ) +} + +void function MeleePinkMist( table e ) +{ + entity target = expect entity( e.target ) + + if ( !IsAlive( target ) ) + return + + e.attackerViewBody.Dissolve( ENTITY_DISSOLVE_PINKMIST, Vector( 0, 0, 0 ), 0 ) + if ( IsValid( e.attacker ) ) + { + target.Die( e.attacker, e.attacker, { damageSourceId = eDamageSourceId.titan_execution, scriptType = DF_GIB } ) + } + else + { + target.Die( e.target, target, { damageSourceId = eDamageSourceId.titan_execution, scriptType = DF_GIB } ) + } + + if ( target.IsPlayer() ) + ClearPlayerAnimViewEntity( target ) + + target.ClearInvulnerable() +} + +function MeleeThrowIntoWallSplat( entity attacker, entity target, e ) +{ + OnThreadEnd( + function () : ( target, e ) + { + if ( IsValid( target ) ) + { + target.ClearParent() + target.Anim_Stop() + target.ClearInvulnerable() + } + } + ) + + target.EndSignal( "OnDeath" ) + + e.startOrigin <- target.GetOrigin() + wait 2.8 + e.thrown = true + + + // attacker got killed? saved! + if ( !IsAlive( attacker ) ) + return + + local angles = attacker.GetAngles() + angles = AnglesCompose( angles, Vector( -15, 0, 0 ) ) + local forward = AnglesToForward( angles ) + + local endPos + for ( ;; ) + { + if ( !target.Anim_IsActive() ) + break + + local org = target.GetOrigin() + if ( IsAlive( attacker ) ) + { + TraceResults titanPilotTrace = TraceLine( attacker.EyePosition(), org, attacker ) + + if ( titanPilotTrace.fraction < 1.0 ) + { + endPos = titanPilotTrace.endPos + break + } + } + + + TraceResults result = TraceLine( org, org + forward * 200 ) + if ( result.fraction < 1.0 ) + { + wait result.fraction * 0.06 + break + } + + WaitFrame() + } + + if ( endPos ) + { + target.SetOrigin( endPos ) + } + + Assert( IsAlive( target ) ) + + target.ClearInvulnerable() + + target.BecomeRagdoll( Vector(0,0,0), false ) + + WaitFrame() // ragdoll take hold! + EmitSoundOnEntity( target, "Titan_Victim_Wall_Splat" ) + + if ( e.gib ) + { + local force = Vector(0,0,0) + if ( IsAlive( attacker ) ) + { + local vec = target.GetOrigin() - attacker.GetOrigin() + vec.Norm() + force = vec + } + target.Die( attacker, attacker, { scriptType = DF_GIB | DF_KILLSHOT, force = force, damageSourceId = eDamageSourceId.titan_execution } ) + } + else + { + target.Die( attacker, attacker, { scriptType = DF_KILLSHOT, damageSourceId = eDamageSourceId.titan_execution } ) + } +} + + +function MeleeAnimThrow( attacker, target, throwDuration ) +{ + attacker.EndSignal( "OnDeath" ) + target.EndSignal( "OnDeath" ) + wait throwDuration - 0.2 + + local angles = attacker.GetAngles() + local forward = AnglesToForward( angles ) + target.ClearParent() + target.SetVelocity( forward * 500 ) + + + target.Die( attacker, attacker, { scriptType = DF_KILLSHOT, damageSourceId = eDamageSourceId.titan_execution } ) +} + +/////////////////////////////////////// +// OGRE MELEES +/////////////////////////////////////// +void function MeleeThread_OgreVsTitan( SyncedMelee action, entity attacker, entity target ) +{ + string attackerAnimation1p = "ogpov_melee_armrip_attacker" + string attackerAnimation3p = "og_melee_armrip_attacker" + string targetAnimation1p = "ogpov_melee_armrip_victim" + string targetAnimation3p = "og_melee_armrip_victim" + + table e = {} + e.attackerStartOrg <- attacker.GetOrigin() + e.lostArm <- false + e.targetStartOrg <- target.GetOrigin() + + entity ref = CreateMeleeScriptMoverBetweenEnts( attacker, target ) + + FirstPersonSequenceStruct attackerSequence + attackerSequence.blendTime = 0.25 + attackerSequence.attachment = "ref" + + FirstPersonSequenceStruct targetSequence = clone attackerSequence + + attackerSequence.thirdPersonAnim = attackerAnimation3p + attackerSequence.firstPersonAnim = attackerAnimation1p + + if ( target.IsPlayer() ) + targetSequence.firstPersonAnim = targetAnimation1p + + targetSequence.thirdPersonAnim = targetAnimation3p + targetSequence.blendTime = 0.25 + + target.e.syncedMeleeAttacker = attacker + DisableWeapons( attacker, [] ) + DisableWeapons( target, [] ) + + // attacker.SetInvulnerable() + target.SetInvulnerable() + + entity soul = target.GetTitanSoul() + soul.SetInvalidHealthBarEnt( true ) + + OnThreadEnd( + function() : ( ref, attacker, target, e ) + { + if ( IsValid( ref ) ) + { + if ( IsValid( attacker ) ) + attacker.ClearParent() + + if ( IsValid( target ) ) + target.ClearParent() + + AssertNoPlayerChildren( ref ) + ref.Kill_Deprecated_UseDestroyInstead() + } + + if ( IsValid( attacker ) ) + { + attacker.UnforceStand() + attacker.ClearParent() + ClearPlayerAnimViewEntity( attacker ) + EnableWeapons( attacker, [] ) + attacker.PlayerMelee_ExecutionEndAttacker() + + if ( IsAlive( attacker ) ) + { + // if we got into solid, teleport back to safe place + PutEntityInSafeSpot( attacker, null, null, expect vector( e.attackerStartOrg ), attacker.GetOrigin() ) + } + } + + if ( IsValid( target ) ) + { + DeleteAnimEvent( target, "lost_arm" ) + + target.e.syncedMeleeAttacker = null + + target.ClearParent() + target.ClearInvulnerable() + if ( target.IsPlayer() ) + { + ClearPlayerAnimViewEntity( target ) + } + + EnableWeapons( target, [] ) + + if ( !target.IsNPC() ) + target.PlayerMelee_ExecutionEndTarget() + + if ( e.lostArm && IsAlive( target ) ) + { + target.Die( attacker, attacker, { scriptType = DF_KILLSHOT, damageSourceId = eDamageSourceId.titan_execution } ) + return + } + else if ( target.IsPlayer() ) + { + PutEntityInSafeSpot( target, null, null, expect vector( e.targetStartOrg ), target.GetOrigin() ) + } + } + } + ) + + attacker.EndSignal( "OnDeath" ) + + EmitDifferentSoundsOnEntityForPlayerAndWorld( "Ogre_1p_Sync_Melee", "Ogre_3p_Sync_Melee", attacker, attacker ) + + AddAnimEvent( target, "lost_arm", TitanLostArm, e ) + + + thread FirstPersonSequence( targetSequence, target, ref ) + waitthread FirstPersonSequence( attackerSequence, attacker, ref ) +} + +//Very similar to the above function for now, eventually won't have the 1st person component at all. +void function TitanVsTitan_3p( SyncedMelee action, entity attacker, entity target ) +{ + if ( !IsAlive( attacker ) ) + return + + if ( !IsAlive( target ) ) + return + + entity attackerSoul = attacker.GetTitanSoul() + #if SP + TitanLoadoutDef loadout = GetTitanLoadoutForCurrentMap() + string executionRef = loadout.titanExecution + TitanExcutionData data = file.executionData_3p[ executionRef ] + #else + TitanLoadoutDef loadout = attackerSoul.soul.titanLoadout // GetActiveTitanLoadout( attacker ) + string executionRef = loadout.titanExecution + TitanExcutionData data = file.executionData_3p[ executionRef ] + if ( data.linkedExecutions.len() > 0 ) + { + array clonedLinkedExecutions = clone data.linkedExecutions + for ( int i = clonedLinkedExecutions.len() - 1; i >= 0; i-- ) + { + if ( GetItemRequiresPrime( clonedLinkedExecutions[ i ] ) == true && !HasPrimeToMatchExecutionType( attacker, GetItemType( clonedLinkedExecutions[ i ] ) ) ) + clonedLinkedExecutions.remove( i ) + } + executionRef = clonedLinkedExecutions.getrandom() + data = file.executionData_3p[ executionRef ] + } + #endif + bool shouldApplyBatteryAfterRodeo = false + if ( SoulHasPassive( attackerSoul, ePassives.PAS_VANGUARD_COREMETER ) ) + { + executionRef = "execution_vanguard_kit" + data = file.executionData_3p[ executionRef ] + shouldApplyBatteryAfterRodeo = true + } + + string victimType = GetSoulTitanSubClass( target.GetTitanSoul() ) + + table e = {} + e.attackerStartOrg <- attacker.GetOrigin() + e.lostArm <- false + e.targetStartOrg <- target.GetOrigin() + + FirstPersonSequenceStruct attackerSequence + attackerSequence.blendTime = 0.25 + attackerSequence.attachment = "ref" + attackerSequence.thirdPersonCameraAttachments = clone data.thirdPersonCameraAttachments + attackerSequence.thirdPersonCameraVisibilityChecks = true + attackerSequence.viewConeFunction = ViewConeZero + attackerSequence.noViewLerp = true + + FirstPersonSequenceStruct targetSequence = clone attackerSequence + + attackerSequence.thirdPersonAnim = data.attackerAnimation3p + attackerSequence.firstPersonAnim = "" + + if ( target.IsPlayer() ) + targetSequence.firstPersonAnim = "" + + targetSequence.thirdPersonAnim = data.targetAnimation3p[ victimType ] + targetSequence.thirdPersonCameraEntity = target + + target.e.syncedMeleeAttacker = attacker + + // HACK FOR SP!!! + e.replacedPrimary <- false + string xo16 = "mp_titanweapon_xo16_shorty" + if ( IsSingleplayer() && attacker.IsPlayer() && data.attackerAnimation3p == "bt_synced_titan_execute_kickshoot_A" ) + { + array weapons = attacker.GetMainWeapons() + if ( weapons.len() > 0 ) + { + if ( weapons[0].GetWeaponClassName() != xo16 ) + { + e.replacedPrimary = true + e.oldPrimary <- weapons[0].GetWeaponClassName() + attacker.SetActiveWeaponBySlot( 0 ) + attacker.ReplaceActiveWeapon( xo16 ) //this assumes the active weapon is the weapon in slot 0 so we need to set active weapon to the one in slot 0 + } + } + } + // END HACK FOR SP!!! + + if ( !target.IsNPC() ) + HolsterViewModelAndDisableWeapons( target ) //Melee anims need to use this to stop players from firing weapons but for the weapon to still show up in the 3p anims + else + DisableWeapons( target, [] ) + + if ( attacker.IsPlayer() ) + { + HolsterViewModelAndDisableWeapons( attacker ) //Melee anims need to use this to stop players from firing weapons but for the weapon to still show up in the 3p anims + attacker.Anim_StopGesture( DEFAULT_SCRIPTED_ANIMATION_BLEND_TIME ) + } + + // attacker.SetInvulnerable() + target.SetInvulnerable() + + entity targetViewBody + FirstPersonSequenceStruct targetBodySequence + entity attackerViewBody + FirstPersonSequenceStruct attackerBodySequence + + bool titanHasPilot = target.IsPlayer() + #if HAS_BOSS_AI + titanHasPilot = titanHasPilot || ( IsBossTitan( target ) ) + #endif + + if ( attacker.IsPlayer() ) + { + Remote_CallFunction_Replay( attacker, "SCB_StopTitanCockpitSounds" ) + } + + if ( target.IsPlayer() ) + { + Remote_CallFunction_Replay( target, "SCB_StopTitanCockpitSounds" ) + } + + if ( data.targetAnimation3pPilot[ victimType ] != "" && titanHasPilot ) + { + if ( target.IsNPC() ) + targetViewBody = CreateNpcTitanPilotModel( target ) + else + targetViewBody = Wallrun_CreateCopyOfPilotModel( target ) + + targetViewBody.SetOrigin( target.GetOrigin() ) + targetViewBody.SetRagdollImpactFX( RAGDOLL_IMPACT_TABLE_IDX ) + targetViewBody.SetContinueAnimatingAfterRagdoll( true ) + + targetBodySequence.attachment = "ref" + targetBodySequence.teleport = true + targetBodySequence.thirdPersonAnim = data.targetAnimation3pPilot[ victimType ] + + AddAnimEvent( targetViewBody, "pink_mist", MeleePinkMistFakeBody ) + } + + if ( data.attackerAnimation3pPilot[ victimType ] != "" && attacker.IsPlayer() ) + { + attackerViewBody = Wallrun_CreateCopyOfPilotModel( attacker ) + + attackerViewBody.SetOrigin( attacker.GetOrigin() ) + attackerViewBody.SetRagdollImpactFX( RAGDOLL_IMPACT_TABLE_IDX ) + attackerViewBody.SetContinueAnimatingAfterRagdoll( true ) + + attackerBodySequence.attachment = "ref" + attackerBodySequence.teleport = true + attackerBodySequence.thirdPersonAnim = data.attackerAnimation3pPilot[ victimType ] + } + + if ( !IsValid( targetViewBody ) ) + { + attackerSequence.thirdPersonAnim = data.attackerAnimation3p_vsAutoTitan + } + + entity soul = target.GetTitanSoul() + soul.SetInvalidHealthBarEnt( true ) + + bool isAttackerRef = false + if ( GetConVarBool( "melee_titan_execution_attacker_can_be_ref" ) ) + { + isAttackerRef = IsAttackerRef( null, target ) + } + + OnThreadEnd( + function() : ( attacker, target, e, attackerViewBody, targetViewBody, shouldApplyBatteryAfterRodeo, isAttackerRef ) + { + if ( IsValid( attacker ) ) + { + DeleteAnimEvent( attacker, "synced_melee_enable_planting" ) + DeleteAnimEvent( attacker, "rocket_pod_fire_left" ) + DeleteAnimEvent( attacker, "rocket_pod_fire_right" ) + + attacker.UnforceStand() + attacker.ClearParent() + ClearPlayerAnimViewEntity( attacker ) + attacker.PlayerMelee_ExecutionEndAttacker() + ForceTitanSustainedDischargeEnd( attacker ) + DeployViewModelAndEnableWeapons( attacker ) //Melee anims need to use this to stop players from firing weapons but for the weapon to still show up in the 3p anims + if ( IsAlive( attacker ) ) + { + if ( !isAttackerRef && IsValid( target ) ) + { + PutEntityInSafeSpot( attacker, target, null, target.GetOrigin(), attacker.GetOrigin() ) + } + else + { + PutEntityInSafeSpot( attacker, target, null, attacker.GetOrigin(), attacker.GetOrigin() ) + } + + if ( attacker.IsTitan() ) + { + Remote_CallFunction_Replay( attacker, "SCB_PlayTitanCockpitSounds" ) + #if TITAN_EXECUTION_GIVES_BATTERY + Rodeo_GiveExecutingTitanABattery( attacker ) + #else + if ( shouldApplyBatteryAfterRodeo ) + Rodeo_GiveExecutingTitanABattery( attacker ) + #endif + } + + if ( IsSingleplayer() ) + { + if ( e.replacedPrimary ) + { + attacker.ReplaceActiveWeapon( e.oldPrimary ) + } + } + else + { + attacker.Anim_Stop() // if you are fighting an NPC, then they can get destroyed early the moment they explode. But sometimes, your animation isn't done playing yet so you can't move + } + } + + } + + if ( IsValid( target ) ) + { + DeleteAnimEvent( target, "melee_killed_ragdoll" ) + DeleteAnimEvent( target, "execution_battery_show" ) + DeleteAnimEvent( target, "execution_battery_hide" ) + + + if ( HasAnimEvent( target, "rider_rodeo_over" ) ) + DeleteAnimEvent( target, "rider_rodeo_over" ) + + target.e.syncedMeleeAttacker = null + + target.ClearParent() + target.ClearInvulnerable() + if ( target.IsPlayer() ) + { + ClearPlayerAnimViewEntity( target ) + DeployViewModelAndEnableWeapons( target ) //Melee anims need to use this to stop players from firing weapons but for the weapon to still show up in the 3p anims + } + + if ( !target.IsNPC() && target.ContextAction_IsMeleeExecution() ) + target.PlayerMelee_ExecutionEndTarget() + + if ( IsAlive( target ) ) //Should have no need to PlayTitanCockpitSounds for target because the target is going to die + { + target.Die( attacker, attacker, { scriptType = DF_KILLSHOT, damageSourceId = eDamageSourceId.titan_execution } ) + } + else if ( target.IsPlayer() ) + { + if ( isAttackerRef && IsValid( attacker ) ) + { + PutEntityInSafeSpot( target, attacker, null, attacker.GetOrigin(), target.GetOrigin() ) + } + else + { + PutEntityInSafeSpot( target, attacker, null, target.GetOrigin(), target.GetOrigin() ) + } + } + } + + if ( IsValid( attackerViewBody ) ) + { + //DeleteAnimEvent( attackerViewBody, "rodeo_battery_rip" ) + DeleteAnimEvent( attackerViewBody, "execution_battery_pilot" ) + DeleteAnimEvent( attackerViewBody, "execution_battery_pilot_jump_jets" ) + attackerViewBody.Hide() + attackerViewBody.Destroy() + } + + if ( IsValid( targetViewBody ) ) + { + targetViewBody.Hide() + targetViewBody.Destroy() + } + } + ) + + attacker.EndSignal( "OnDeath" ) + entity bossPlayer = target.GetBossPlayer() + if ( IsValid( bossPlayer ) ) //Executing an auto-Titan, when the pilot disconnects it destroys the auto-titan creating weird circumstances. + bossPlayer.EndSignal( "OnDestroy" ) + target.EndSignal( "OnDestroy" ) + + if ( isAttackerRef ) + { + thread ClearParentOnDeathOrDestroy( target, attacker ) + } + else + { + thread ClearParentOnDeathOrDestroy( attacker, target ) + } + + EmitDifferentSoundsOnEntityForPlayerAndWorld( data.sound_1p, data.sound_3p, attacker, attacker ) + + AddAnimEvent( target, "rider_rodeo_over", ForceTitanRodeoToEnd ) + AddAnimEvent( target, "melee_killed_ragdoll", PredatorMeleeKilledRagdoll ) + AddAnimEvent( attacker, "synced_melee_enable_planting", EnablePlantingOnEntity ) + AddAnimEvent( attacker, "rocket_pod_fire_left", Northstar_Rocket_Pod_Left, target ) + AddAnimEvent( attacker, "rocket_pod_fire_right", Northstar_Rocket_Pod_Right, target ) + AddAnimEvent( target, "execution_battery_show", Execution_ShowBattery ) + AddAnimEvent( target, "execution_battery_hide", Execution_HideBattery ) + if ( attackerViewBody != null ) + { + AddAnimEvent( attackerViewBody, "execution_battery_pilot", Execution_GivePilotBattery ) + AddAnimEvent( attackerViewBody, "execution_battery_pilot_jump_jets", Execution_BatteryStealJumpJets ) + } + + + if ( isAttackerRef ) + { + attackerSequence.enablePlanting = true + attackerSequence.playerPushable = true + targetSequence.useAnimatedRefAttachment = true + } + else + { + targetSequence.enablePlanting = true + targetSequence.playerPushable = true + attackerSequence.useAnimatedRefAttachment = true + } + + array ignoreEnts = [ attacker, target ] + + vector refAngles = GetRefAnglesBetweenEnts( attacker, target ) + + if ( !attacker.IsOnGround() ) + { + refAngles = <0,refAngles.y,0> + } + + vector fwd = AnglesToForward( refAngles ) + fwd *= -1 + vector targetAngles = VectorToAngles( fwd ) + if ( !target.IsNPC() ) + { + targetAngles.x = 0 + target.SetAngles( targetAngles ) + } + + target.SetAngles( targetAngles ) + + bool attackerDoing1PViewbodyAnim = false + + if ( attackerViewBody != null ) + { + attackerDoing1PViewbodyAnim = true + + attackerBodySequence.useAnimatedRefAttachment = true + + // could use FirstPersonSequenceForce1P here, but since this uses a propdynamic rather than a player, easier to do it manually + if ( GetCurrentPlaylistVarInt( "fp_embark_enabled", 0 ) == 1 ) + { + // hide from everyone else + attackerViewBody.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE & ~ENTITY_VISIBLE_TO_OWNER + attackerViewBody.SetOwner( attacker ) + + entity attackerViewBodyProxy = Wallrun_CreateCopyOfPilotModel( attacker ) + attackerViewBodyProxy.SetOrigin( attacker.GetOrigin() ) + attackerViewBodyProxy.SetRagdollImpactFX( RAGDOLL_IMPACT_TABLE_IDX ) + attackerViewBodyProxy.SetContinueAnimatingAfterRagdoll( true ) + attackerViewBodyProxy.SetBodygroup( attackerViewBodyProxy.FindBodyGroup( "head" ), 1 ) + attackerViewBodyProxy.SetOwner( attacker ) + attackerViewBodyProxy.kv.VisibilityFlags = ENTITY_VISIBLE_TO_OWNER + + // create the viewpoint entity + entity camera = CreateEntity( "point_viewcontrol" ) + camera.SetParent( attackerViewBodyProxy, FORCE1P_PILOT_1P_ATTACHMENT ) + camera.kv.spawnflags = 56 + DispatchSpawn( camera ) + attacker.SetViewEntity( camera, false ) + + Remote_CallFunction_NonReplay( attacker, "ServerCallback_HideHudForFPHackAnim" ) + ScreenFadeFromBlack( attacker, 1.0, 0.5 ) + thread Forced1PAttackerViewBodySequence( attackerBodySequence, attackerViewBodyProxy, attacker, camera ) + } + + thread FirstPersonSequence( attackerBodySequence, attackerViewBody, attacker ) + } + + // to my knowledge this is never used in tf2, so not bothering to do anything for it + if ( targetViewBody != null ) + { + targetBodySequence.useAnimatedRefAttachment = true + thread FirstPersonSequence( targetBodySequence, targetViewBody, target ) + } + + if ( isAttackerRef ) + { + if ( GetCurrentPlaylistVarInt( "fp_embark_enabled", 0 ) == 1 ) + { + if ( attacker.IsPlayer() && !attackerDoing1PViewbodyAnim ) + FirstPersonSequenceForce1P( attackerSequence, attacker ) + + if ( target.IsPlayer() ) + FirstPersonSequenceForce1P( targetSequence, target, attacker ) + } + + thread FirstPersonSequence( attackerSequence, attacker ) + waitthread FirstPersonSequence( targetSequence, target, attacker ) + } + else + { + if ( GetCurrentPlaylistVarInt( "fp_embark_enabled", 0 ) == 1 ) + { + if ( target.IsPlayer() ) + FirstPersonSequenceForce1P( targetSequence, target ) + + if ( attacker.IsPlayer() && !attackerDoing1PViewbodyAnim ) + FirstPersonSequenceForce1P( attackerSequence, attacker, target ) + } + + thread FirstPersonSequence( targetSequence, target ) + waitthread FirstPersonSequence( attackerSequence, attacker, target ) + } +} + +void function Forced1PAttackerViewBodySequence( FirstPersonSequenceStruct attackerBodySequence, entity attackerViewBody, entity attacker, entity camera ) +{ + attacker.EndSignal( "OnDestroy" ) + attacker.EndSignal( "OnDeath" ) + attacker.EndSignal( "OnAnimationDone" ) + + OnThreadEnd( function() : ( attackerViewBody, attacker, camera ) + { + attacker.ClearViewEntity() + camera.Destroy() + attackerViewBody.Destroy() + }) + + FirstPersonSequence( attackerBodySequence, attackerViewBody, attacker ) +} + +void function Execution_ShowBattery( entity titan ) +{ + entity titanSoul = titan.GetTitanSoul() + if ( !IsValid( titanSoul ) ) //Out of bounds + return + string titanType = GetSoulTitanSubClass( titanSoul ) + entity batteryContainer = titanSoul.soul.batteryContainer + Assert( IsValid( titanSoul.soul.batteryContainer ), " need to find the repro for this" ) + if ( !IsValid( titanSoul.soul.batteryContainer ) ) + return + + batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_up_idle" ) ) +} + +void function Execution_HideBattery( entity titan ) +{ + entity titanSoul = titan.GetTitanSoul() + if ( !IsValid( titanSoul ) ) //Out of bounds + return + string titanType = GetSoulTitanSubClass( titanSoul ) + entity batteryContainer = titanSoul.soul.batteryContainer + Assert( IsValid( titanSoul.soul.batteryContainer ), " need to find the repro for this" ) + if ( !IsValid( titanSoul.soul.batteryContainer ) ) + return + + batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_down_idle" ) ) + EmitSoundOnEntity( batteryContainer, GetAudioFromAlias( titanType, "rodeo_battery_steal_3p" ) ) +} + +void function Execution_GivePilotBattery( entity fakePilotModel ) +{ + entity tempBattery3p = CreatePropDynamic( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS ) + tempBattery3p.SetParent( fakePilotModel, "R_HAND", false, 0.0 ) + tempBattery3p.RemoveFromSpatialPartition() + tempBattery3p.Show() + Battery_StartFX( tempBattery3p ) +} + + +void function Execution_BatteryStealJumpJets( entity fakePilotModel ) +{ + int attachmentIndex = fakePilotModel.LookupAttachment( "vent_left" ) + int fxIndex = GetParticleSystemIndex( TEAM_JUMPJET_DBL ) + StartParticleEffectOnEntity( fakePilotModel, fxIndex, FX_PATTACH_POINT_FOLLOW, attachmentIndex ) + + attachmentIndex = fakePilotModel.LookupAttachment( "vent_right" ) + StartParticleEffectOnEntity( fakePilotModel, fxIndex, FX_PATTACH_POINT_FOLLOW, attachmentIndex ) +} + +/* +void function RodeoBatteryRemoval( entity pilot ) +{ + entity titan = GetTitanBeingRodeoed( pilot ) + if ( !IsValid( titan ) ) + return + + // THROW RODEO RIDER OFF + entity soul = titan.GetTitanSoul() + string titanType = GetSoulTitanSubClass( soul ) + + soul.SetLastRodeoHitTime( Time() ) + + RodeoBatteryPackRemovalDamage( pilot, titan, soul ) + + if ( !PlayerHasBattery( pilot ) ) + { + AddPlayerScore( pilot, "PilotBatteryStolen" ) + entity battery = Rodeo_CreateBatteryPack( titan ) + Rodeo_PilotPicksUpBattery( pilot, battery ) + thread BatteryThiefHighlight( pilot ) + + if ( titan.IsPlayer() ) + { + EmitSoundOnEntityOnlyToPlayer( titan, titan, TITAN_GOT_BATTERY_RIPPED_SOUND ) //Consider playing this in world once we get sounds that aren't just notification beeps + } + } + + vector direction = CalculateDirectionToThrowOffBatteryThief( pilot, titan ) + + ThrowRiderOff( pilot, titan, direction ) //This signals RodeoOver +} +*/ + +void function ClearParentOnDeathOrDestroy( entity clearParentEntity, entity onDeathOrDestroyEntity ) +{ + Assert( IsValid( clearParentEntity ) ) + Assert( IsAlive( clearParentEntity ) ) + + Assert( IsValid( onDeathOrDestroyEntity ) ) + Assert( IsAlive( onDeathOrDestroyEntity ) ) + + OnThreadEnd( + function() : ( clearParentEntity, onDeathOrDestroyEntity ) + { + if ( IsValid( clearParentEntity ) ) + { + clearParentEntity.ClearParent() + + if ( IsValid( onDeathOrDestroyEntity ) ) + { + PutEntityInSafeSpot( clearParentEntity, onDeathOrDestroyEntity, null, onDeathOrDestroyEntity.GetOrigin(), clearParentEntity.GetOrigin() ) + } + } + } + ) + + onDeathOrDestroyEntity.EndSignal( "OnDeath" ) + onDeathOrDestroyEntity.WaitSignal( "OnDestroy" ) +} + +void function PredatorMeleeKilledRagdoll( entity titan ) +{ + titan.e.forceRagdollDeath = true +} + +void function MeleePinkMistFakeBody( entity target ) +{ + target.Dissolve( ENTITY_DISSOLVE_PINKMIST, < 0, 0, 0 >, 0 ) +} + +void function TitanLostArm( entity titan ) +{ + table e = expect table( GetOptionalAnimEventVar( titan, "lost_arm" ) ) + + e.lostArm = true +} + +void function MeleeKilledRagdoll( entity titan ) +{ + entity attacker = expect entity( GetOptionalAnimEventVar( titan, "melee_killed_ragdoll" ) ) + + if ( !IsValid( attacker ) ) + return + titan.Die( attacker, attacker, { scriptType = DF_MELEE, damageSourceId = eDamageSourceId.titan_execution } ) + titan.SetContinueAnimatingAfterRagdoll( true ) + titan.BecomeRagdoll( < 0, 0, 0 >, false ) +} + +void function OnNPCTitanDeath( entity titan, var damageInfo ) //Debug function, for bug 129802 +{ + PrintFunc() +} + +void function OnNPCTitanSignalDeath( entity titan ) //Debug function, for bug 129802 +{ + PrintFunc() + + titan.WaitSignal( "OnDeath" ) + + printt( "titan : " + titan + " recieved OnDeath Signal in OnNPCTitanSignalDeath" ) +} + + +void function Northstar_Rocket_Pod_Left( entity guy ) +{ + entity victim = expect entity( GetOptionalAnimEventVar( guy, "rocket_pod_fire_left" ) ) + Rocket_Pod( guy, "muzzle_flash", victim ) +} + +void function Northstar_Rocket_Pod_Right( entity guy ) +{ + entity victim = expect entity( GetOptionalAnimEventVar( guy, "rocket_pod_fire_right" ) ) + Rocket_Pod( guy, "muzzle_flash2", victim ) +} + +void function Rocket_Pod( entity guy, string tag, entity victim ) +{ + entity oldOffhandWeapon = guy.GetOffhandWeapon( 0 ) + guy.TakeOffhandWeapon( 0 ) + guy.GiveOffhandWeapon( "mp_titanweapon_salvo_rockets", 0, [ "northstar_prime_execution" ] ) + + entity newOffhandWeapon = guy.GetOffhandWeapon( 0 ) + int attachID = guy.LookupAttachment( tag ) + vector angles = guy.GetAttachmentAngles( attachID ) + WeaponPrimaryAttackParams params + params.pos = guy.GetAttachmentOrigin( attachID ) + params.dir = AnglesToForward( angles ) + + if ( IsAlive( victim ) && victim.IsTitan() ) + { + vector victimTagPos = victim.GetAttachmentOrigin( victim.LookupAttachment( "CHESTFOCUS" ) ) + RandomVec( 30 ) + params.dir = Normalize( victimTagPos - params.pos ) + StartParticleEffectInWorld(GetParticleSystemIndex( $"P_muzzleflash_predator" ), params.pos, VectorToAngles( params.dir ) ) + } + + // DebugDrawSphere(params.pos, 10, 255,0,0, true, 1.0 ) + // DebugDrawLine( params.pos, params.pos + params.dir*200, 255,0,0, true, 1.0 ) + + thread OnWeaponPrimaryAttack_titanweapon_salvo_rockets( newOffhandWeapon, params ) + + guy.TakeOffhandWeapon( 0 ) + + if ( oldOffhandWeapon ) + guy.GiveOffhandWeapon( oldOffhandWeapon.GetWeaponClassName(), 0, oldOffhandWeapon.GetMods() ) +} \ No newline at end of file diff --git a/Northstar.Custom/mod/scripts/vscripts/rodeo/_rodeo_titan.gnut b/Northstar.Custom/mod/scripts/vscripts/rodeo/_rodeo_titan.gnut new file mode 100644 index 00000000..a2ff31ae --- /dev/null +++ b/Northstar.Custom/mod/scripts/vscripts/rodeo/_rodeo_titan.gnut @@ -0,0 +1,2491 @@ +untyped //Panel.s stuff needs to be typed + +global function RodeoTitan_Init + +global function EnableTitanRodeo +global function DisableTitanRodeo +global function DebugRodeoTimes +global function PlayerBeginsTitanRodeo +global function ForceTitanRodeoToEnd +global function PlayerRodeoViewCone +global function OpenViewCone +global function RodeoPanelIsOpen +global function PlayerRemovesBatteryPack +global function Rodeo_PilotAddsBatteryToFriendlyTitan +global function GiveFriendlyRodeoPlayerProtection +global function TakeAwayFriendlyRodeoPlayerProtection +global function Rodeo_GiveBatteryToPlayer +global function Rodeo_PilotThrowsBattery +global function Rodeo_RemoveBatteryOffPlayer +global function Rodeo_RemoveAllBatteriesOffPlayer +global function Rodeo_GiveExecutingTitanABattery +global function Rodeo_CreateBatteryPack +global function SetSoulBatteryCount +global function GetPlayerBatteryCount +global function PlayerHasMaxBatteryCount +global function Rodeo_PilotPicksUpBattery_Silent + +global function AddOnRodeoStartedCallback +global function AddOnRodeoEndedCallback + +global function PilotBattery_SetMaxCount +global function ThrowRiderOff + +global function Burnmeter_EmergencyBattery +global function Burnmeter_AmpedBattery + +global function Battery_StartFX +global function Battery_StopFX +global function Battery_StopFXAndHideIconForPlayer + +global function RemovePlayerAirControl //This function should really be in a server only SP & MP utility script file. No such file exists as of right now. +global function RestorePlayerAirControl //This function should really be in a server only SP & MP utility script file. No such file exists as of right now. + +#if DEV +global function SetDebugRodeoPrint +global function GetDebugRodeoPrint +#endif + +#if MP +global function SetApplyBatteryCallback +#endif + +const float BATTERY_PICKUP_IGNORE_FRAC = 0.98 +const RODEO_EXPLOSION_DAMAGEFRAC = 0.3 +global const RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS = $"models/props/titan_battery_static/titan_battery_static.mdl" //Need a separate one for rodeo anims, instead of manually rotating the existing one +const RODEO_BATTERY_RIP_PILOT_PUSHED_OFF_VERTICAL_HEIGHT = 80 +const RODEO_BATTERY_RIP_PILOT_PUSHED_OFF_HORIZONTAL_SPEED = 450 +const RODEO_THROW_BATTERY_BUTTON_HOLD_TIME = 0.5 +const RODEO_CLAMBER_FAILED_SOUND_DEBOUNCE_TIME = 2.0 +global const BATTERY_FX_FRIENDLY = $"P_xo_battery" +global const BATTERY_FX_AMPED = $"P_xo_battery_amped" + +const HAS_BATTERY_THIEF_ICON = false + +const string PILOT_PICKS_UP_BATTERY_SOUND = "UI_TitanBattery_Pilot_PickUp" +const string PILOT_APPLIES_BATTERY_TO_TITAN_HEALTH_RESTORED_SOUND = "UI_TitanBattery_Pilot_Give_TitanBattery" + + +global int RODEO_BATTERY_EXPLOSION_EFFECT + +const string TITAN_GOT_BATTERY_RIPPED_SOUND = "UI_TitanBattery_Pilot_Take_TitanBattery" + +global float ANTI_RODEO_DEFAULT_START_DELAY = 0.5 +global float ANTI_RODEO_DEFAULT_DRAIN_DURATION = 1.25 +global float ANTI_RODEO_DEFAULT_WINDOW_DURATION = 0.1 +global float ANTI_RODEO_DEFAULT_WINDOW_START = 0.55 + +struct AntiRodeoPlayerData +{ + bool antiRodeoPressed + float startTime + float windowStartFrac + float windowEndFrac + entity antiRodeoPlayer + bool wasCrouched +} + +struct +{ + array onRodeoEndedCallbacks + array onRodeoStartedCallbacks + + table antiRodeoPlayerData + + int maxPilotBatteryCount = 1 + bool debugRodeoPrint = false + + table playersThatWantToUseRodeoGrenade + + void functionref(entity,entity,entity) applyBatteryCallback +} file + +//----------------------------------------------------------------------------- +// _rodeo_titan.nut +// +// Script for a player (pilot) rodoeing a titan. +// +//----------------------------------------------------------------------------- + +void function RodeoTitan_Init() +{ + PrecacheParticleSystem( $"P_impact_rodeo_damage" ) //Rodeo hit spark + PrecacheParticleSystem( $"P_rodeo_damage_1" ) //DamageState1 + PrecacheParticleSystem( $"P_rodeo_damage_2" ) //DamageState2 + PrecacheParticleSystem( $"P_rodeo_damage_3" ) //DamageState3 + PrecacheParticleSystem( BATTERY_FX_FRIENDLY ) + PrecacheParticleSystem( BATTERY_FX_AMPED ) + + RegisterSignal( "CancelAirControlLoss" ) + RegisterSignal( "FriendlyRodeoDeployWeapon" ) + RegisterSignal( "MonitorRodeoPastPointOfNoReturn" ) + RegisterSignal( "PostRodeoAirControl" ) + PrecacheModel( RODEO_BATTERY_MODEL ) + PrecacheModel( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS ) + + AddSoulDeathCallback( SoulRodeoEnds ) + AddSoulTransferFunc( RecreateRodeoPanelDamageFX ) + + RODEO_BATTERY_EXPLOSION_EFFECT = PrecacheParticleSystem( $"P_impact_exp_FRAG_metal" ) + + AddCallback_OnPlayerKilled( Rodeo_DropAllBatteriesOnDeath ) + AddCallback_OnTouchHealthKit( "item_titan_battery", Rodeo_OnTouchBatteryPack ) + AddCallback_OnPilotBecomesTitan( Rodeo_ApplyAllBatteriesOnEmbark ) + + //AddSoulInitFunc( Rodeo_HealthDecayThink ) + + if ( IsMultiplayer() ) + { + //AddDeathCallback( "player", TitanDropsBatteryOnDeath ) //SP has its own functions. Maybe we should just copy SP's stuff? They have the green highlight FX for it too + //AddDeathCallback( "npc_titan", TitanDropsBatteryOnDeath ) //SP has its own functions. Maybe we should just copy SP's stuff? They have the green highlight FX for it too + + AddDamageCallback( "player", ShowRequestRodeoBatteryHint_OnDamage ) + AddCallback_OnPilotBecomesTitan( ShowRequestRodeoBatteryHint_OnPilotBecomesTitan ) + + AddClientCommandCallback( "OfferRodeoBattery", ClientCommand_OfferRodeoBattery ) + AddClientCommandCallback( "RequestRodeoBattery", ClientCommand_RequestRodeoBattery ) + + #if MP + AddClientCommandCallback( "TryNukeGrenade", ClientCommand_TryNukeGrenade ) + RegisterSignal( "TryNukeGrenade" ) + RegisterSignal( "RodeoNukeWindowEnded" ) + #endif + } + else + { + AddSoulInitFunc( DisableBTRodeo ) + } +} + +void function Rodeo_HealthDecayThink( entity soul ) //Remove if we don't want rodeo battery to drain health +{ + thread Rodeo_HealthDecayThinkInternal( soul ) +} + +void function Rodeo_HealthDecayThinkInternal( entity soul ) //Remove if we don't want rodeo battery to drain health +{ + soul.EndSignal( "OnDestroy" ) //This needs to be OnDestroy instead of OnDeath because souls don't have a death animation + soul.EndSignal( "OnTitanDeath" ) + + bool draining = false + + while ( 1 ) + { + entity titan = soul.GetTitan() + + if ( Rodeo_ShouldDrainHealth( soul ) ) + { + if ( !draining ) + { + draining = true + EmitSoundOnEntity( titan, "titan_energyshield_down" ) + } + + int damageAmout = Rodeo_GetDrainAmount( soul ) + + titan.TakeDamage( damageAmout, soul.e.lastRodeoAttacker, soul.e.lastRodeoAttacker, { scriptType = damageTypes.rodeoBatteryRemoval | DF_NO_INDICATOR, damageSourceId = eDamageSourceId.rodeo_battery_removal, hitbox = 2 } ) + } + else + { + if ( draining ) + { + draining = false + StopSoundOnEntity( titan, "titan_energyshield_down" ) + } + } + WaitFrame() + } +} + +bool function Rodeo_ShouldDrainHealth( entity soul ) //Remove if we don't want rodeo battery to drain health +{ + entity titan = soul.GetTitan() + if ( !IsAlive( titan ) ) + return false + + if ( GetDoomedState( titan ) ) + return false + + int batt = GetSoulBatteryCount( soul ) + int maxBattHealth = GetSegmentHealthForTitan( titan ) * batt + int health = titan.GetHealth() + return ( health > maxBattHealth ) +} + +int function Rodeo_GetDrainAmount( entity soul ) //Remove if we don't want rodeo battery to drain health +{ + entity titan = soul.GetTitan() + + float damagePerSec = GetSegmentHealthForTitan( titan ) / RODEO_DRAIN_TIME + float damagePerFrame = ceil( GetSegmentHealthForTitan( titan ) / RODEO_DRAIN_TIME ) * 0.1 + int damageAmout = int( damagePerFrame ) + + int batt = GetSoulBatteryCount( soul ) + int maxBattHealth = GetSegmentHealthForTitan( titan ) * batt + int health = titan.GetHealth() + if ( health - maxBattHealth < damageAmout ) + damageAmout = health - maxBattHealth + + return damageAmout +} + +void function GiveFriendlyRodeoPlayerProtection( entity titan ) +{ + entity friendlyRider = GetFriendlyRodeoPilot( titan ) + if ( IsValid( friendlyRider ) ) + { + //printt( "Set friendlyRider PassDamageToParent true" ) + friendlyRider.kv.PassDamageToParent = true //rodeo player now passes damage to titan + } +} + +void function TakeAwayFriendlyRodeoPlayerProtection( entity titan ) +{ + entity friendlyRider = GetFriendlyRodeoPilot( titan ) + if ( IsValid( friendlyRider ) ) + { + //printt( "Set friendlyRider PassDamageToParent false" ) + friendlyRider.kv.PassDamageToParent = false //rodeo player now takes full damage + } +} + +void function CreateSparksInsideTitanPanel( panel ) +{ + entity impactSpark = CreateEntity( "info_particle_system" ) + impactSpark.kv.start_active = 1 + impactSpark.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE + impactSpark.SetValueForEffectNameKey( $"P_impact_rodeo_damage" ) + SetTargetName( impactSpark, UniqueString() ) + impactSpark.SetParent( panel, "hatch", false, 0 ) + DispatchSpawn( impactSpark ) + impactSpark.Kill_Deprecated_UseDestroyInstead( 1.5 ) +} + + +void function CreateDamageStateParticlesForPanel( var panel, asset particleSystem = $"P_impact_rodeo_damage" ) +{ + entity impactSpark = CreateEntity( "info_particle_system" ) + impactSpark.kv.start_active = 1 + impactSpark.SetOwner( panel.GetParent() ) + impactSpark.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) // not visible to owner + impactSpark.SetValueForEffectNameKey( particleSystem ) + SetTargetName( impactSpark, UniqueString() ) + impactSpark.SetParent( panel, "hatch", false, 0 ) + DispatchSpawn( impactSpark ) + if ( IsValid( panel.s.lastDamageStateParticleSystem ) ) + { + //printt("Killing particle system: " + panel.s.lastDamageStateParticleSystem) + panel.s.lastDamageStateParticleSystem.Kill_Deprecated_UseDestroyInstead() + } + + panel.s.lastDamageStateParticleSystem = impactSpark +} + + +void function RecreateRodeoPanelDamageFX( entity soul, entity titan, entity oldTitan ) +{ + thread RecreateRodeoPanelDamageFX_threaded( soul ) +} + + +void function RecreateRodeoPanelDamageFX_threaded( entity soul ) +{ + WaitEndFrame() + entity panel = soul.soul.batteryContainer + + if (! IsValid( panel ) ) + return + + entity lastDamageStateParticleSystem = expect entity ( panel.s.lastDamageStateParticleSystem ) + + if ( IsValid( lastDamageStateParticleSystem ) ) + { + CreateDamageStateParticlesForPanel( panel, lastDamageStateParticleSystem.GetValueForEffectNameKey() ) //This kills the last particle system too + } +} + +void function RodeoPanelIsOpen( entity panel ) +{ + panel.s.opened = true + + entity titan = panel.GetParent() + Assert( titan.IsTitan() ) + + entity soul = titan.GetTitanSoul() + Assert( IsValid( soul ) ) + + soul.SetLastRodeoHitTime( Time() ) //Make warning always trigger now when panel is ripped + soul.soul.batteryContainerBeingUsed = false +} + +void function RodeoBatteryRemoval( entity pilot ) +{ + entity titan = GetTitanBeingRodeoed( pilot ) + if ( !IsValid( titan ) ) + return + + // THROW RODEO RIDER OFF + entity soul = titan.GetTitanSoul() + string titanType = GetSoulTitanSubClass( soul ) + + soul.SetLastRodeoHitTime( Time() ) + + RodeoBatteryPackRemovalDamage( pilot, titan, soul ) + + bool playerHadBattery = PlayerHasBattery( pilot ) + + if ( !playerHadBattery ) + { + AddPlayerScore( pilot, "PilotBatteryStolen" ) + entity battery = Rodeo_CreateBatteryPack( titan ) + Rodeo_PilotPicksUpBattery( pilot, battery ) + thread BatteryThiefHighlight( pilot ) + + if ( titan.IsPlayer() ) + { + EmitSoundOnEntityOnlyToPlayer( titan, titan, TITAN_GOT_BATTERY_RIPPED_SOUND ) //Consider playing this in world once we get sounds that aren't just notification beeps + } + } + + vector direction = CalculateDirectionToThrowOffBatteryThief( pilot, titan ) + + // for classic rodeo, let them stay on + if ( GetCurrentPlaylistVarInt( "classic_rodeo", 0 ) == 0 ) + ThrowRiderOff( pilot, titan, direction ) //This signals RodeoOver + + #if MP + PIN_PlayerRodeoedEnemyTitanToCompletion( pilot, titan, playerHadBattery ) + #endif +} + +void function RodeoBatteryGrenadeShow( entity pilot ) +{ + entity titanSoul = pilot.GetTitanSoulBeingRodeoed() + Assert( IsValid( titanSoul ) ) + + foreach( tempProp in pilot.p.rodeoAnimTempProps ) + { + tempProp.Show() + } +} + +void function RodeoBatteryRemoval_ShowBattery( entity pilot ) +{ + foreach( tempProp in pilot.p.rodeoAnimTempProps ) + { + tempProp.Show() + } + + entity titanSoul = pilot.GetTitanSoulBeingRodeoed() + + string titanType = GetSoulTitanSubClass( titanSoul ) + + entity batteryContainer = titanSoul.soul.batteryContainer + batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_down_idle" ) ) + +} + + +void function RodeoBatteryStealthMovementWarning( entity pilot ) +{ + if ( !PlayerHasPassive( pilot, ePassives.PAS_STEALTH_MOVEMENT ) ) + return + + entity titanSoul = pilot.GetTitanSoulBeingRodeoed() + + titanSoul.SetLastRodeoHitTime( Time() ) //This shows the warning icon on the Titan's hud +} + +vector function CalculateDirectionToThrowOffBatteryThief( entity batteryThief, entity titan ) +{ + vector backward + vector right + + if ( titan.IsPlayer() ) + { + backward = titan.GetViewForward() * -1.0 + right = titan.GetViewRight() + } + else + { + backward = titan.GetForwardVector() * -1.0 + right = titan.GetRightVector() + } + + backward.z = 0 + right.z = 0 + + backward = Normalize( backward ) + right = Normalize( right ) + + // map the player's controls to his angles, and add that velocity + float xAxis = batteryThief.GetInputAxisRight() + float yAxis = batteryThief.GetInputAxisForward() + + xAxis = GraphCapped( xAxis, -1.0, 1.0, -0.4, 0.4 ) + yAxis = GraphCapped( yAxis, -1.0, 1.0, 1.0, 0.75 ) //Cap it so you don't actually let the players jump forwards + + vector direction + if ( fabs( xAxis ) < 0.2 && fabs( yAxis ) < 0.2 ) + { + // no significant controller deflection, just push forward by 0.75 as default + direction = backward * 0.75 + } + else + { + vector forwardVec = backward * yAxis + vector rightVec = right * xAxis + direction = rightVec + forwardVec + } + + direction *= RODEO_BATTERY_RIP_PILOT_PUSHED_OFF_HORIZONTAL_SPEED + direction.z = RODEO_BATTERY_RIP_PILOT_PUSHED_OFF_VERTICAL_HEIGHT + + // JFS: R2DLC-310 SCRIPT ERROR: PHONE_HOME: [SERVER] vecAbsVelocity isn't valid + if ( LengthSqr( direction ) < 0.0 ) + return <0, 0, RODEO_BATTERY_RIP_PILOT_PUSHED_OFF_VERTICAL_HEIGHT> + + return direction +} + +void function CancelAirControlLossAfterTouchGround( entity player ) +{ + player.Signal( "CancelAirControlLoss" ) +} + + +void function BatteryThiefHighlight( entity player ) +{ + Highlight_SetEnemyHighlight( player, "battery_thief" ) + + OnThreadEnd( + function() : ( player ) + { + if ( !IsValid( player ) ) + return + + if ( Hightlight_HasEnemyHighlight( player, "battery_thief" ) ) + Highlight_ClearEnemyHighlight( player ) + } + ) + + wait RODEO_BATTERY_THIEF_ICON_DURATION +} + +void function ForceTitanRodeoToEnd( entity titan ) //TODO: Not typed since it is added via anim event +{ + entity soul = titan.GetTitanSoul() + if ( !IsValid( soul ) ) + return + + SoulRodeoEnds( soul, null ) +} + + +void function SoulRodeoEnds( entity soul, var damageInfo ) +{ + entity titan = soul.GetTitan() + + if( !IsValid( titan ) ) + return + + entity rider = GetRodeoPilot( titan ) + + if ( !IsValid( rider ) ) + return + + rider.Signal( "RodeoOver" ) + rider.ClearParent() +} + + +void function EnableTitanRodeo( entity titan ) +{ + Assert( titan.IsTitan(), "tried calling EnableTitanRodeo on non-titan" ) + + entity titanSoul = titan.GetTitanSoul() + + Assert( IsValid( titanSoul ) ) + + titanSoul.SetIsValidRodeoTarget( true ) //Lets rodeo happen on them. +} + + +void function DisableTitanRodeo( entity titan ) +{ + Assert( titan.IsTitan(), "tried calling DisableTitanRodeo( on non-titan" ) + + entity titanSoul = titan.GetTitanSoul() + + Assert( IsValid( titanSoul ) ) + + titanSoul.SetIsValidRodeoTarget( false ) //Stops rodeo from happening on them. +} + +void function AddOnRodeoStartedCallback( void functionref(entity,entity) callbackFunc ) +{ + Assert (!( file.onRodeoStartedCallbacks.contains( callbackFunc ) )) + file.onRodeoStartedCallbacks.append( callbackFunc ) +} + +void function AddOnRodeoEndedCallback( void functionref(entity,entity) callbackFunc ) +{ + Assert (!( file.onRodeoEndedCallbacks.contains( callbackFunc ) )) + file.onRodeoEndedCallbacks.append( callbackFunc ) +} + +function PlayerBeginsTitanRodeo( entity player, RodeoPackageStruct rodeoPackage, entity rodeoTitan ) +{ + entity soul = rodeoTitan.GetTitanSoul() + Assert( IsValid( soul ) ) + + bool sameTeam = player.GetTeam() == rodeoTitan.GetTeam() + bool playerWasEjecting = player.p.pilotEjecting // have to store this off here because the "RodeoStarted" signal below ends eject, so it will be too late to check it in actual rodeo function. Used to check for eject -> rodeo + + player.p.rodeoShouldAdjustJumpOffVelocity = true + + player.Signal( "RodeoStarted" ) + + bool playerHadBatteryAtStartOfRodeo = PlayerHasBattery( player ) + + OnThreadEnd( + function () : ( player, soul, sameTeam, rodeoTitan, playerHadBatteryAtStartOfRodeo ) + { + RodeoPackageStruct rodeoPackage = player.p.rodeoPackage + entity newRodeoTitan = rodeoTitan + + //Clear the rodeo alert and update the newRodeoTitan to be the soul's titan + if ( IsValid( soul ) ) + { + soul.SetLastRodeoHitTime( 0 ) //Clear rodeo warning for next time a player jumps on + newRodeoTitan = soul.GetTitan() //rodeoTitan might have changed because a player embarked/disembarked etc + + foreach ( callbackFunc in file.onRodeoEndedCallbacks ) + { + callbackFunc( player, newRodeoTitan ) + } + + for( int i = 0; i < soul.rodeoReservedSlots.len(); ++i ) + { + if ( soul.rodeoReservedSlots[ i ] == player ) + { + soul.rodeoReservedSlots[ i ] = null + break + } + } + + if ( soul.soul.batteryContainerBeingUsed && playerHadBatteryAtStartOfRodeo ) //i.e. rodeo got interruped early + { + string titanType = GetSoulTitanSubClass( soul ) + entity batteryContainer = soul.soul.batteryContainer + batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_up" ) ) + soul.soul.batteryContainerBeingUsed = false + + } + + // if the player is invalid, we still need to enable rodeo on the titan + // normally this would happen in Rodeo_Detach(), but that only works if the player is valid + if ( !IsValid( player ) ) + EnableTitanRodeo( newRodeoTitan ) + } + + if ( IsValid( player ) ) + { + player.Signal( "RodeoOver" ) + player.SetNameVisibleToFriendly( true ) // show name of the pilot again + player.SetNameVisibleToEnemy( true ) + ClearPlayerAnimViewEntity( player ) + player.AnimViewEntity_SetLerpOutTime( 0.4 ) // blend out the clear anim view entity + player.ClearParent() + player.Anim_Stop() + player.SetOneHandedWeaponUsageOff() + player.SetTitanSoulBeingRodeoed( null ) + player.UnforceStand() + player.kv.PassDamageToParent = false + player.TouchGround() // so you can double jump off + StopSoundOnEntity( player, rodeoPackage.cockpitSound ) + StopSoundOnEntity( player, rodeoPackage.worldSound ) + if ( Rodeo_IsAttached( player ) ) + Rodeo_Detach( player ) + + if ( IsAlive( player ) ) + { + int attachIndex = newRodeoTitan.LookupAttachment( rodeoPackage.attachPoint ) + vector startPos = newRodeoTitan.GetAttachmentOrigin( attachIndex ) + + if ( !PlayerCanTeleportHere( player, startPos, newRodeoTitan ) ) + { + startPos = newRodeoTitan.GetOrigin() + if ( !PlayerCanTeleportHere( player, startPos, newRodeoTitan ) ) + startPos = player.GetOrigin() + } + + thread PlayerJumpsOffRodeoTarget( player, newRodeoTitan, startPos ) + } + + #if MP + player.Signal( "RodeoNukeWindowEnded" ) + if ( player in file.playersThatWantToUseRodeoGrenade ) + delete file.playersThatWantToUseRodeoGrenade[ player ] + #endif + } + } + ) + + + soul.EndSignal( "OnTitanDeath" ) + soul.EndSignal( "OnDestroy" ) + player.EndSignal( "OnDeath" ) + player.EndSignal( "RodeoOver" ) + + string rodeoTitanType = rodeoPackage.rodeoTargetType + + #if MP + thread OpenRodeoNukeWindow( player, rodeoTitan ) + #endif + + thread WatchForPlayerJumpingOffRodeo( player ) + + // hide name of the pilot while he is rodeoing + player.SetNameVisibleToFriendly( false ) + player.SetNameVisibleToEnemy( false ) + player.ForceStand() + thread ManagePlayerWeaponDeployment( player, soul ) //Spin this off in its own thread since there are multiple ways for weapon to be deployed + player.SetOneHandedWeaponUsageOn() + player.TouchGround() // so you can double jump off + player.SetTitanSoulBeingRodeoed( soul ) + + if ( soul.GetShieldHealth() > 0.0 ) // This was not evaluating properly with 0 being an int, so make it a float which works + GiveFriendlyRodeoPlayerProtection( rodeoTitan ) + + foreach ( callbackFunc in file.onRodeoStartedCallbacks ) + callbackFunc( player, rodeoTitan ) + + if ( player.GetTeam() != rodeoTitan.GetTeam() && !PlayerHasPassive( player, ePassives.PAS_STEALTH_MOVEMENT ) ) + soul.SetLastRodeoHitTime( Time() ) // Alert Titan immediately if you don't have passive + + soul.soul.batteryMovedDown = false + if ( ShouldThrowGrenadeInHatch( player ) ) //Either player is going to apply a battery, or it's going to throw a grenade. In either case, we want the battery to move + { + Rodeo_MoveBatteryDown( soul ) + } + + soul.soul.batteryContainerBeingUsed = true //All rodeo points mark batteryContainer as being true, various exit points mark it as being false when they are done cleaning it up (e.g. playing the appropriate battery going up/down anims) + + if ( !sameTeam ) + { + #if FACTION_DIALOGUE_ENABLED + thread PlayRodeoFactionDialogueAfterDelay( player, 0.5 ) + #endif + TitanVO_AlertTitansTargetingThisTitanOfRodeo( player, soul ) + } + + waitthread PlayerClimbsIntoRodeoPosition( player, rodeoTitan, rodeoPackage, playerWasEjecting ) + + #if MP + player.Signal( "RodeoNukeWindowEnded" ) + #endif + + // There has been a wait, verify things are still valid. + + if ( !IsValid( soul ) ) + return + + entity rodeoTitan = soul.GetTitan() + + if ( !IsAlive( rodeoTitan ) ) + return + + TryBatteryStyleRodeo( player, rodeoTitan, soul, rodeoPackage ) +} + +#if FACTION_DIALOGUE_ENABLED +void function PlayRodeoFactionDialogueAfterDelay( entity player, float delay = 0.5 ) +{ + player.EndSignal( "OnDeath" ) + player.EndSignal( "RodeoOver" ) + + wait delay + PlayFactionDialogueToPlayer( "kc_rodeo", player ) +} +#endif + +void function Rodeo_MoveBatteryDown( entity soul ) +{ + if ( soul.soul.batteryMovedDown ) + return + + string titanType = GetSoulTitanSubClass( soul ) + entity batteryContainer = soul.soul.batteryContainer + batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_down" ) ) + soul.soul.batteryMovedDown = true +} + +void function ManagePlayerWeaponDeployment( entity player, entity titanSoul ) +{ + HolsterAndDisableWeapons( player ) + + titanSoul.EndSignal( "OnTitanDeath" ) + titanSoul.EndSignal( "OnDestroy" ) + player.EndSignal( "OnDeath" ) + player.EndSignal( "RodeoOver" ) + player.EndSignal( "FriendlyRodeoDeployWeapon" ) + + OnThreadEnd( + function() : ( player ) + { + DeployAndEnableWeapons( player ) + } + ) + + WaitForever() +} + + + + +vector function GetAntiRodeoThrowOffDirection( entity rodeoRider, entity titan ) +{ + vector backward + vector right + + if ( titan.IsPlayer() ) + { + backward = titan.GetViewForward() * -1.0 + right = titan.GetViewRight() + } + else + { + backward = titan.GetForwardVector() * -1.0 + right = titan.GetRightVector() + } + + backward.z = 0 + right.z = 0 + + // map the player's controls to his angles, and add that velocity + float xAxis = rodeoRider.GetInputAxisRight() + float yAxis = rodeoRider.GetInputAxisForward() + + xAxis = GraphCapped( xAxis, -1.0, 1.0, -0.4, 0.4 ) + yAxis = GraphCapped( yAxis, -1.0, 1.0, 1.0, 0.75 ) //Cap it so you don't actually let the players jump forwards + + vector direction + if ( fabs( xAxis ) < 0.2 && fabs( yAxis ) < 0.2 ) + { + // no significant controller deflection, just push forward by 0.75 as default + direction = backward * 0.75 + } + else + { + vector forwardVec = backward * yAxis + vector rightVec = right * xAxis + direction = rightVec + forwardVec + } + + direction *= 600 + direction.z = 25 + + return direction +} + + +void function RodeoPilotPullsOutWeapon( entity rodeoPilot, entity rodeoTitan, string rodeoTitanType ) +{ + PlayerRodeoViewCone( rodeoPilot, rodeoTitanType ) + + Rodeo_OnFinishClimbOnAnimation( rodeoPilot ) // This is to let code know the rodeoPilot has finished climbing on the rodeo and ready to fire + rodeoPilot.Signal( "FriendlyRodeoDeployWeapon" ) +} + +void function TryBatteryStyleRodeo( entity rodeoPilot, entity rodeoTitan, entity titanSoul, RodeoPackageStruct rodeoPackage ) +{ + titanSoul.EndSignal( "OnTitanDeath" ) + + string rodeoTitanType = rodeoPackage.rodeoTargetType + if ( rodeoPilot.GetTeam() == rodeoTitan.GetTeam() ) + { + if ( PilotCanApplyBattery( rodeoPilot, rodeoTitan ) ) + waitthread PlayerAppliesBatteryPack( rodeoPilot, rodeoTitan, titanSoul, rodeoPackage ) + + rodeoTitan = titanSoul.GetTitan() + Assert( IsAlive( rodeoTitan ) ) + + //printt( "After applying battery" ) + + //This is default R1 style rodeo, with the panel ripped and ready to be shot at + FirstPersonSequenceStruct sequence + sequence.attachment = "hijack" + sequence.thirdPersonAnimIdle = GetAnimFromAlias( rodeoTitanType, "pt_rodeo_back_right_idle" ) + sequence.firstPersonAnimIdle = GetAnimFromAlias( rodeoTitanType, "ptpov_rodeo_back_right_idle" ) + sequence.useAnimatedRefAttachment = true + thread FirstPersonSequence( sequence, rodeoPilot, rodeoTitan ) + + RodeoPilotPullsOutWeapon( rodeoPilot, rodeoTitan, rodeoTitanType ) + WaitForever() + + } + + #if MP + if ( PlayerWantsToThrowNukeGrenade( rodeoPilot ) ) + { + waitthread PlayerAppliesBatteryPack( rodeoPilot, rodeoTitan, titanSoul, rodeoPackage ) + return + } + #endif + + bool classicRodeo = GetCurrentPlaylistVarInt( "classic_rodeo", 0 ) == 1 + + if ( ShouldThrowGrenadeInHatch( rodeoPilot ) ) + { + if ( !classicRodeo ) + waitthread PlayerThrowsGrenadeInHatch( rodeoPilot, rodeoTitan, titanSoul, rodeoPackage ) //This ends rodeo at the end of the sequence + } + else + { + waitthread PlayerRemovesBatteryPack( rodeoPilot, rodeoTitan, titanSoul, rodeoPackage ) //This ends rodeo at the end of the sequence + } + + if ( classicRodeo ) + { + RodeoBatteryRemoval_ShowBattery( rodeoPilot ) // hide battery, will be shown at the end of a battery removal rodeo + + //This is default R1 style rodeo, with the panel ripped and ready to be shot at + FirstPersonSequenceStruct sequence + sequence.attachment = "hijack" + sequence.thirdPersonAnimIdle = GetAnimFromAlias( rodeoTitanType, "pt_rodeo_back_right_idle" ) + sequence.firstPersonAnimIdle = GetAnimFromAlias( rodeoTitanType, "ptpov_rodeo_back_right_idle" ) + sequence.useAnimatedRefAttachment = true + thread FirstPersonSequence( sequence, rodeoPilot, rodeoTitan ) + + RodeoPilotPullsOutWeapon( rodeoPilot, rodeoTitan, rodeoTitanType ) + entity weakpointHitbox = CreateClassicRodeoWeakpoint( rodeoPilot, rodeoTitan ) + + OnThreadEnd( function() : ( weakpointHitbox ) + { + if ( IsValid( weakpointHitbox ) ) + weakpointHitbox.Destroy() + }) + + while ( true ) + { + // the point of no return stuff breaks jumping if you pulled a battery, so do this manually, sucks but whatever + if ( rodeoPilot.IsInputCommandHeld( IN_JUMP ) ) + ThrowRiderOff( rodeoPilot, rodeoTitan, CalculateDirectionToThrowOffBatteryThief( rodeoPilot, rodeoTitan ) ) //This signals RodeoOver + + WaitFrame() + } + } +} + +struct RodeoRiderSequenceStruct +{ + bool wasCloaked = false + float cloakEndTime = 0.0 + string interiorSound = "" + string exteriorSound = "" +} + +void function DisableCloakBeforeRodeoSequence( entity rodeoPilot, RodeoRiderSequenceStruct dataStruct ) +{ + if ( !IsCloaked( rodeoPilot ) ) + return + + dataStruct.wasCloaked = true + dataStruct.cloakEndTime = rodeoPilot.GetCloakEndTime() + DisableCloak( rodeoPilot, 0.0 ) + +} + +void function RestoreCloakAfterRodeoSequence( entity rodeoPilot, RodeoRiderSequenceStruct dataStruct ) +{ + if ( !IsAlive( rodeoPilot ) ) + return + + if ( !dataStruct.wasCloaked ) + return + + Assert( dataStruct.cloakEndTime > 0.0 ) + + float remainingCloakDuration = max( 0.0, dataStruct.cloakEndTime - Time() ) + if ( remainingCloakDuration > CLOAK_FADE_IN ) //Has to be greater than 1.0 fade in duration, otherwise will cloak forever + EnableCloak( rodeoPilot, remainingCloakDuration, CLOAK_FADE_IN ) +} + +void function PlayerRemovesBatteryPack( entity rodeoPilot, entity rodeoTitan, entity titanSoul, RodeoPackageStruct rodeoPackage ) +{ + string titanType = GetSoulTitanSubClass( titanSoul ) + + RodeoRiderSequenceStruct dataStruct + dataStruct.interiorSound = GetAudioFromAlias( titanType, "rodeo_battery_steal_1p" ) + dataStruct.exteriorSound = GetAudioFromAlias( titanType, "rodeo_battery_steal_3p" ) + DisableCloakBeforeRodeoSequence( rodeoPilot, dataStruct ) + + entity tempBattery3p + tempBattery3p = CreatePropDynamic( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS ) + tempBattery3p.SetParent( rodeoPilot, "R_HAND", false, 0.0 ) + tempBattery3p.RemoveFromSpatialPartition() + tempBattery3p.Hide() + + entity pilotFirstPersonProxy = rodeoPilot.GetFirstPersonProxy() + entity tempBattery1p = CreatePropDynamic( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS ) + tempBattery1p.SetParent( pilotFirstPersonProxy, "R_HAND", false, 0.0 ) + tempBattery1p.RemoveFromSpatialPartition() + tempBattery1p.Hide() + + rodeoPilot.p.rodeoAnimTempProps.append( tempBattery1p ) + rodeoPilot.p.rodeoAnimTempProps.append( tempBattery3p ) + + AddAnimEvent( rodeoPilot, "rodeo_battery_show", RodeoBatteryRemoval_ShowBattery ) //Consider adding this in add player + AddAnimEvent( rodeoPilot, "rodeo_battery_rip", RodeoBatteryRemoval ) + AddAnimEvent( rodeoPilot, "rodeo_battery_stealth_movement_warning", RodeoBatteryStealthMovementWarning ) + thread MonitorRodeoPastPointOfNoReturn( rodeoPilot, titanSoul ) + + OnThreadEnd( + function() : ( rodeoPilot, titanSoul, titanType, dataStruct ) + { + if ( IsValid( titanSoul ) ) + { + entity rodeoPanel = titanSoul.soul.batteryContainer + if ( IsValid( rodeoPanel ) ) + { + titanSoul.soul.batteryContainerBeingUsed = false + + if ( titanSoul.soul.batteryContainerPastPointOfNoReturn ) + { + rodeoPanel.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_up" ) ) + + } + else + { + rodeoPanel.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_up_idle" ) ) + rodeoPanel.Anim_DisableSequenceTransition() //Snap into place instead of blending + } + + titanSoul.soul.batteryContainerPastPointOfNoReturn = false + } + } + + + if ( !IsValid( rodeoPilot ) ) + return + + if ( HasAnimEvent( rodeoPilot, "rodeo_battery_rip" ) ) + DeleteAnimEvent( rodeoPilot, "rodeo_battery_rip" ) + + if ( HasAnimEvent( rodeoPilot, "rodeo_battery_show" ) ) + DeleteAnimEvent( rodeoPilot, "rodeo_battery_show" ) + + if ( HasAnimEvent( rodeoPilot, "rodeo_battery_stealth_movement_warning" ) ) + DeleteAnimEvent( rodeoPilot, "rodeo_battery_stealth_movement_warning" ) + + ClearRodeoAnimTempProps( rodeoPilot ) + + StopSoundOnEntity( rodeoPilot, dataStruct.interiorSound ) + StopSoundOnEntity( rodeoPilot, dataStruct.exteriorSound ) + + RestoreCloakAfterRodeoSequence( rodeoPilot, dataStruct ) + } + ) + + FirstPersonSequenceStruct sequence + sequence.attachment = "hijack" + string batteryRipAnim = GetAnimFromAlias( titanType, "pt_rodeo_back_right_hijack_battery" ) // default, old style + //printt( "Battery Rip Anim: " + batteryRipAnim ) + sequence.thirdPersonAnim = batteryRipAnim + sequence.firstPersonAnim = GetAnimFromAlias( titanType, "ptpov_rodeo_back_right_hijack_battery" ) + + if ( GetBugReproNum() == 112023 ) + rodeoTitan.SnapEyeAngles( < 89, 100.02, 0 > ) + + EmitDifferentSoundsOnEntityForPlayerAndWorld( dataStruct.interiorSound, dataStruct.exteriorSound, rodeoPilot, rodeoTitan ) //Play sound on rodeoPilot instead of panel + FirstPersonSequence( sequence, rodeoPilot, rodeoTitan ) +} + +void function MonitorRodeoPastPointOfNoReturn( entity rodeoPilot, entity titanSoul ) +{ + titanSoul.Signal( "MonitorRodeoPastPointOfNoReturn" ) + titanSoul.EndSignal( "MonitorRodeoPastPointOfNoReturn" ) + rodeoPilot.EndSignal( "RodeoOver" ) + + titanSoul.soul.batteryContainerPastPointOfNoReturn = false + + rodeoPilot.WaitSignal( "RodeoPointOfNoReturn" ) + titanSoul.soul.batteryContainerPastPointOfNoReturn = true +} + +void function PlayerThrowsGrenadeInHatch( entity rodeoPilot, entity rodeoTitan, entity titanSoul, RodeoPackageStruct rodeoPackage ) +{ + AddAnimEvent( rodeoPilot, "rodeo_battery_grenade_damage", RodeoBatteryRemoval ) + AddAnimEvent( rodeoPilot, "rodeo_battery_grenade_show", RodeoBatteryGrenadeShow ) + AddAnimEvent( rodeoPilot, "rodeo_battery_stealth_movement_warning", RodeoBatteryStealthMovementWarning ) + + entity grenade3p = CreatePropDynamic( GRENADE_MODEL ) + grenade3p.SetParent( rodeoPilot, "PROPGUN", false, 0.0 ) + grenade3p.RemoveFromSpatialPartition() + grenade3p.Hide() + + entity grenade1p = CreatePropDynamic( GRENADE_MODEL ) + grenade1p.SetParent( rodeoPilot.GetFirstPersonProxy(), "PROPGUN", false, 0.0 ) + grenade1p.RemoveFromSpatialPartition() + grenade1p.Hide() + + rodeoPilot.p.rodeoAnimTempProps.append( grenade3p ) + rodeoPilot.p.rodeoAnimTempProps.append( grenade1p ) + + string titanType = GetSoulTitanSubClass( titanSoul ) + RodeoRiderSequenceStruct dataStruct + dataStruct.interiorSound = GetAudioFromAlias( titanType, "rodeo_grenade_1p" ) + dataStruct.exteriorSound = GetAudioFromAlias( titanType, "rodeo_grenade_3p" ) + DisableCloakBeforeRodeoSequence( rodeoPilot, dataStruct ) + + OnThreadEnd( + function() : ( rodeoPilot, titanSoul, titanType, dataStruct ) + { + if ( IsValid( titanSoul ) ) + { + titanSoul.soul.batteryContainerBeingUsed = false + titanSoul.soul.batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_up" ) ) + } + + if ( !IsValid( rodeoPilot ) ) + return + + if ( HasAnimEvent( rodeoPilot, "rodeo_battery_grenade_damage" ) ) + DeleteAnimEvent( rodeoPilot, "rodeo_battery_grenade_damage" ) + + if ( HasAnimEvent( rodeoPilot, "rodeo_battery_grenade_show" ) ) + DeleteAnimEvent( rodeoPilot, "rodeo_battery_grenade_show" ) + + if ( HasAnimEvent( rodeoPilot, "rodeo_battery_stealth_movement_warning" ) ) + DeleteAnimEvent( rodeoPilot, "rodeo_battery_stealth_movement_warning" ) + + ClearRodeoAnimTempProps( rodeoPilot ) + + StopSoundOnEntity( rodeoPilot, dataStruct.interiorSound ) + StopSoundOnEntity( rodeoPilot, dataStruct.exteriorSound ) + + RestoreCloakAfterRodeoSequence( rodeoPilot, dataStruct ) + } + ) + + FirstPersonSequenceStruct sequence + sequence.attachment = "hijack" + //string batteryRipAnim = GetAnimFromAlias( titanSubClass, "pt_rodeo_back_right_hijack_battery" ) // Do this once the animations aren't named the same/enabled for different titans + //printt( "Battery Rip Anim: " + batteryRipAnim ) + sequence.thirdPersonAnim = GetAnimFromAlias( titanType, "pt_rodeo_grenade" ) + sequence.firstPersonAnim = GetAnimFromAlias( titanType, "ptpov_rodeo_grenade" ) + + EmitDifferentSoundsOnEntityForPlayerAndWorld( dataStruct.interiorSound, dataStruct.exteriorSound, rodeoPilot, rodeoTitan ) //Play sound on rodeoPilot instead of panel + + waitthread FirstPersonSequence( sequence, rodeoPilot, rodeoTitan ) +} + +void function PlayerAppliesBatteryPack_DelayedClearSyncedEntity( entity rodeoPilot, entity titanSoul ) +{ + if ( !IsValid( rodeoPilot ) ) + { + return + } + + if ( !IsValid( titanSoul ) ) + { + return + } + + if ( titanSoul.soul.batteryContainerBeingUsed ) + { + return + } + + rodeoPilot.SetSyncedEntity( null ) +} + +void function PlayerAppliesBatteryPack( entity rodeoPilot, entity rodeoTitan, entity titanSoul, RodeoPackageStruct rodeoPackage ) +{ + + entity battery + + #if MP + bool nukeVersion = false + if ( PlayerWantsToThrowNukeGrenade( rodeoPilot ) ) + { + nukeVersion = true + // battery = CreatePropDynamic( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS ) + } + else + { + + battery = GetBatteryOnBack( rodeoPilot ) + battery.Hide() //Hide it because the animation has a battery model already + } + #else + battery = GetBatteryOnBack( rodeoPilot ) + battery.Hide() //Hide it because the animation has a battery model already + #endif + + entity rodeoPanel = titanSoul.soul.batteryContainer + + entity tempBattery3p + tempBattery3p = CreatePropDynamic( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS ) + tempBattery3p.SetParent( rodeoPilot, "R_HAND", false, 0.0 ) + tempBattery3p.RemoveFromSpatialPartition() + + entity tempBattery1p + tempBattery1p = CreatePropDynamic( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS ) + tempBattery1p.SetParent( rodeoPilot.GetFirstPersonProxy(), "R_HAND", false, 0.0 ) + tempBattery1p.RemoveFromSpatialPartition() + + #if MP + if ( nukeVersion ) + tempBattery1p.SetSkin( 1 ) + #endif + if ( IsAmpedBattery( battery ) ) + { + tempBattery1p.SetSkin( 2 ) + tempBattery3p.SetSkin( 2 ) + } + + rodeoPilot.p.rodeoAnimTempProps.append( tempBattery3p ) + rodeoPilot.p.rodeoAnimTempProps.append( tempBattery1p ) + + string soundAlias = "rodeo_battery_return" + string animAlias = "rodeo_back_right_apply_battery" + + #if MP + if ( nukeVersion ) + { + soundAlias = "nuke_rodeo_battery_return" + animAlias = "nuke_rodeo_back_right_apply_battery" + } + #endif + + string titanType = GetSoulTitanSubClass( titanSoul ) + RodeoRiderSequenceStruct dataStruct + dataStruct.interiorSound = GetAudioFromAlias( titanType, soundAlias + "_1p" ) + dataStruct.exteriorSound = GetAudioFromAlias( titanType, soundAlias + "_3p" ) + DisableCloakBeforeRodeoSequence( rodeoPilot, dataStruct ) + + OnThreadEnd( + function() : ( battery, titanSoul, titanType, rodeoPilot, dataStruct ) + { + if ( IsValid( battery ) ) + battery.Show() + + entity batteryContainer = titanSoul.soul.batteryContainer + if ( IsValid( batteryContainer ) ) + { + if ( IsValid( battery ) ) + { + batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_up" ) ) + } + else + { + batteryContainer.Anim_Play( GetAnimFromAlias( titanType, "hatch_rodeo_up_idle" ) ) + batteryContainer.Anim_DisableSequenceTransition() + + } + + if ( IsValid( rodeoPilot ) && IsValid( titanSoul ) ) + { + delaythread( 0.1 ) PlayerAppliesBatteryPack_DelayedClearSyncedEntity( rodeoPilot, titanSoul ) + } + + titanSoul.soul.batteryContainerBeingUsed = false + } + + + if ( !IsValid( rodeoPilot ) ) + return + + ClearRodeoAnimTempProps( rodeoPilot ) + + StopSoundOnEntity( rodeoPilot, dataStruct.interiorSound ) + StopSoundOnEntity( rodeoPilot, dataStruct.exteriorSound ) + + RestoreCloakAfterRodeoSequence( rodeoPilot, dataStruct ) + } + ) + + FirstPersonSequenceStruct sequence + sequence.attachment = "hijack" + string batteryApplicationAnim = GetAnimFromAlias( titanType, "pt_" + animAlias ) // default, old style + //printt( "Battery Application Anim: " + batteryApplicationAnim ) + sequence.thirdPersonAnim = batteryApplicationAnim + sequence.firstPersonAnim = GetAnimFromAlias( titanType, "ptpov_" + animAlias ) + + EmitDifferentSoundsOnEntityForPlayerAndWorld( dataStruct.interiorSound, dataStruct.exteriorSound, rodeoPilot, rodeoTitan ) //Play sound on rodeoPilot instead of panel + + entity batteryContainer = titanSoul.soul.batteryContainer + if ( batteryContainer ) + { + rodeoPilot.SetSyncedEntity( batteryContainer ) + } + + waitthread FirstPersonSequence( sequence, rodeoPilot, rodeoTitan ) + + //Time passed, need to update titan reference + rodeoTitan = titanSoul.GetTitan() + Assert( IsAlive( rodeoTitan ) ) + + + #if MP + if ( nukeVersion ) + thread RodeoForceNuke( rodeoPilot ) + else + Rodeo_PilotAddsBatteryToFriendlyTitan( rodeoPilot, rodeoTitan ) + #else + Rodeo_PilotAddsBatteryToFriendlyTitan( rodeoPilot, rodeoTitan ) + #endif + +} + +void function ClearRodeoAnimTempProps( entity player ) +{ + foreach( tempProp in player.p.rodeoAnimTempProps ) + { + if ( IsValid( tempProp ) ) + tempProp.Destroy() + } + + player.p.rodeoAnimTempProps.clear() +} + +void function RodeoBatteryPackRemovalDamage( entity attacker, entity victim, entity victimTitanSoul ) +{ + victimTitanSoul.e.lastRodeoAttacker = attacker + + int damageAmount = GetSegmentHealthForTitan( victim ) + + if ( PlayerHasBattery( attacker ) ) //i.e. you are throwing a grenade + damageAmount /= 2 + + SetSoulBatteryCount( victimTitanSoul, GetSoulBatteryCount( victimTitanSoul ) - 1 ) + + int damageScriptType = damageTypes.rodeoBatteryRemoval + + if ( GetDoomedState( victim ) ) + { + damageAmount = victim.GetHealth() + 1 + } + else if ( IsHardcoreGameMode() ) + { + damageAmount = victim.GetHealth() + } + + table damageTable = + { + scriptType = damageScriptType + forceKill = false + damageSourceId = eDamageSourceId.rodeo_battery_removal + origin = victim.GetOrigin() + hitbox = 2 + } + + victim.TakeDamage( damageAmount, attacker, attacker, damageTable ) + if ( victim.IsNPC() ) + victim.SetEnemyLKP( attacker, attacker.GetOrigin() ) + + entity batteryContainer = victimTitanSoul.soul.batteryContainer + int hatchAttachmentIndex = batteryContainer.LookupAttachment( "REF" ) + + TitanLoseSegementFX( victim, attacker, victim.GetAttachmentOrigin( hatchAttachmentIndex ) ) + + if ( IsSingleplayer() && attacker.IsPlayer() ) + { + UnlockAchievement( attacker, achievements.RODEO ) + } +} + +void function Rodeo_DropAllBatteriesOnDeath( entity player, entity attacker, var damageInfo ) +{ + Rodeo_DropAllBatteries( player ) +} + +void function Rodeo_ApplyAllBatteriesOnEmbark( entity player, entity titan ) +{ + thread Rodeo_ApplyAllBatteriesOnEmbark_Thread( player, titan ) +} + +void function Rodeo_ApplyAllBatteriesOnEmbark_Thread( entity player, entity titan ) +{ + player.EndSignal( "OnDeath" ) + + if ( !PlayerHasBattery( player ) ) + return + + entity soul = player.GetTitanSoul() + soul.EndSignal( "OnDestroy" ) + soul.EndSignal( "OnTitanDeath" ) + + table e + e[ "hadAmped" ] <- false + + while ( GetPlayerBatteryCount( player ) > 0 ) + { + thread Rodeo_ApplyBatteryDelayed( player, e ) + } + + wait 0.4 + + MessagePlayerGivingBatteryToTitan( player, player, eEventNotifications.Rodeo_YouEmbarkedWithABattery, -1, e[ "hadAmped" ] ) +} + +void function Rodeo_ApplyBatteryDelayed( entity player, table e ) +{ + entity battery = Rodeo_TakeBatteryAwayFromPilot( player ) + e[ "hadAmped" ] = e[ "hadAmped" ] || IsAmpedBattery( battery ) + int skin = battery.GetSkin() + battery.Destroy() + + entity dummyBattery = CreatePropDynamic( RODEO_BATTERY_MODEL_FOR_RODEO_ANIMS ) + dummyBattery.SetSkin( skin ) + dummyBattery.Hide() + + entity soul = player.GetTitanSoul() + + OnThreadEnd( + function() : ( dummyBattery ) { + if ( IsValid( dummyBattery ) ) + dummyBattery.Destroy() + } + ) + + if ( !IsValid( soul ) ) + return + + dummyBattery.EndSignal( "OnDestroy" ) + soul.EndSignal( "OnDestroy" ) + soul.EndSignal( "OnTitanDeath" ) + + wait 0.4 // delay so that it applies the battery when the player is inside the titan, so he can see the health bar change + + if ( IsValid( soul.GetTitan() ) ) + Rodeo_ApplyBatteryToTitan( dummyBattery, soul.GetTitan() ) +} + +void function Rodeo_GiveExecutingTitanABattery( entity attacker ) +{ + Rodeo_ApplyBatteryToTitan( null, attacker ) + MessagePlayerGivingBatteryToTitan( attacker, attacker, eEventNotifications.Rodeo_TitanPickedUpBattery, -1, false ) +} + +void function Rodeo_GiveBatteryToPlayer( entity player ) +{ + if ( PlayerHasMaxBatteryCount( player ) ) + return + + entity battery = Rodeo_CreateBatteryPack() + Rodeo_OnTouchBatteryPack_Internal( player, battery ) //Just setting the origin to the player's origin also works, but it will parent weirdly to a pilot's back. probably because we end up doing 2 SetOrigins in the same frame +} + +void function Burnmeter_AmpedBattery( entity player ) +{ + #if MP + Burnmeter_EmergencyBattery( player ) + entity battery = GetBatteryOnBack( player ) + + if ( battery == null ) // not ideal but at least the game won't crash + return + + battery.SetSkin( 2 ) // yellow - CHANGE SKIN TO ORANGE someday + Battery_StartFX( battery ) + #endif +} + +void function Burnmeter_EmergencyBattery( entity player ) +{ + entity battery = Rodeo_CreateBatteryPack() + if ( !PlayerHasMaxBatteryCount( player ) ) + { + Rodeo_OnTouchBatteryPack_Internal( player, battery ) //Just setting the origin to the player's origin also works, but it will parent weirdly to a pilot's back. probably because we end up doing 2 SetOrigins in the same frame + return + } + else + { + //Based off ThrowBattery + vector ornull thrownSpot = CalculateSpotForThrownBattery( player, battery ) + + if ( thrownSpot == null ) + thrownSpot = player.GetOrigin() + + expect vector( thrownSpot ) + + vector viewVector = player.GetViewVector() + + //printt( "viewVector: " + viewVector ) + //battery.SetPhysics( MOVETYPE_FLYGRAVITY ) + + battery.SetParent( player ) //HACK: Clear Ground Entity of battery. Not really sure why this is needed + battery.ClearParent() + + battery.SetAngles( < 0, 0, 0 > ) + battery.SetOrigin( thrownSpot ) + + vector playerVel = player.GetVelocity() + vector verticalAdjustment = < 0, 0, 0 > + if ( playerVel.z == 0 ) + verticalAdjustment = < 0, 0, 100 > + + vector batteryVel = playerVel + viewVector * 50 + verticalAdjustment + battery.SetVelocity( batteryVel ) + + } + +} + +entity function Rodeo_CreateBatteryPack( entity titanStolenFrom = null ) +{ + entity batteryPack = CreateEntity( "item_titan_battery" ) + batteryPack.SetValueForModelKey( RODEO_BATTERY_MODEL ) + batteryPack.kv.fadedist = 10000 + DispatchSpawn( batteryPack ) + batteryPack.SetModel( RODEO_BATTERY_MODEL ) + batteryPack.s.touchEnabledTime <- 0 + batteryPack.s.batteryCarriedStatusEffect <- 0 + + batteryPack.Minimap_SetAlignUpright( true ) + batteryPack.Minimap_SetZOrder( MINIMAP_Z_OBJECT ) + batteryPack.Minimap_SetClampToEdge( false ) + batteryPack.Minimap_AlwaysShow( TEAM_MILITIA, null ) + batteryPack.Minimap_AlwaysShow( TEAM_IMC, null ) + + Battery_StartFX( batteryPack ) + + if ( HAS_BATTERY_THIEF_ICON && titanStolenFrom != null ) + { + Assert( titanStolenFrom.IsTitan() ) + if ( titanStolenFrom.IsPlayer() ) + { + batteryPack.SetBossPlayer( titanStolenFrom ) + } + else + { + entity titanOwner = titanStolenFrom.GetBossPlayer() + if ( IsValid( titanOwner ) ) + batteryPack.SetBossPlayer( titanOwner ) + } + + thread ClearBatteryBossPlayerAfterDelay( batteryPack, titanStolenFrom, RODEO_BATTERY_THIEF_ICON_DURATION ) + } + + batteryPack.Highlight_SetInheritHighlight( true ) + + if ( IsSingleplayer() ) + { + thread AttachTriggerToBattery( batteryPack ) + } + + //thread MonitorBatteryVelocity( batteryPack ) + return batteryPack +} + +void function Battery_StartFX( entity battery ) +{ + Battery_StopFX( battery ) //Clear existing fx first. Not quite ideal but easier to do this than have bug potential for FX to stack on top of each other. + int attachID = battery.LookupAttachment( "fx_center" ) + + asset fx = BATTERY_FX_FRIENDLY + if ( IsAmpedBattery( battery ) ) + fx = BATTERY_FX_AMPED + + battery.e.fxArray.append( StartParticleEffectOnEntity_ReturnEntity( battery, GetParticleSystemIndex( fx ), FX_PATTACH_POINT_FOLLOW, attachID ) ) +} + +void function Battery_StopFX( entity battery ) +{ + foreach( fx in battery.e.fxArray ) + { + EffectStop( fx ) + } + + battery.e.fxArray.clear() +} + +void function Battery_StopFXAndHideIconForPlayer( entity player ) +{ + if ( !PlayerHasBattery( player ) ) + return + + entity battery = GetBatteryOnBack( player ) + + Battery_StopFX( battery ) + battery.ClearBossPlayer() //Boss player controls visibility of icon +} + +void function AttachTriggerToBattery( entity batteryPack ) +{ + entity trigger = CreateEntity( "trigger_cylinder" ) + trigger.SetRadius( 100 ) + trigger.SetAboveHeight( 100 ) + trigger.SetBelowHeight( 100 ) //i.e. make the trigger a sphere as opposed to a cylinder + trigger.SetOrigin( batteryPack.GetOrigin() ) + trigger.SetParent( batteryPack ) + trigger.kv.triggerFilterNpc = "none" // none + trigger.kv.triggerFilterPlayer = "titan" // titan players only + DispatchSpawn( trigger ) + trigger.SetEnterCallback( BatteryTrigger_ApplyBattery ) +} + +void function BatteryTrigger_ApplyBattery( entity trigger, entity player ) +{ + if ( player.IsTitan() ) + { + entity batteryPack = trigger.GetParent() + + if ( batteryPack != null ) + { + Rodeo_OnTouchBatteryPack( player, batteryPack ) + } + } +} + +void function Rodeo_PilotPicksUpBattery_Silent( entity pilot, entity battery ) +{ + Assert( battery.GetParent() == null ) + + if ( PlayerHasBattery( pilot ) ) + { + battery.Destroy() + battery = GetBatteryOnBack( pilot ) + } + + SetPlayerBatteryCount( pilot, GetPlayerBatteryCount( pilot ) + 1 ) + if ( GetPlayerBatteryCount( pilot ) == 1 ) + { + battery.SetParent( pilot, "BATTERY_ATTACH" ) + battery.MarkAsNonMovingAttachment() + battery.RemoveFromSpatialPartition() + SetBatteryOnBack( pilot, battery ) + } + + if ( GAMETYPE == FREE_AGENCY && PlayerHasMaxBatteryCount( pilot ) && PlayerEarnMeter_GetOwnedFrac( pilot ) < 1.0 ) + { + Rodeo_RemoveAllBatteriesOffPlayer( pilot ) + return + } + + if ( battery.s.batteryCarriedStatusEffect == 0 ) + battery.s.batteryCarriedStatusEffect = StatusEffect_AddEndless( battery, eStatusEffect.battery_carried, 1.0 ) + battery.Minimap_Hide( TEAM_MILITIA, null ) + battery.Minimap_Hide( TEAM_IMC, null ) +} + +void function Rodeo_PilotPicksUpBattery( entity pilot, entity battery ) +{ + Rodeo_PilotPicksUpBattery_Silent( pilot, battery ) + EmitSoundOnEntityOnlyToPlayer( pilot, pilot, PILOT_PICKS_UP_BATTERY_SOUND ) + //AddPlayerHeldButtonEventCallback( player, IN_USE, Rodeo_PilotThrowsBattery, RODEO_THROW_BATTERY_BUTTON_HOLD_TIME ) +} + +entity function Rodeo_TakeBatteryAwayFromPilot( entity pilot ) +{ + //RemovePlayerHeldButtonEventCallback( player, IN_USE, Rodeo_PilotThrowsBattery, RODEO_THROW_BATTERY_BUTTON_HOLD_TIME ) + SetPlayerBatteryCount( pilot, GetPlayerBatteryCount( pilot ) - 1 ) + + if ( GetPlayerBatteryCount( pilot ) == 0 ) + { + entity battery = GetBatteryOnBack( pilot ) + Assert( IsValid( battery ) ) + Assert( battery.GetParent() == pilot ) + + SetBatteryOnBack( pilot, null ) //Defensive fix for 209362. Set it to null before doing any other actions on it which might cause execution to jump somewhere else. I think doing PutEntityInSafeSpot() might cause this? + battery.Minimap_AlwaysShow( TEAM_MILITIA, null ) + battery.Minimap_AlwaysShow( TEAM_IMC, null ) + + battery.s.touchEnabledTime = Time() + 0.3 + + Battery_StartFX( battery ) //Needed to properly restore effect when player is killed while cloaked and carrying a battery + + battery.Show() + + if ( battery.s.batteryCarriedStatusEffect > 0 ) + { + StatusEffect_Stop( battery, battery.s.batteryCarriedStatusEffect ) + battery.s.batteryCarriedStatusEffect = 0 + } + + battery.ClearParent() + battery.AddToSpatialPartition() + battery.SetAngles( <0, 0, 0 > ) + battery.SetVelocity( < 0, 0, 1 > ) + PutEntityInSafeSpot( battery, pilot, null, pilot.GetOrigin(), battery.GetOrigin() ) //This might cause thread of execution to jump somewhere else, see 209362 + return battery + } + else + { + return null + } + + unreachable +} + +void function Rodeo_PilotThrowsBattery( entity pilot ) +{ + if ( pilot.ContextAction_IsActive() ) //Maybe letting you throw the battery out of the dropship might be cool? + return + + entity battery = GetBatteryOnBack( pilot ) + + vector ornull thrownSpot = CalculateSpotForThrownBattery( pilot, battery ) + + if ( thrownSpot == null ) + { + EmitSoundOnEntityOnlyToPlayer( pilot, pilot, "CoOp_SentryGun_DeploymentDeniedBeep" ) + return + } + + expect vector( thrownSpot ) + + vector viewVector = pilot.GetViewVector() + + //printt( "viewVector: " + viewVector ) + + entity playerBattery = Rodeo_TakeBatteryAwayFromPilot( pilot ) + Assert( playerBattery == battery ) + + //battery.SetPhysics( MOVETYPE_FLYGRAVITY ) + + battery.SetAngles( < 0, 0, 0 > ) + battery.SetOrigin( thrownSpot ) + vector pilotVel = pilot.GetVelocity() + vector verticalAdjustment = < 0, 0, 0 > + if ( pilotVel.z == 0 ) + verticalAdjustment = < 0, 0, 200 > + + vector batteryVel = pilotVel + viewVector * 300 + verticalAdjustment + //printt( "batteryVel: " + batteryVel) + //battery.SetVelocity( Vector( 0, 0, 0 ) ) + battery.SetVelocity( batteryVel ) + + MessageToPlayer( pilot, eEventNotifications.Rodeo_YouDroppedABattery ) +} + +vector ornull function CalculateSpotForThrownBattery( entity pilot, entity battery ) +{ + vector viewVector = pilot.GetViewVector() + vector eyePos = pilot.EyePosition() + vector batteryMins = battery.GetBoundingMins() + vector batteryMaxs = battery.GetBoundingMaxs() + vector endPos = eyePos + viewVector * 100 + TraceResults hullResult = TraceHull( eyePos, endPos, batteryMins, batteryMaxs, pilot, TRACE_MASK_SOLID | TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE ) + + //PrintTraceResults( hullResult ) + + if ( hullResult.startSolid ) + return null + + if ( hullResult.hitEnt == pilot ) + return null + + if ( hullResult.fraction == 1.0 ) + return endPos + + return hullResult.endPos +} + +void function Rodeo_DropAllBatteries( entity player ) +{ + if ( !PlayerHasBattery( player ) ) + return + + while ( GetPlayerBatteryCount( player ) > 1 ) + { + entity newBattery = Rodeo_CreateBatteryPack() + newBattery.s.touchEnabledTime = Time() + 0.3 + //look into using the players bounds for placement, instead of hardcoded numbers + array offsets = [<0,0,0>, <30,0,0>, <0,30,0>, <0,-30,0> ] + newBattery.SetOrigin( player.GetWorldSpaceCenter() + offsets[ GetPlayerBatteryCount( player ) ] ) //Temp fix, should change the origin + newBattery.SetAngles( <0, 0, 0 > ) + vector baseVelocity = player.GetVelocity() + baseVelocity.z = 0 + newBattery.SetVelocity( baseVelocity + AnglesToForward( <0, RandomInt( 360.0 ), 0 > ) * 100 + <0,0,1> ) + Rodeo_TakeBatteryAwayFromPilot( player ) + } + + entity battery = Rodeo_TakeBatteryAwayFromPilot( player ) + Assert ( IsValid( battery ) ) +} + +void function Rodeo_RemoveBatteryOffPlayer( entity player ) //Meant to be used in prematch etc. +{ + if ( !PlayerHasBattery( player ) ) + return + + entity battery = Rodeo_TakeBatteryAwayFromPilot( player ) + if ( IsValid( battery ) ) + { + battery.Destroy() + } +} + +void function Rodeo_RemoveAllBatteriesOffPlayer( entity player ) //Meant to be used in prematch etc. +{ + if ( !PlayerHasBattery( player ) ) + return + + while ( GetPlayerBatteryCount( player ) > 0 ) + { + Rodeo_RemoveBatteryOffPlayer( player ) + } +} + +void function Rodeo_ApplyBatteryToTitan( entity battery, entity titan ) +{ + entity soul = titan.GetTitanSoul() + + if ( !IsValid( soul ) ) + return + + int healingAmount + if ( IsSingleplayer() ) + healingAmount = 2000 + else + healingAmount = GetSegmentHealthForTitan( titan ) + + int health = titan.GetHealth() + int maxHealth = titan.GetMaxHealth() + + SetSoulBatteryCount( soul, GetSoulBatteryCount( soul ) + 1 ) + + int healthDifference = maxHealth - health + + if ( IsSingleplayer() ) + { + if ( soul.IsDoomed() ) + UndoomTitan( titan, 1 ) + else if ( healthDifference >= healingAmount ) + titan.SetHealth( titan.GetHealth() + healingAmount ) + else + titan.SetHealth( titan.GetMaxHealth() ) + + if ( GetHealthFrac( titan ) >= BATTERY_PICKUP_IGNORE_FRAC ) + { + titan.SetHealth( titan.GetMaxHealth() ) + } + + titan.GetTitanSoul().nextRegenTime = Time() + if ( healthDifference < healingAmount ) + { + titan.GetTitanSoul().SetShieldHealth( healingAmount - healthDifference + titan.GetTitanSoul().GetShieldHealth() ) + } + + if ( GetShieldHealthFrac( titan ) >= BATTERY_PICKUP_IGNORE_FRAC ) + { + titan.GetTitanSoul().SetShieldHealth( titan.GetTitanSoul().GetShieldHealthMax() ) + } + + } + else if ( IsMultiplayer() ) + { + if ( SoulHasPassive( soul, ePassives.PAS_VANGUARD_DOOM ) && soul.IsDoomed() ) + { + UndoomTitan( titan, 1 ) + titan.SetHealth( 1 ) + } + float coreFrac = GetCurrentPlaylistVarFloat( "battery_core_frac", 0.2 ) + float shieldFrac = GetCurrentPlaylistVarFloat( "battery_shield_frac", 1.0 ) + float ampedHealthSegmentFrac = GetCurrentPlaylistVarFloat( "amped_battery_health_frac", 2.0 ) + float healthSegmentFrac = GetCurrentPlaylistVarFloat( "battery_health_frac", 0.5 ) + + AddCreditToTitanCoreBuilder( titan, coreFrac ) //Always give core + + int shieldHealth = soul.GetShieldHealth() + int shieldMaxHealth = soul.GetShieldHealthMax() + + int shieldDifference = shieldMaxHealth - shieldHealth + + bool batteryIsAmped = IsAmpedBattery( battery ) + float frac = batteryIsAmped ? ampedHealthSegmentFrac : healthSegmentFrac + + int addHealth = int( healingAmount * frac ) + + int totalHealth = minint( titan.GetMaxHealth(), titan.GetHealth() + addHealth ) + if ( soul.IsDoomed() && batteryIsAmped ) + { + UndoomTitan( titan, 1 ) + soul.SetShieldHealth( soul.GetShieldHealthMax() ) + } + else + { + titan.SetHealth( totalHealth ) + soul.SetShieldHealth( soul.GetShieldHealthMax() ) + } + } + + if ( battery != null ) + { + Assert( battery.GetParent() == null ) + battery.Destroy() + } +} + +bool function Rodeo_OnTouchBatteryPack( entity player, entity batteryPack ) +{ + Rodeo_OnTouchBatteryPack_Internal( player, batteryPack ) + + //Basically always return false since we don't want the battery pack to go away when being touched. ApplyBatteryToTitan() etc will deal with lifetime of battery + return false +} + +void function Rodeo_OnTouchBatteryPack_Internal( entity player, entity batteryPack ) +{ + float currentTime = Time() + + if ( currentTime < batteryPack.s.touchEnabledTime ) + return + + if ( !IsAlive( player ) ) + return + + if ( player.IsPhaseShifted() ) + return + + if ( IsValid( batteryPack.GetParent() ) ) + return + + if ( PlayerHasMaxBatteryCount( player ) ) + { + if ( IsSingleplayer() ) + { + MessageToPlayer( player, eEventNotifications.BATT_Full, batteryPack ) + } + return + } + + if ( player.IsTitan() ) + { + //Try Titans not being able to pick up battery + if ( GetCurrentPlaylistVarInt( "rodeo_battery_disembark_to_pickup", 1 ) == 1 ) + { + if ( currentTime - player.p.batteryLastTouchedNotificationTime > 5.0 ) + { + MessageToPlayer( player, eEventNotifications.Rodeo_DisembarkToPickUpBattery ) + player.p.batteryLastTouchedNotificationTime = currentTime + + } + } + else + { + if ( IsSingleplayer() ) + { + if ( player.GetHealth() >= player.GetMaxHealth() * BATTERY_PICKUP_IGNORE_FRAC && player.GetTitanSoul().GetShieldHealth() >= player.GetTitanSoul().GetShieldHealthMax() * BATTERY_PICKUP_IGNORE_FRAC ) + { + MessageToPlayer( player, eEventNotifications.BATT_HealthFull, batteryPack ) + return + } + } + bool amped = IsAmpedBattery( batteryPack ) + + Rodeo_ApplyBatteryToTitan( batteryPack, player ) + MessagePlayerGivingBatteryToTitan( player, player, eEventNotifications.Rodeo_TitanPickedUpBattery, -1, amped ) + } + return + } + else + { + if ( IsCloaked( player ) ) + Battery_StopFX( batteryPack ) //Will be turned on again when player loses cloak + + Rodeo_PilotPicksUpBattery( player, batteryPack ) + AddPlayerScore( player, "PilotBatteryPickup" ) +// MessageToPlayer( player, eEventNotifications.Rodeo_PilotPickedUpBattery ) + return + } +} + +void function Rodeo_PilotAddsBatteryToFriendlyTitan( entity rider, entity titan ) +{ + if ( !titan.IsTitan() ) + return + + if ( titan.GetTeam() != rider.GetTeam() ) + return + + if ( !PlayerHasBattery( rider ) ) + return + + entity battery = Rodeo_TakeBatteryAwayFromPilot( rider ) + bool amped = IsAmpedBattery( battery ) + + if ( file.applyBatteryCallback != null ) + file.applyBatteryCallback( rider, titan, battery ) + + Rodeo_ApplyBatteryToTitan( battery, titan ) //This destroys the battery + + AddPlayerScore( rider, "PilotBatteryApplied" ) + + EmitSoundOnEntityOnlyToPlayer( rider, rider, PILOT_APPLIES_BATTERY_TO_TITAN_HEALTH_RESTORED_SOUND ) + + if ( titan.IsPlayer() ) + MessagePlayerGivingBatteryToTitan( titan, rider, eEventNotifications.Rodeo_PilotAppliedBatteryToYou, eEventNotifications.Rodeo_YouAppliedBatteryToTitan, amped ) + else + MessagePlayerGivingBatteryToTitan( titan, rider, eEventNotifications.Rodeo_PilotAppliedBatteryToYourPetTitan, eEventNotifications.Rodeo_YouAppliedBatteryToPetTitan, amped ) +} + +void function MessagePlayerGivingBatteryToTitan( entity receivingTitan, entity givingPlayer, int enumForRecevingHealth, int enumForGivingHealth, bool wasAmped ) +{ + entity receivingPlayer = receivingTitan + + if ( !receivingTitan.IsPlayer() ) + receivingPlayer = receivingTitan.GetBossPlayer() + + if ( !IsValid( receivingPlayer ) ) + return + + MessageToPlayer( receivingPlayer, enumForRecevingHealth, givingPlayer, wasAmped ) + if ( givingPlayer != receivingPlayer ) + MessageToPlayer( givingPlayer, enumForGivingHealth, receivingTitan, wasAmped ) +} + +bool function IsTitanAtFullHealth( entity receivingTitan ) +{ + if ( !receivingTitan.IsTitan() ) + return false + + return ( receivingTitan.GetHealth() == receivingTitan.GetMaxHealth() ) +} + +function DebugRodeoTimes() +{ + array settings = [ "atlas", "ogre", "stryder" ] + + array< asset > models = [ $"models/Humans/imc_pilot/male_cq/imc_pilot_male_cq.mdl", $"models/humans/pilot/female_cq/pilot_female_cq.mdl" ] + table times = {} + + array rodeoAnims = [ + "pt_rodeo_move_back_entrance", + "pt_rodeo_move_right_entrance", + "pt_rodeo_move_front_entrance", + "pt_rodeo_move_front_lower_entrance", + "pt_rodeo_move_back_mid_entrance", + "pt_rodeo_move_back_lower_entrance", + "pt_rodeo_move_left_entrance" + ] + + foreach ( model in models ) + { + times[ model ] <- [] + entity prop = CreatePropDynamic( model, Vector(0,0,0), Vector(0,0,0) ) + printt( "Human model: " + model ) + + foreach ( setting in settings ) + { + foreach ( alias in rodeoAnims ) + { + string animation = GetAnimFromAlias( setting, alias ) + float time = prop.GetSequenceDuration( animation ) + times[ model ].append( { time = time, animation = animation } ) + } + } + + prop.Kill_Deprecated_UseDestroyInstead() + } + + printt( "Time comparison: " ) + bool wrong = false + for ( int i = 0; i < times[ models[0] ].len(); i++ ) + { + if ( times[models[0]][i].time == times[models[1]][i].time ) + { + printt( " MATCH: " + ( i + 1 ) + " times: " + times[models[0]][i].time + " " + times[models[1]][i].time + " " + times[models[1]][i].animation ) + } + else + { + printt( "MISMATCH: " + ( i + 1 ) + " times: " + times[models[0]][i].time + " " + times[models[1]][i].time + " " + times[models[1]][i].animation ) + } + if ( ( i + 1 ) % rodeoAnims.len() == 0 ) + printt( " " ) + } + Assert( !wrong, "Times did not match between male and female, see above" ) +} + +void function SetBatteryOnBack( entity player, entity battery ) +{ + player.SetPlayerNetEnt( "batteryOnBack", battery ) +} + +bool function ClientCommand_RequestRodeoBattery( entity player, array args ) +{ + //PrintFunc() + if ( !ShouldLetPlayerRequestBattery( player ) ) + return true + + player.SetPlayerNetTime( "requestRodeoBatteryLastUsedTime", Time() ) + + foreach( friendlyPlayer in GetPlayerArrayOfTeam( player.GetTeam() ) ) + { + if ( friendlyPlayer == player ) + continue + + if ( friendlyPlayer.IsTitan() ) + continue + + //Could check to see if players actually have a battery here, but that stops players from being told that they should pick up a battery for someone in need + MessageToPlayer( friendlyPlayer, eEventNotifications.Rodeo_RequestBattery, player ) + } + + return true +} + +bool function ClientCommand_OfferRodeoBattery( entity player, array args ) +{ + //PrintFunc() + if ( args.len() != 1 ) + return true + + int friendlyTitanEntIndex = args[ 0 ].tointeger() + + if ( friendlyTitanEntIndex < 1 ) //Data sanitation. GetEntByIndex() will assert if passed a negative number. 0 is always world spawn, so the first valid argument is 1 + return true + + entity friendlyTitan = GetEntByIndex( friendlyTitanEntIndex ) + + if ( !ShouldShowOfferRodeoBatteryHint( player, friendlyTitan ) ) + return true + + entity battery = GetBatteryOnBack( player ) + + MessageToPlayer( friendlyTitan, eEventNotifications.Rodeo_FriendlyPickedUpBattery, player, battery.GetEncodedEHandle() ) + + player.SetPlayerNetTime( "offerRodeoBatteryLastUsedTime", Time() ) + + return true + +} + +void function PlayerRodeoViewCone( entity player, string rodeoTargetType ) +{ + player.PlayerCone_FromAnim() + player.GetFirstPersonProxy().HideFirstPersonProxy() + OpenViewCone( player ) + player.PlayerCone_Disable() + player.EnableWorldSpacePlayerEyeAngles() +} + + +void function OpenViewCone( entity player ) +{ + player.PlayerCone_FromAnim() + player.PlayerCone_SetMinYaw( -179 ) + player.PlayerCone_SetMaxYaw( 181 ) + player.PlayerCone_SetMinPitch( -60 ) + player.PlayerCone_SetMaxPitch( 60 ) +} + +bool function PilotCanApplyBattery( entity rodeoPilot, entity rodeoTitan ) +{ + if ( !IsAlive( rodeoTitan ) ) + return false + + if ( rodeoTitan.GetTeam() != rodeoPilot.GetTeam() ) + return false + + if ( !PlayerHasBattery( rodeoPilot ) ) + return false + + entity titanSoul = rodeoTitan.GetTitanSoul() + Assert( IsValid( titanSoul ) ) + + string titanType = GetSoulTitanSubClass( titanSoul ) + + return true +} + +void function ClearBatteryBossPlayerAfterDelay( entity battery, entity titan, float delay ) +{ + entity soul = titan.GetTitanSoul() + + if ( !IsValid( soul ) ) + return + + soul.EndSignal( "OnTitanDeath" ) //End signal on soul to properly handle pilot getting in/out of titan + battery.EndSignal( "OnDestroy" ) + + OnThreadEnd( + function() : ( battery ) + { + if ( IsValid( battery ) ) + battery.ClearBossPlayer() + } + ) + + wait delay +} + +const float BATTERY_USES_ATTACKER_ORIGIN_THRESHOLD = 500 * 500 //500 seems like a lot, but the Titan melee execution sequences can go pretty far + +void function TitanDropsBatteryOnDeath( entity titan, var damageInfo ) //Todo: Might want to do something special for titan melee execution, so the attacker automatically gets a battery +{ + if ( !titan.IsTitan() ) + return + + entity battery = Rodeo_CreateBatteryPack() + vector titanOrigin = titan.GetOrigin() + battery.SetOrigin( titanOrigin ) + entity attacker = DamageInfo_GetAttacker( damageInfo ) + vector safeOrigin = titanOrigin + vector attackerOrigin + if ( IsValid( attacker ) ) + { + vector attackerOrigin = attacker.GetOrigin() + float distSqr = DistanceSqr( attackerOrigin, titanOrigin ) + //printt( "Distance sqr: " + distSqr ) + if ( distSqr <= ( BATTERY_USES_ATTACKER_ORIGIN_THRESHOLD ) ) // + { + //printt( "Putting attackerOrigin as safeOrigin" ) + + safeOrigin = attackerOrigin + } + } + + bool result = PutEntityInSafeSpot( battery, null, null, safeOrigin, titanOrigin ) + if ( !result ) + { + battery.Destroy() //Can't put the battery anywhere safe, so just destroy it. + //printt( "Destroy battery since we can't put it in a safe spot" ) + } +} + +void function ShowRequestRodeoBatteryHint_OnDamage( entity playerTitan, var damageInfo ) +{ + ShowRequestRodeoBatteryHint( playerTitan ) +} + +void function ShowRequestRodeoBatteryHint_OnPilotBecomesTitan( entity player, entity titan ) +{ + //printt( "player health: " + player.GetHealth() ) + ShowRequestRodeoBatteryHint( player ) +} + +void function ShowRequestRodeoBatteryHint( entity playerTitan ) +{ + //PrintFunc() + if ( !ShouldLetPlayerRequestBattery( playerTitan ) ) + return + + float currentTime = Time() + + if ( playerTitan.p.rodeoRequestBatteryHintLastShownTime > 0.0 && currentTime < playerTitan.p.rodeoRequestBatteryHintLastShownTime + REQUEST_RODEO_BATTERY_HINT_COOLDOWN ) //Use a different cooldown for the hint as opposed to the ability + { + //printt( "Current time: " + currentTime + ", lastShownTime: " + playerTitan.p.rodeoRequestBatteryHintLastShownTime + ", cooldown: " + REQUEST_RODEO_BATTERY_HINT_COOLDOWN ) + return + } + + int stringID = GetStringID( "#RODEO_REQUEST_BATTERY_HINT" ) + MessageToPlayer( playerTitan, eEventNotifications.Rodeo_ShowBatteryHint, null, stringID ) + + playerTitan.p.rodeoRequestBatteryHintLastShownTime = currentTime +} + +void function SetSoulBatteryCount( entity soul, int count ) +{ + count = maxint( 0, count ) + + soul.SetTitanSoulNetInt( "rodeoBatteryCount", count ) +} + +void function PilotBattery_SetMaxCount( int batteryCount ) +{ + file.maxPilotBatteryCount = batteryCount +} + +bool function PlayerHasMaxBatteryCount( entity player ) +{ + if ( !PlayerHasBattery( player ) ) + { + Assert( GetPlayerBatteryCount( player ) == 0 ) + return false + } + + return GetPlayerBatteryCount( player ) == file.maxPilotBatteryCount +} + +void function ThrowRiderOff( entity rider, entity titan, vector direction, bool adjustAirControl = true ) +{ + if ( GetBugReproNum() == 112023 ) //Track down why eye angles of rider snaps violently when titan is looking downwards + { + thread AnglesDebug( rider ) + } + + rider.p.rodeoShouldAdjustJumpOffVelocity = false + + rider.Signal( "RodeoOver" ) + rider.ClearParent() + + #if DEV + if ( GetDebugRodeoPrint() ) + printt( "Throw Rider off: origin before vertical adjustment: " + rider.GetOrigin() ) + #endif + + rider.SetOrigin( rider.GetOrigin() + Vector( 0, 0, 100 ) ) + + #if DEV + if ( GetDebugRodeoPrint() ) + printt( "Throw Rider off: origin after vertical adjustment: " + rider.GetOrigin() ) + #endif + + //printt( "Rider eye angles: " + rider.EyeAngles() + ", rider Angles: " + rider.GetAngles() + " titan eye Angles:" + titan.EyeAngles() ) + + // Set it higher in SP so bosses less exploitable + #if SP + direction += Vector( 0, 0, SP_RODEO_BOOST ) + #endif + + rider.SetVelocity( direction ) + rider.JumpedOffRodeo() + + int attachIndex = titan.LookupAttachment( "hijack" ) //TODO: Hardcoded, no way to get rodeopackage.attachpoint easily at this point anymore! + vector startPos = titan.GetAttachmentOrigin( attachIndex ) + + //printt( "startPos of attachment: " + startPos ) + + if ( !PlayerCanTeleportHere( rider, startPos, titan ) ) + { + startPos = titan.GetOrigin() + if ( !PlayerCanTeleportHere( rider, startPos, titan ) ) + startPos = rider.GetOrigin() + } + + PutEntityInSafeSpot( rider, titan, null, startPos, rider.GetOrigin() ) + + #if DEV + if ( GetDebugRodeoPrint() ) + printt( "Throw Rider off: origin after PutEntityInSafeSpot: " + rider.GetOrigin() ) + #endif + + if ( adjustAirControl ) + thread PostRodeoAirControl( rider ) +} + +void function PostRodeoAirControl( entity player ) +{ + player.Signal( "PostRodeoAirControl" ) + player.EndSignal( "PostRodeoAirControl" ) + player.EndSignal( "OnDeath" ) + + OnThreadEnd( + function() : ( player ) + { + RestorePlayerAirControl( player ) + } + ) + + const float POST_RODEO_AIR_CONTROL_DURATION = 0.75 + const float POST_RODEO_AIR_CONTROL_SCALE = 0.5 + const float POST_RODEO_AIR_CONTROL_JUMP_DELAY = 0.45 + + // give the player time to be thrown in the proper direction before they get back double jump + RemovePlayerAirControl( player ) + player.ConsumeDoubleJump() + wait POST_RODEO_AIR_CONTROL_JUMP_DELAY + player.TouchGround() + + float startTime = Time() + while ( Time() - startTime < POST_RODEO_AIR_CONTROL_DURATION && !player.IsOnGround() && !player.IsWallRunning() && !player.IsWallHanging() ) + { + float elapsedTime = Time() - startTime + player.kv.airSpeed = player.GetPlayerSettingsField( "airSpeed" ) * POST_RODEO_AIR_CONTROL_SCALE * (1 - (elapsedTime / POST_RODEO_AIR_CONTROL_DURATION)) + player.kv.airAcceleration = player.GetPlayerSettingsField( "airAcceleration" ) * POST_RODEO_AIR_CONTROL_SCALE * (1 - (elapsedTime / POST_RODEO_AIR_CONTROL_DURATION)) + //printt( "scale", POST_RODEO_AIR_CONTROL_SCALE * (1 - (elapsedTime / POST_RODEO_AIR_CONTROL_DURATION)) ) + + WaitFrame() + } +} + +void function AnglesDebug( rider ) +{ + printt( "Begin Angles Debug, Rider eye angles: " + rider.EyeAngles() + ", rider Angles: " + rider.GetAngles() ) + while( !rider.IsOnGround() ) + { + printt( "Rider eye angles: " + rider.EyeAngles() + ", rider Angles: " + rider.GetAngles() ) + WaitFrame() + } + + printt( "End Angles Debug, Rider eye angles: " + rider.EyeAngles() + ", rider Angles: " + rider.GetAngles() ) +} + + + +void function SetPlayerBatteryCount( entity player, int count ) +{ + Assert( count <= file.maxPilotBatteryCount ) + Assert( count >= 0 ) + player.SetPlayerNetInt( "batteryCount", count ) +} + +int function GetPlayerBatteryCount( entity player ) +{ + return player.GetPlayerNetInt( "batteryCount" ) +} + +void function DisableBTRodeo( entity soul ) +{ + string settings = GetSoulPlayerSettings( soul ) + var rodeoAllow = Dev_GetPlayerSettingByKeyField_Global( settings, "rodeo_allow" ) + + if ( rodeoAllow == null ) + return + + if ( rodeoAllow == 0 ) + { + soul.SetIsValidRodeoTarget( false ) + } +} +void function RemovePlayerAirControl( entity player ) //This function should really be in a server only SP & MP utility script file. No such file exists as of right now. +{ + Assert( player.IsPlayer() ) + player.kv.airSpeed = 0 + player.kv.airAcceleration = 0 +} + +void function RestorePlayerAirControl( entity player ) //This function should really be in a server only SP & MP utility script file. No such file exists as of right now. +{ + Assert( player.IsPlayer() ) + player.kv.airSpeed = player.GetPlayerSettingsField( "airSpeed" ) + player.kv.airAcceleration = player.GetPlayerSettingsField( "airAcceleration" ) +} + +bool function ShouldThrowGrenadeInHatch( entity rodeoPilot ) +{ + bool batteryPullingDisabled = (GetCurrentPlaylistVarInt( "rodeo_battery_disable_pulls_from_titans", 0 ) == 1) + if ( batteryPullingDisabled ) + return true + + #if MP + if ( PlayerWantsToThrowNukeGrenade( rodeoPilot ) ) + return false + #endif + + if ( PlayerHasBattery( rodeoPilot ) ) + return true + + return false +} + + +#if DEV +void function SetDebugRodeoPrint( bool value ) +{ + file.debugRodeoPrint = value +} + +bool function GetDebugRodeoPrint() +{ + return file.debugRodeoPrint +} +#endif + +#if MP +void function SetApplyBatteryCallback( void functionref(entity,entity,entity) func ) +{ + file.applyBatteryCallback = func +} + +bool function PlayerWantsToThrowNukeGrenade( entity player ) +{ + return ( player in file.playersThatWantToUseRodeoGrenade ) +} + +bool function HasSuperRodeoGrenade( entity player ) +{ + // HACK: because we ran out of player global net ints for "numSuperRodeoGrenades" in bounty hunt + if ( GameRules_GetGameMode() != FD ) + return false + return player.GetPlayerNetInt( "numSuperRodeoGrenades" ) > 0 +} + +void function DeductSuperRodeoGrenade( entity player, int amount ) +{ + int num = player.GetPlayerNetInt( "numSuperRodeoGrenades" ) + player.SetPlayerNetInt( "numSuperRodeoGrenades", num-amount ) +} + +void function RodeoForceNuke( entity pilot ) +{ + entity titan = GetTitanBeingRodeoed( pilot ) + if ( !IsValid( titan ) ) + return + + if ( !titan.IsNPC() || titan.GetTitanSoul().IsEjecting() ) + return + + table damageTable = + { + scriptType = damageTypes.rodeoBatteryRemoval + forceKill = false + damageSourceId = eDamageSourceId.core_overload + origin = titan.GetOrigin() + hitbox = 2 + } + titan.TakeDamage( 1, pilot, pilot, damageTable ) + + if ( !IsAlive( titan ) || titan.GetTitanSoul().IsEjecting() ) + return + + DeductSuperRodeoGrenade( pilot, 1 ) + + // THROW RODEO RIDER OFF + entity soul = titan.GetTitanSoul() + soul.soul.nukeAttacker = pilot + NPC_SetNuclearPayload( titan ) + + vector ejectAngles = titan.GetAngles() + ejectAngles.x = 270 + vector riderEjectAngles = AnglesCompose( ejectAngles, < 5, 0, 0 > ) + + float speed = RandomFloatRange( 1900, 2100 ) + float gravityScale = expect float ( pilot.GetPlayerSettingsField( "gravityscale" ) ) + vector riderVelocity = AnglesToForward( riderEjectAngles ) * (speed * gravityScale) * 0.95 + ThrowRiderOff( pilot, titan, riderVelocity ) + + if ( titan.ContextAction_IsBusy() ) + titan.ContextAction_ClearBusy() + thread TitanEjectPlayer( titan, true ) +} + +void function OpenRodeoNukeWindow( entity player, entity titan ) +{ + player.EndSignal( "RodeoNukeWindowEnded" ) + player.EndSignal( "OnDeath" ) + player.EndSignal( "RodeoOver" ) + titan.EndSignal( "OnDeath" ) + + if ( player in file.playersThatWantToUseRodeoGrenade ) + delete file.playersThatWantToUseRodeoGrenade[ player ] + + if ( player.GetTeam() == titan.GetTeam() ) + return + + if ( !HasSuperRodeoGrenade( player ) ) + return + + Remote_CallFunction_NonReplay( player, "ServerCallback_NukeGrenadeWindowOpen" ) + + OnThreadEnd( + function() : ( player ) + { + if ( IsValid( player ) ) + Remote_CallFunction_NonReplay( player, "ServerCallback_NukeGrenadeWindowClosed" ) + } + ) + + player.WaitSignal( "TryNukeGrenade" ) + + if ( !HasSuperRodeoGrenade( player ) ) + return + + file.playersThatWantToUseRodeoGrenade[ player ] <- true + + MessageToPlayer( player, eEventNotifications.FD_SuperRodeoUsed ) + Rodeo_MoveBatteryDown( titan.GetTitanSoul() ) +} + +bool function ClientCommand_TryNukeGrenade( entity player, array args ) +{ + if ( HasSuperRodeoGrenade( player ) ) + player.Signal( "TryNukeGrenade" ) + + return true +} +#endif \ No newline at end of file diff --git a/Northstar.Custom/mod/scripts/vscripts/rodeo/sh_classic_rodeo.gnut b/Northstar.Custom/mod/scripts/vscripts/rodeo/sh_classic_rodeo.gnut new file mode 100644 index 00000000..6e2f0259 --- /dev/null +++ b/Northstar.Custom/mod/scripts/vscripts/rodeo/sh_classic_rodeo.gnut @@ -0,0 +1,61 @@ +untyped + +global function ClassicRodeo_InitPlaylistVars + +#if SERVER +global function CreateClassicRodeoWeakpoint +#endif + +const asset RODEO_WEAKPOINT_HITBOX_MODEL = $"models/Weapons/ammoboxes/backpack_single.mdl" + +void function ClassicRodeo_InitPlaylistVars() +{ + AddPrivateMatchModeSettingEnum( "#MODE_SETTING_CATEGORY_TITAN", "classic_rodeo", [ "Disabled", "Enabled" ], "0" ) +} + +#if SERVER +entity function CreateClassicRodeoWeakpoint( entity player, entity titan ) +{ + entity weakpoint = CreatePropScript( RODEO_WEAKPOINT_HITBOX_MODEL ) + weakpoint.SetParent( titan, "RODEO_BATTERY" ) + weakpoint.SetLocalAngles( < 90, -90, 0 > ) + weakpoint.SetTakeDamageType( DAMAGE_YES ) + SetTeam( weakpoint, TEAM_UNASSIGNED ) + SetObjectCanBeMeleed( weakpoint, false ) + weakpoint.kv.solid = 6 + weakpoint.Hide() + + // stryder ones don't really work in the default position, so change it + // note: stryders are way too easy to hit because of this lol so possibly fuck with it + if ( GetSoulTitanSubClass( titan.GetTitanSoul() ) == "stryder" ) + weakpoint.SetLocalOrigin( < 0, 4, -4 > ) + + weakpoint.s.pilot <- player + weakpoint.s.titan <- titan + + AddEntityCallback_OnDamaged( weakpoint, OnRodeoWeakpointDamaged ) +} + +void function OnRodeoWeakpointDamaged( entity weakpoint, var damageInfo ) +{ + entity attacker = DamageInfo_GetAttacker( damageInfo ) + entity titan = attacker.GetParent() + float damageAmount = DamageInfo_GetDamage( damageInfo ) + DamageInfo_SetDamage( damageInfo, 0 ) // make sure weakpoint ent doesn't die ever + + if ( attacker != weakpoint.s.pilot || titan != weakpoint.s.titan ) + return + + // hitmarker + attacker.NotifyDidDamage( weakpoint, DamageInfo_GetHitBox( damageInfo ), DamageInfo_GetDamagePosition( damageInfo ), DamageInfo_GetCustomDamageType( damageInfo ) | DF_CRITICAL, damageAmount, DamageInfo_GetDamageFlags( damageInfo ), DamageInfo_GetHitGroup( damageInfo ), DamageInfo_GetWeapon( damageInfo ), DamageInfo_GetDistFromAttackOrigin( damageInfo ) ) + + // figure out damage to deal to titan + entity attackerWeapon = attacker.GetActiveWeapon() + int rodeoDamage = attackerWeapon.GetWeaponSettingInt( eWeaponVar.damage_rodeo ) // only really on weapons that were in tf1, unfortunately + if ( rodeoDamage == 0 ) + rodeoDamage = int( attackerWeapon.GetWeaponSettingInt( eWeaponVar.damage_near_value_titanarmor ) * ( attackerWeapon.GetWeaponSettingFloat( eWeaponVar.damage_headshot_scale ) * 1.5 ) ) // would use headshot scale, but it's a bit low in most cases to be competitive + + // damage titan + titan.TakeDamage( rodeoDamage, attacker, attackerWeapon, { damageSourceId = eDamageSourceId.rodeo } ) +} +#endif \ No newline at end of file diff --git a/Northstar.Custom/mod/scripts/vscripts/sh_3psequence_to_1p_hacks.gnut b/Northstar.Custom/mod/scripts/vscripts/sh_3psequence_to_1p_hacks.gnut new file mode 100644 index 00000000..8b50811a --- /dev/null +++ b/Northstar.Custom/mod/scripts/vscripts/sh_3psequence_to_1p_hacks.gnut @@ -0,0 +1,178 @@ +global function FirstPersonSequenceForce1P_Init +global function FirstPersonSequenceForce1P_InitPlaylistVars + +#if SERVER + global function FirstPersonSequenceForce1P +#endif + +#if CLIENT + global function ServerCallback_HideHudForFPHackAnim +#endif + +global const string FORCE1P_PILOT_1P_ATTACHMENT = "HEADFOCUS" +global const string FORCE1P_TITAN_1P_ATTACHMENT = "HATCH_HEAD" // CHEST_LASER could be better, but is only on atlas titans + +global const string FORCE1P_PILOT_1P_HIDDEN_BODYGROUP = "head" +global const string FORCE1P_TITAN_1P_HIDDEN_BODYGROUP = "torso" + +global const string FORCE1P_PILOT_ENTITYCLASS = "npc_pilot_elite" +global const string FORCE1P_TITAN_ENTITYCLASS = "npc_titan" + +global struct Forced1PSequenceData +{ + entity player + entity camera + entity ownerProxy + entity thirdPersonProxy +} + +void function FirstPersonSequenceForce1P_Init() +{ + // atm do this no matter what playlist we're on since playlist overrides seem to get sent to clients after networkvar registration + // not nice but whatever lol + AddCallback_OnRegisteringCustomNetworkVars( FirstPersonSequenceForce1P_RegisterCustomNetworkFunctions ) +} + +void function FirstPersonSequenceForce1P_InitPlaylistVars() +{ + PrecacheModel( $"models/weapons/sentry_turret/sentry_turret.mdl" ) + AddPrivateMatchModeSettingEnum( "#MODE_SETTING_CATEGORY_RIFF", "fp_embark_enabled", [ "Disabled", "Enabled" ], "0" ) +} + +void function FirstPersonSequenceForce1P_RegisterCustomNetworkFunctions() +{ + Remote_RegisterFunction( "ServerCallback_HideHudForFPHackAnim" ) +} + +#if SERVER +Forced1PSequenceData function FirstPersonSequenceForce1P( FirstPersonSequenceStruct sequence, entity player, entity other = null ) +{ + string attachment = FORCE1P_PILOT_1P_ATTACHMENT + string hiddenBodygroup = FORCE1P_PILOT_1P_HIDDEN_BODYGROUP + string entityclass = FORCE1P_PILOT_ENTITYCLASS + + if ( player.IsTitan() ) + { + attachment = FORCE1P_TITAN_1P_ATTACHMENT + hiddenBodygroup = FORCE1P_TITAN_1P_HIDDEN_BODYGROUP + entityclass = FORCE1P_TITAN_ENTITYCLASS + } + + // hide player from everyone, unlike VisibilityFlags, this won't hide children, which is way easier to deal with + player.Hide() + + Forced1PSequenceData cleanupData + cleanupData.player = player + + // for some melee sequences, player.GetAngles() will be the angles the player had before they began the melee, which can cause desyncs + // eyeangles are fine though + vector angles = player.GetAngles() + angles.y = player.EyeAngles().y + + // create the first proxy entity, this should visually be identical to the player, but only visible to them, and with head/torso hidden + // this is an npc because some firstpersonsequences use animation features that only work on npcs and pilots, not props, so need to do this + entity ownerProxy = CreateEntity( entityclass ) //CreatePropDynamic( player.GetModelName(), player.GetOrigin(), player.GetAngles() ) + ownerProxy.SetModel( player.GetModelName() ) + ownerProxy.SetValueForModelKey( player.GetModelName() ) + ownerProxy.SetOrigin( player.GetOrigin() ) + ownerProxy.SetAngles( angles ) + ownerProxy.kv.VisibilityFlags = ENTITY_VISIBLE_TO_OWNER + ownerProxy.kv.solid = 0 // nonsolid + SetTeam( ownerProxy, player.GetTeam() ) + ownerProxy.SetOwner( player ) + ownerProxy.SetSkin( player.GetSkin() ) + ownerProxy.SetCamo( player.GetCamo() ) // note: this seems weird, doesn't set right + DispatchSpawn( ownerProxy ) + ownerProxy.SetModel( player.GetModelName() ) + ownerProxy.SetValueForModelKey( player.GetModelName() ) + ownerProxy.SetInvulnerable() + cleanupData.ownerProxy = ownerProxy + + int bodygroupValue = 1 + if ( hiddenBodygroup == "torso" ) + bodygroupValue = 2 + + // hide annoying bodygroup + ownerProxy.SetBodygroup( ownerProxy.FindBodyGroup( hiddenBodygroup ), bodygroupValue ) + // don't play anim until later so we can do cleanup stuff + + // create the second proxy entity, this visible to everyone else + entity thirdPersonProxy = CreateEntity( entityclass ) //CreatePropDynamic( player.GetModelName(), player.GetOrigin(), player.GetAngles() ) + thirdPersonProxy.SetModel( player.GetModelName() ) + thirdPersonProxy.SetValueForModelKey( player.GetModelName() ) + thirdPersonProxy.SetOrigin( player.GetOrigin() ) + thirdPersonProxy.SetAngles( angles ) + thirdPersonProxy.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE & ~ENTITY_VISIBLE_TO_OWNER + thirdPersonProxy.kv.solid = 0 // nonsolid + SetTeam( thirdPersonProxy, player.GetTeam() ) + thirdPersonProxy.SetOwner( player ) + thirdPersonProxy.SetSkin( player.GetSkin() ) + thirdPersonProxy.SetCamo( player.GetCamo() ) // note: this seems weird, doesn't set right + DispatchSpawn( thirdPersonProxy ) + thirdPersonProxy.SetModel( player.GetModelName() ) + thirdPersonProxy.SetValueForModelKey( player.GetModelName() ) + thirdPersonProxy.SetInvulnerable() + cleanupData.thirdPersonProxy = thirdPersonProxy + + if ( player.IsTitan() ) + Highlight_SetEnemyHighlight( thirdPersonProxy, "enemy_titan" ) + else + Highlight_SetEnemyHighlight( thirdPersonProxy, "enemy_player" ) + + thread FirstPersonSequence( sequence, thirdPersonProxy, other ) + + // create the viewpoint entity + entity camera = CreateEntity( "point_viewcontrol" ) + camera.SetParent( ownerProxy, attachment ) + camera.kv.spawnflags = 56 + DispatchSpawn( camera ) + player.SetViewEntity( camera, false ) + cleanupData.camera = camera + + Remote_CallFunction_NonReplay( player, "ServerCallback_HideHudForFPHackAnim" ) + // play this anim now, so we can cleanup after it's done + thread CleanupForced1PSequenceAfterAnimDone( sequence, ownerProxy, other, cleanupData ) + return cleanupData +} + +void function CleanupForced1PSequenceAfterAnimDone( FirstPersonSequenceStruct sequence, entity player, entity other, Forced1PSequenceData cleanupData ) +{ + player.EndSignal( "OnDestroy" ) + player.EndSignal( "OnAnimationDone" ) + + OnThreadEnd( function() : ( cleanupData ) + { + CleanupForced1PSequence( cleanupData ) + }) + + FirstPersonSequence( sequence, player, other ) +} + +void function CleanupForced1PSequence( Forced1PSequenceData cleanupData ) +{ + cleanupData.player.Show() + cleanupData.player.ClearViewEntity() + cleanupData.camera.Destroy() + cleanupData.ownerProxy.Destroy() + cleanupData.thirdPersonProxy.Destroy() +} +#endif + +#if CLIENT +void function ServerCallback_HideHudForFPHackAnim() +{ + thread MainHud_TurnOff_RUI( true ) + HidePermanentCockpitRui() + + thread EnableHudOnViewRestored() +} + +void function EnableHudOnViewRestored() +{ + while ( GetViewEntity() != GetLocalClientPlayer() ) + WaitFrame() + + thread MainHud_TurnOn_RUI( true ) + ShowPermanentCockpitRui() +} +#endif \ No newline at end of file diff --git a/Northstar.Custom/mod/scripts/vscripts/titan/sh_first_person_embark.gnut b/Northstar.Custom/mod/scripts/vscripts/titan/sh_first_person_embark.gnut deleted file mode 100644 index 0c47c014..00000000 --- a/Northstar.Custom/mod/scripts/vscripts/titan/sh_first_person_embark.gnut +++ /dev/null @@ -1,47 +0,0 @@ -global function FirstPersonEmbark_Init -global function FirstPersonEmbark_InitPlaylistVars - -#if CLIENT - global function ServerCallback_HideHudForFPEmbark -#endif - -void function FirstPersonEmbark_Init() -{ - // atm do this no matter what playlist we're on since playlist overrides seem to get sent to clients after networkvar registration - // not nice but whatever lol - AddCallback_OnRegisteringCustomNetworkVars( FirstPersonEmbark_RegisterCustomNetworkFunctions ) - - // busted rn lol - if ( GetCurrentPlaylistVarInt( "fp_embark_enabled", 0 ) == 0 ) - return - - #if CLIENT - AddCallback_PlayerClassChanged( ShowHudOnEmbarkFinished ) - #endif -} - -void function FirstPersonEmbark_RegisterCustomNetworkFunctions() -{ - Remote_RegisterFunction( "ServerCallback_HideHudForFPEmbark" ) -} - -void function FirstPersonEmbark_InitPlaylistVars() -{ - AddPrivateMatchModeSettingEnum( "#MODE_SETTING_CATEGORY_TITAN", "fp_embark_enabled", [ "Disabled", "Enabled" ], "0" ) -} - -#if CLIENT -void function ServerCallback_HideHudForFPEmbark() -{ - thread MainHud_TurnOff_RUI( true ) - HidePermanentCockpitRui() -} - -void function ShowHudOnEmbarkFinished( entity player ) -{ - if ( !player.IsTitan() ) - return - - ShowPermanentCockpitRui() -} -#endif \ No newline at end of file diff --git a/Northstar.Custom/mod/scripts/vscripts/titan/sh_titan_embark.gnut b/Northstar.Custom/mod/scripts/vscripts/titan/sh_titan_embark.gnut index f9df2730..5a1ecd8f 100644 --- a/Northstar.Custom/mod/scripts/vscripts/titan/sh_titan_embark.gnut +++ b/Northstar.Custom/mod/scripts/vscripts/titan/sh_titan_embark.gnut @@ -951,20 +951,8 @@ function TitanEmbark_PlayerEmbarks( entity player, entity titan, table e ) } ) - if ( GetCurrentPlaylistVarInt( "fp_embark_enabled", 0 ) == 1 ) - { - Remote_CallFunction_NonReplay( player, "ServerCallback_HideHudForFPEmbark" ) - - // fp embark hacks - entity viewControl = CreateEntity( "point_viewcontrol" ) - viewControl.kv.spawnflags = 56 - DispatchSpawn( viewControl ) - - viewControl.SetParent( player, "headshot" ) - viewControl.SetOrigin( < 4, 0, 0 > ) - viewControl.SetAngles( < 0, 0, 0 > ) - player.SetViewEntity( viewControl, false ) - } + if ( GetCurrentPlaylistVarInt( "fp_embark_enabled", 0 ) == 1 && !doFirstPersonAnim ) + FirstPersonSequenceForce1P( sequence, player, titan ) thread FirstPersonSequence( sequence, player, titan ) // EmitDifferentSoundsOnEntityForPlayerAndWorld( firstPersonAudio, thirdPersonAudio, titan, player ) @@ -1032,15 +1020,9 @@ void function Embark_DelayedFadeOut( entity player, entity titan, float delay ) } else { - OnThreadEnd( function() : ( player ) - { - player.ClearViewEntity() - }) - wait EMBARK_FADE_TIME - 0.2 ScreenFadeToBlack( player, 0.2, 0.4 ) wait 0.2 - player.ClearViewEntity() // make sure player is in normal first person again } ScreenFadeFromBlack( player, EMBARK_FADE_TIME, EMBARK_FADE_TIME ) @@ -1105,10 +1087,7 @@ void function PhaseEmbarkPhaseStart( entity player ) EmitSoundOnEntity( player, "pilot_phaseembark_activate_3p" ) if ( GetCurrentPlaylistVarInt( "fp_embark_enabled", 0 ) == 1 ) - { player.PhaseShiftBegin( 0.0, 0.2 ) - player.GetPetTitan().SetForceVisibleInPhaseShift( true ) // doesn't work for some reason - } thread PhaseEmbarkPhaseCleanup( player ) } -- cgit v1.2.3