aboutsummaryrefslogtreecommitdiff
path: root/Northstar.CustomServers/scripts/vscripts/titan
diff options
context:
space:
mode:
authorBobTheBob <32057864+BobTheBob9@users.noreply.github.com>2021-06-22 14:30:49 +0100
committerBobTheBob <32057864+BobTheBob9@users.noreply.github.com>2021-06-22 14:30:49 +0100
commit207facbc402f5639cbcd31f079214351ef605cf2 (patch)
tree4710b2a88dd64f3dfea1609d31a5de9141640951 /Northstar.CustomServers/scripts/vscripts/titan
parentc2d438568df6d98cf731807e30eaa7da31e5ea52 (diff)
downloadNorthstarMods-207facbc402f5639cbcd31f079214351ef605cf2.tar.gz
NorthstarMods-207facbc402f5639cbcd31f079214351ef605cf2.zip
initial commit after moving to new repo
Diffstat (limited to 'Northstar.CustomServers/scripts/vscripts/titan')
-rw-r--r--Northstar.CustomServers/scripts/vscripts/titan/_battery_generator.gnut128
-rw-r--r--Northstar.CustomServers/scripts/vscripts/titan/_replacement_titans.gnut1183
-rw-r--r--Northstar.CustomServers/scripts/vscripts/titan/_replacement_titans_drop.gnut443
-rw-r--r--Northstar.CustomServers/scripts/vscripts/titan/_titan_commands.gnut49
-rw-r--r--Northstar.CustomServers/scripts/vscripts/titan/_titan_health.gnut1072
-rw-r--r--Northstar.CustomServers/scripts/vscripts/titan/_titan_hints.gnut267
-rw-r--r--Northstar.CustomServers/scripts/vscripts/titan/_titan_hotdrop.gnut778
-rw-r--r--Northstar.CustomServers/scripts/vscripts/titan/_titan_triple_health.gnut524
-rw-r--r--Northstar.CustomServers/scripts/vscripts/titan/class_titan.gnut77
9 files changed, 4521 insertions, 0 deletions
diff --git a/Northstar.CustomServers/scripts/vscripts/titan/_battery_generator.gnut b/Northstar.CustomServers/scripts/vscripts/titan/_battery_generator.gnut
new file mode 100644
index 00000000..567ad6e7
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/titan/_battery_generator.gnut
@@ -0,0 +1,128 @@
+global function InitDestroyableGenerator
+global function ClearGenerators
+
+const GENERATOR_HEALTH = 200
+
+const MODEL_DESTROYED_GENERATOR = $"models/beacon/charge_generator_01_destroyed.mdl"
+const FX_GENERATOR_EXP = $"P_generator_exp"
+
+struct
+{
+ array<entity> generators
+} file
+
+void function InitDestroyableGenerator()
+{
+ AddSpawnCallbackEditorClass( "script_ref", "script_battery_generator", SpawnPropGenerator )
+ AddSpawnCallback_ScriptName( "prop_battery_generator", PropBatteryGeneratorThink )
+
+ PrecacheModel( MODEL_GENERATOR )
+ PrecacheModel( MODEL_DESTROYED_GENERATOR )
+ PrecacheParticleSystem( FX_GENERATOR_EXP )
+}
+
+void function SpawnPropGenerator( entity generatorRef )
+{
+ entity generator = CreatePropScript( MODEL_GENERATOR, generatorRef.GetOrigin(), generatorRef.GetAngles(), 6 )
+ thread PropBatteryGeneratorThink( generator )
+ generatorRef.Destroy()
+}
+
+void function PropBatteryGeneratorThink( entity generator )
+{
+ SetObjectCanBeMeleed( generator, true )
+ SetVisibleEntitiesInConeQueriableEnabled( generator, true )
+ generator.SetTakeDamageType( DAMAGE_EVENTS_ONLY )
+ generator.SetDamageNotifications( true )
+ generator.SetMaxHealth( GENERATOR_HEALTH )
+ generator.SetHealth( GENERATOR_HEALTH )
+ generator.DisableHibernation()
+ AddEntityCallback_OnDamaged( generator, GeneratorOnDamage )
+
+ entity trigger = CreateEntity( "trigger_cylinder" )
+ trigger.SetRadius( 150 )
+ trigger.SetAboveHeight( 150 )
+ trigger.SetBelowHeight( 150 ) //i.e. make the trigger a sphere as opposed to a cylinder
+ trigger.SetOrigin( generator.GetOrigin() )
+ trigger.SetParent( generator )
+ DispatchSpawn( trigger )
+ trigger.SetEnterCallback( GeneratorTriggerThink )
+
+
+ file.generators.append( generator )
+}
+
+void function GeneratorTriggerThink( entity trigger, entity ent )
+{
+ if ( ent.IsTitan() || IsSuperSpectre( ent ) )
+ {
+ entity generator = trigger.GetParent()
+
+ if ( generator != null )
+ {
+ GeneratorDestroy( generator )
+ }
+ }
+}
+
+void function GeneratorOnDamage( entity generator, var damageInfo )
+{
+ if ( !IsValid( generator ) )
+ {
+ // sometimes OnDamage gets called twice in the same frame, ( scorch's fire )
+ // and first call destroys generator in GeneratorDestroy()
+ return
+ }
+
+ float damage = DamageInfo_GetDamage( damageInfo )
+
+ int health = generator.GetHealth()
+ health -= int( damage )
+
+ if ( health <= 0 )
+ GeneratorDestroy( generator )
+ else
+ generator.SetHealth( health )
+}
+
+void function GeneratorDestroy( entity generator )
+{
+ int solidType = 6 //phys collision
+ entity destroyedProp = CreatePropDynamic( MODEL_DESTROYED_GENERATOR, generator.GetOrigin(), generator.GetAngles(), solidType )
+ if ( generator.GetParent() )
+ destroyedProp.SetToSameParentAs( generator )
+
+ destroyedProp.AllowMantle()
+ destroyedProp.DisableHibernation()
+ int fxID = GetParticleSystemIndex( FX_GENERATOR_EXP )
+ vector origin = generator.GetOrigin()
+ vector up = generator.GetUpVector()
+
+ EmitSoundOnEntity( destroyedProp, "BatteryCrate_Explosion" )
+ StartParticleEffectOnEntity( destroyedProp, fxID, FX_PATTACH_ABSORIGIN_FOLLOW, -1 )
+
+ entity battery = CreateTitanBattery( origin + ( up * 40 ) )
+ battery.DisableHibernation()
+
+ //throw out the battery
+ vector right = generator.GetRightVector() * RandomFloatRange( -0.5, 0.5 )
+ vector forward = generator.GetForwardVector() * RandomFloatRange( -0.5, 0.5 )
+ vector velocity = Normalize( up + right + forward ) * 10
+
+ //for moving geo
+ vector parentVelocity = generator.GetVelocity()
+
+ battery.SetVelocity( velocity + parentVelocity )
+
+ file.generators.fastremovebyvalue( generator )
+ generator.Destroy()
+}
+
+void function ClearGenerators()
+{
+ foreach ( g in file.generators )
+ {
+ g.Destroy()
+ }
+ file.generators = []
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/titan/_replacement_titans.gnut b/Northstar.CustomServers/scripts/vscripts/titan/_replacement_titans.gnut
new file mode 100644
index 00000000..c9d986bc
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/titan/_replacement_titans.gnut
@@ -0,0 +1,1183 @@
+untyped
+
+global function ReplacementTitans_Init
+
+global function EmptyTitanPlaysAnim
+global function TryReplacementTitanReadyAnnouncement
+
+global function IsReplacementTitanAvailable
+
+global function SetTitanRespawnTimer
+global function GetTitanRespawnTimer
+global function DecrementBuildTimer
+global function ReplacementTitanTimerFinished
+global function GetAttachmentAtTimeFromModel
+global function TryETATitanReadyAnnouncement
+global function TryUpdateTitanRespawnTimerForNewTitanSelection
+global function IsReplacementDropInProgress
+
+global function req
+global function ReplacementTitan
+global function TryAnnounceTitanfallWarningToEnemyTeam
+global function GetTitanForPlayer
+
+
+global function ShouldSetTitanRespawnTimer
+
+global function PauseTitanTimers
+global function PauseTitansThink
+
+global function IsReplacementTitanAvailableForGameState
+
+global function SetReplacementTitanGamemodeRules
+global function SetRequestTitanGamemodeRules
+
+global function CreateTitanForPlayerAndHotdrop
+
+struct {
+ array<int> ETATimeThresholds = [ 120, 60, 30, 15 ]
+ float ETA2MinUpperBound = 123
+ float ETA2MinLowerBound = 115
+ float ETA60sUpperBound = 63
+ float ETA60sLowerBound = 55
+ float ETA30sUpperBound = 33
+ float ETA30sLowerBound = 25
+ float ETA15sUpperBound = 18
+ float ETA15sLowerBound = 12
+ float ETAAnnouncementAllowanceTime = 6.0
+
+ bool buildTimerDisabled = false
+
+ table warpFallDebounce = {}
+
+ bool functionref( entity ) ReplacementTitanGamemodeRules
+ bool functionref( entity, vector ) RequestTitanGamemodeRules
+
+} file
+
+const nagInterval = 40
+
+global const float WARPFALL_SOUND_DELAY = 1.1
+global const float WARPFALL_FX_DELAY = 0.9
+
+function ReplacementTitans_Init()
+{
+ ReplacementTitansDrop_Init()
+
+ RegisterSignal( "titan_impact" )
+
+ RegisterSignal( "SetTitanRespawnTimer" )
+ RegisterSignal( "CalledInReplacementTitan" )
+
+ PrecacheEffect( TURBO_WARP_FX )
+ PrecacheEffect( TURBO_WARP_COMPANY )
+
+
+ AddCallback_OnClientConnecting( ReplacementTitan_InitPlayer )
+ AddClientCommandCallback( "ClientCommand_RequestTitan", ClientCommand_RequestTitan )
+ AddSoulDeathCallback( ResetTitanReplacementAnnouncements )
+
+ level.maxTitansPerTeam <- 2
+
+ if ( file.ReplacementTitanGamemodeRules == null )
+ file.ReplacementTitanGamemodeRules = ReplacementTitanGamemodeRules_Default
+ if ( file.RequestTitanGamemodeRules == null )
+ file.RequestTitanGamemodeRules = RequestTitanGamemodeRules_Default
+
+ FlagInit( "LevelHasRoof" )
+}
+
+
+void function ReplacementTitan_InitPlayer( entity player )
+{
+ player.p.replacementTitanETATimer = GetTimeLimit_ForGameMode() * 60.0
+}
+
+
+bool function IsReplacementTitanAvailable( player, timeBuffer = 0 )
+{
+ expect entity( player )
+
+ if ( !IsReplacementTitanAvailableForGameState() )
+ return false
+
+ if ( player.IsTitan() )
+ return false
+
+ if ( IsAlive( player.GetPetTitan() ) )
+ return false
+
+ if ( player.isSpawning )
+ return false
+
+ if ( !file.ReplacementTitanGamemodeRules( player ) )
+ return false
+
+ switch ( Riff_TitanAvailability() )
+ {
+ case eTitanAvailability.Default:
+ if ( player.titansBuilt == 0 )
+ return true
+ else
+ break
+
+ default:
+ return Riff_IsTitanAvailable( player )
+ }
+
+ if ( player.IsBot() )
+ return true
+
+ return ReplacementTitanTimerFinished( player, timeBuffer )
+}
+
+function IsReplacementTitanAvailableForGameState()
+{
+ #if HAS_GAMEMODES
+ local currentGameState = GetGameState()
+
+ switch ( currentGameState ) //need to add a new entry in here for every new game state we make
+ {
+ case eGameState.WaitingForCustomStart:
+ case eGameState.WaitingForPlayers:
+ case eGameState.PickLoadout:
+ case eGameState.Prematch:
+ case eGameState.SwitchingSides:
+ case eGameState.Postmatch:
+ return false
+
+ case eGameState.Playing:
+ case eGameState.SuddenDeath:
+ return true
+
+ case eGameState.WinnerDetermined:
+ case eGameState.Epilogue:
+ {
+ if ( IsRoundBased() )
+ {
+ if ( !IsRoundBasedGameOver() )
+ return false
+
+ if ( !ShouldRunEvac() )
+ return false
+ }
+
+ return true
+ }
+
+ default:
+ Assert( false, "Unknown Game State: " + currentGameState )
+ return false
+ }
+ #endif
+
+ return true
+}
+
+void function SetReplacementTitanGamemodeRules( bool functionref( entity ) rules )
+{
+ file.ReplacementTitanGamemodeRules = rules
+}
+
+void function SetRequestTitanGamemodeRules( bool functionref( entity, vector ) rules )
+{
+ file.RequestTitanGamemodeRules = rules
+}
+
+bool function ReplacementTitanGamemodeRules_Default( entity player )
+{
+ return true
+}
+
+bool function RequestTitanGamemodeRules_Default( entity player, vector origin )
+{
+ return true
+}
+
+float function GetTitanRespawnTimer( entity player )
+{
+ return player.GetNextTitanRespawnAvailable() - Time()
+}
+
+
+#if SP
+void function DecrementBuildTimer( entity player, float amount )
+{
+ if ( !player.IsTitan() )
+ return
+ // core ability in use
+ if ( TitanCoreInUse( player ) )
+ return
+
+ if ( !IsAlive( player ) )
+ return
+
+ SetTitanCoreTimer( player, GetTitanCoreTimer( player ) - amount )
+}
+#endif
+
+#if MP
+void function DecrementBuildTimer( entity player, float amount )
+{
+ Assert( !TitanDamageRewardsTitanCoreTime() || !player.IsTitan() )
+
+ amount = ModifyBuildTimeForPlayerBonuses( player, amount )
+
+ bool shouldDecrementBuildTimer = true
+
+ if ( player.IsTitan() )
+ {
+ // core ability in use
+ if ( TitanCoreInUse( player ) )
+ return
+
+ if ( !IsAlive( player ) )
+ return
+ }
+ else
+ {
+ //Don't decrement build time for Titan if already have Titan in map
+ if ( player.GetPetTitan() )
+ return
+ }
+
+ if ( player.IsTitan() )
+ {
+ SetTitanCoreTimer( player, GetTitanCoreTimer( player ) - amount )
+ }
+ else if ( shouldDecrementBuildTimer )
+ {
+ float remainingTime = GetTitanRespawnTimer( player )
+ SetTitanRespawnTimer( player, remainingTime - amount )
+ }
+}
+#endif
+
+float function ModifyBuildTimeForPlayerBonuses( entity player, float amount )
+{
+ if ( PlayerHasServerFlag( player, SFLAG_FAST_BUILD2 ) )
+ amount *= 2.0
+ else if ( PlayerHasServerFlag( player, SFLAG_FAST_BUILD1 ) )
+ amount *= 1.5
+
+ return amount
+}
+
+
+void function TryUpdateTitanRespawnTimerForNewTitanSelection( entity player )
+{
+ if ( GetCurrentPlaylistVarInt( "titan_build_time_use_set_file", 0 ) == 1 )
+ {
+ if ( ShouldSetTitanRespawnTimer( player ) )
+ {
+ if ( player.GetTitanBuildTime() != GetTitanBuildTime( player ) )
+ {
+ float timeElapsed = player.GetTitanBuildTime() - ( player.GetNextTitanRespawnAvailable() - Time() )
+ ResetTitanBuildTime( player ) // update titan build time here
+ float newTime = Time() + ( player.GetTitanBuildTime() - timeElapsed )
+ player.SetNextTitanRespawnAvailable( max( 0, newTime ) )
+ }
+ }
+ }
+}
+
+void function SetTitanRespawnTimer( entity player, float timeDiff )
+{
+ //printt( "SetTitanRespawnTimer with timeDiff: " + timeDiff )
+ if ( ShouldSetTitanRespawnTimer( player ) == false )
+ return
+
+ float newTime = Time() + timeDiff
+ player.SetNextTitanRespawnAvailable( max( Time() - 1, newTime ) )
+
+ thread WaitToAnnounceTitanETA( player, timeDiff )
+}
+
+bool function ShouldSetTitanRespawnTimer( player )
+{
+ if ( Riff_TitanAvailability() == eTitanAvailability.Custom )
+ return false
+
+ if ( Riff_TitanAvailability() == eTitanAvailability.Default )
+ return true
+
+ if ( player.IsTitan() )
+ return true
+
+ if ( IsValid( player.GetPetTitan() ) )
+ return true
+
+ if ( player.GetNextTitanRespawnAvailable() < 0 )
+ return false
+
+ return true
+}
+
+
+
+function WaitToAnnounceTitanETA( entity player, timeDiff )
+{
+ player.EndSignal( "OnDestroy" )
+ player.Signal( "SetTitanRespawnTimer" )
+ player.EndSignal( "SetTitanRespawnTimer" )
+ player.EndSignal( "CalledInReplacementTitan" )
+ player.EndSignal( "ChoseToSpawnAsTitan" )
+
+ if ( timeDiff > 0 )
+ wait GetTimeTillNextETAAnnouncement( player )
+
+ TryETATitanReadyAnnouncement( player )
+}
+
+float function GetTimeTillNextETAAnnouncement( entity player )
+{
+// if ( !IsValid( player ) )
+// return 0
+
+ float timeTillNextTitan = player.GetNextTitanRespawnAvailable() - Time()
+ if ( timeTillNextTitan <= 0 )
+ {
+ //printt( "Waiting 0, Titan Ready" )
+ return 0
+ }
+
+// if ( !( "replacementTitanETATimer" in player.s ) )
+// return 0
+
+ if ( timeTillNextTitan >= file.ETA2MinUpperBound && player.p.replacementTitanETATimer > 120 ) //Give some leadup time to conversation starting
+ {
+ //printt( "Waiting " + ( timeTillNextTitan - file.ETA2MinUpperBound ) + " till 2 min announcement" )
+ return timeTillNextTitan - file.ETA2MinUpperBound
+ }
+
+ if ( timeTillNextTitan >= file.ETA2MinLowerBound && player.p.replacementTitanETATimer > 120 )
+ {
+ //printt( "Waiting 0 till 2 min announcement" )
+ return 0 //Play 2 min ETA announcement immediately
+ }
+
+ if ( timeTillNextTitan >= file.ETA60sUpperBound && player.p.replacementTitanETATimer > 60 )
+ {
+ //printt( "Waiting " + ( timeTillNextTitan - file.ETA60sUpperBound ) + " till 60s announcement" )
+ return timeTillNextTitan - file.ETA60sUpperBound
+ }
+
+ if ( timeTillNextTitan >= file.ETA60sLowerBound && player.p.replacementTitanETATimer > 60 )
+ {
+ //printt( "Waiting 0 till 60s announcement" )
+ return 0
+ }
+
+ if ( timeTillNextTitan >= file.ETA30sUpperBound && player.p.replacementTitanETATimer > 30 )
+ {
+ //printt( "Waiting " + ( timeTillNextTitan - file.ETA30sUpperBound ) + " till 30s announcement" )
+ return timeTillNextTitan - file.ETA30sUpperBound
+ }
+
+ if ( timeTillNextTitan >= file.ETA30sLowerBound && player.p.replacementTitanETATimer > 30 )
+ {
+ //printt( "Waiting 0 till 30 announcement" )
+ return 0
+ }
+
+ if ( timeTillNextTitan >= file.ETA15sUpperBound && player.p.replacementTitanETATimer > 15 )
+ {
+ //printt( "Waiting " + ( timeTillNextTitan - file.ETA15sUpperBound ) + " till 15s announcement" )
+ return timeTillNextTitan - file.ETA15sUpperBound
+ }
+
+ if ( timeTillNextTitan >= file.ETA15sLowerBound && player.p.replacementTitanETATimer > 15 )
+ {
+ //printt( "Waiting 0 till 15s announcement" )
+ return 0
+ }
+
+ //printt( "Waiting " + timeTillNextTitan + " till next Titan" )
+ return timeTillNextTitan
+
+
+}
+
+function TryETATitanReadyAnnouncement( entity player )
+{
+ //printt( "TryETATitanReadyAnnouncement" )
+ if ( !IsAlive( player ) )
+ return
+
+ if ( GetPlayerTitanInMap( player ) )
+ return
+
+ if ( player.GetNextTitanRespawnAvailable() < 0 )
+ return
+
+ if ( GetGameState() > eGameState.SuddenDeath )
+ return
+
+ if ( GameTime_PlayingTime() < 5.0 )
+ return
+
+ local timeTillNextTitan = player.GetNextTitanRespawnAvailable() - Time()
+ //printt( "TryETATitanReadyAnnouncement timetillNextTitan: " + timeTillNextTitan )
+ if ( floor(timeTillNextTitan) <= 0 )
+ {
+ //Titan is ready, let TryReplacementTitanReadyAnnouncement take care of it
+ TryReplacementTitanReadyAnnouncement( player )
+ return
+ }
+
+ //This entire loop is probably too complicated now for what it's doing. Simplify next game!
+ //Loop might be pretty hard to read, a particular iteration of the loop is written in comments below
+ for ( int i = 0; i < file.ETATimeThresholds.len(); ++i )
+ {
+ if ( fabs( timeTillNextTitan - file.ETATimeThresholds[ i ] ) < file.ETAAnnouncementAllowanceTime )
+ {
+ if ( player.p.replacementTitanETATimer > file.ETATimeThresholds[ i ] )
+ {
+ if ( player.titansBuilt )
+ PlayConversationToPlayer( "TitanReplacementETA" + file.ETATimeThresholds[ i ] + "s" , player )
+ else
+ PlayConversationToPlayer( "FirstTitanETA" + file.ETATimeThresholds[ i ] + "s", player )
+
+ player.p.replacementTitanETATimer = float ( file.ETATimeThresholds[ i ] )
+ wait timeTillNextTitan - file.ETATimeThresholds[ i ]
+ if ( IsAlive( player ) )
+ SetTitanRespawnTimer( player, player.GetNextTitanRespawnAvailable() - Time() )
+ return
+ }
+ }
+ }
+
+ /*if ( fabs( timeTillNextTitan - 120 ) < ETAAnnouncementAllowanceTime && player.p.replacementTitanETATimer > 120 )
+ {
+ if ( player.titansBuilt )
+ PlayConversationToPlayer( "TitanReplacementETA120s", player )
+ else
+ PlayConversationToPlayer( "FirstTitanETA120s", player )
+ player.p.replacementTitanETATimer = 120
+ wait timeTillNextTitan - 120
+ SetTitanRespawnTimer( player, player.GetNextTitanRespawnAvailable() - Time() )
+ return
+ }
+ */
+
+}
+
+function TryReplacementTitanReadyAnnouncement( entity player )
+{
+ while( true )
+ {
+ //printt( "TryReplacementTitanReadyAnnouncementLoop" )
+ if ( !IsAlive( player ) )
+ return
+
+ if ( GetGameState() > eGameState.SuddenDeath )
+ return
+
+ if ( GetPlayerTitanInMap( player ) )
+ return
+
+ if ( level.nv.titanDropEnabledForTeam != TEAM_BOTH && level.nv.titanDropEnabledForTeam != player.GetTeam() )
+ return
+
+ if ( player.p.replacementTitanReady_lastNagTime == 0 || Time() - player.p.replacementTitanReady_lastNagTime >= nagInterval )
+ {
+ //Don't play Titan Replacement Announcements if you don't have it ready
+ switch ( Riff_TitanAvailability() )
+ {
+ case eTitanAvailability.Default:
+ break
+
+ default:
+ if ( !Riff_IsTitanAvailable( player ) )
+ return
+ }
+
+ if ( player.titansBuilt )
+ {
+ PlayConversationToPlayer( "TitanReplacementReady", player )
+ }
+ else
+ {
+ PlayConversationToPlayer( "FirstTitanReady", player )
+ }
+ player.p.replacementTitanReady_lastNagTime = Time()
+ }
+
+ wait 5.0 // Once every 5 seconds should be fine
+ }
+}
+
+void function ResetTitanReplacementAnnouncements( entity soul, var damageInfo )
+{
+ entity player = soul.GetBossPlayer()
+
+ if ( !IsValid( player ) )
+ return
+
+ player.p.replacementTitanETATimer = expect float( level.nv.gameEndTime )
+}
+
+function req()
+{
+ ReplacementTitan( GetPlayerArray()[0] )
+}
+
+bool function ClientCommand_RequestTitan( entity player, array<string> args )
+{
+ ReplacementTitan( player ) //Separate function because other functions will call ReplacementTitan
+ return true
+}
+
+// This a baseline titan request function; the only things that prevent this from happening are
+// common cases; wrong gamestate, already has a titan, is currently dead, etc...
+bool function RequestTitan( entity player )
+{
+ if ( !IsReplacementTitanAvailableForGameState() )
+ return false
+
+ if ( player.IsTitan() )
+ return false
+
+ if ( IsAlive( player.GetPetTitan() ) )
+ return false
+
+ if ( player.isSpawning )
+ return false
+
+ if ( !IsAlive( player ) )
+ return false
+
+ Point spawnPoint = GetTitanReplacementPoint( player, false )
+ local origin = spawnPoint.origin
+ Assert( origin )
+
+ //Check titanfall request against any custom gamemode rules
+ if ( !file.RequestTitanGamemodeRules( player, spawnPoint.origin ) )
+ return false
+
+ //if ( ShouldDoTitanfall() )
+ thread CreateTitanForPlayerAndHotdrop( player, spawnPoint )
+ //else
+ // thread ForcePilotToBecomeTitan( player )
+
+ return true
+}
+
+bool function ReplacementTitan( entity player )
+{
+ if ( !IsAlive( player ) )
+ {
+ printt( "ReplacementTitan", player, player.entindex(), "failed", "IsAlive( player ) was false" )
+ return false
+ }
+
+ if ( !IsReplacementTitanAvailable( player, 0 ) )
+ {
+ printt( "ReplacementTitan", player, player.entindex(), "failed", "IsReplacementTitanAvailable was false" )
+ return false
+ }
+
+ entity titan = GetPlayerTitanInMap( player )
+ if ( IsAlive( titan ) )
+ {
+ printt( "ReplacementTitan", player, player.entindex(), "failed", "GetPlayerTitanInMap was true" )
+ return false
+ }
+
+ if ( player in file.warpFallDebounce )
+ {
+ if ( Time() - file.warpFallDebounce[ player ] < 3.0 )
+ {
+ printt( "ReplacementTitan", player, player.entindex(), "failed", "player in file.warpFallDebounce was true" )
+ return false
+ }
+ }
+
+ Point spawnPoint = GetTitanReplacementPoint( player, false )
+ local origin = spawnPoint.origin
+ Assert( origin )
+
+ #if MP
+ PIN_PlayerAbility( player, "titanfall", "titanfall", {pos = origin} )
+ #endif
+
+ //Check titanfall request against any custom gamemode rules
+ if ( !file.RequestTitanGamemodeRules( player, spawnPoint.origin ) )
+ return false
+
+ #if SP
+ thread CreateTitanForPlayerAndHotdrop( player, spawnPoint )
+ #endif
+
+ #if MP
+ if ( ShouldDoTitanfall() )
+ thread CreateTitanForPlayerAndHotdrop( player, spawnPoint )
+ else
+ thread ForcePilotToBecomeTitan( player )
+ #endif
+
+ return true
+}
+
+#if MP
+
+void function ForcePilotToBecomeTitan( entity player )
+{
+ float fadeTime = 0.5
+ float holdTime = 2.0
+
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "OnDestroy" )
+
+ if ( GAMETYPE != SST )
+ {
+ #if FACTION_DIALOGUE_ENABLED
+ PlayFactionDialogueToPlayer( "mp_titanInbound" , player )
+ #else
+ if ( player.titansBuilt )
+ PlayConversationToPlayer( "TitanReplacement", player )
+ else
+ PlayConversationToPlayer( "FirstTitanInbound", player )
+ #endif
+ }
+
+ player.Signal( "RodeoOver" )
+ player.Signal( "ScriptAnimStop" )
+
+ table<string,bool> e = {}
+ e.settingsRestored <- false
+
+ OnThreadEnd(
+ function() : ( player, e )
+ {
+ if ( IsValid( player ) && !e.settingsRestored )
+ {
+ Rodeo_Allow( player )
+ player.Show()
+ player.MakeVisible()
+ }
+ }
+ )
+ Rodeo_Disallow( player )
+
+ ScreenFadeToBlack( player, fadeTime, holdTime )
+ player.DissolveNonLethal( ENTITY_DISSOLVE_CORE, Vector( 0, 0, 0 ), 500 )
+
+ wait fadeTime
+ player.SetInvulnerable()
+ player.Hide()
+
+ wait holdTime
+ ScreenFadeFromBlack( player, 1.0, 0.5 )
+ waitthread TitanPlayerHotDropsIntoLevel( player )
+ e.settingsRestored = true
+ Rodeo_Allow( player )
+ player.Show()
+ player.MakeVisible()
+ player.ClearInvulnerable()
+}
+#endif
+
+bool function IsReplacementDropInProgress( entity player )
+{
+ return expect bool( player.s.replacementDropInProgress )
+}
+
+void function CreateTitanForPlayerAndHotdrop( entity player, Point spawnPoint, TitanLoadoutDef ornull overrideLoadout = null )
+{
+ Assert( IsValid( player ) )
+
+ if ( player.isSpawning )
+ {
+ printt( "CreateTitanForPlayerAndHotdrop", player, player.entindex(), "failed", "player.isSpawning was true" )
+ return
+ }
+
+ if ( player.s.replacementDropInProgress )
+ {
+ printt( "CreateTitanForPlayerAndHotdrop", player, player.entindex(), "failed", "player.s.replacementDropInProgress was true" )
+ return
+ }
+
+ player.s.replacementDropInProgress = true
+
+ entity titanFallDisablingEntity = CreateInfoTarget()
+
+ OnThreadEnd(
+ function() : ( player, titanFallDisablingEntity )
+ {
+ if ( IsValid( titanFallDisablingEntity ) ) //As a fail safe. Should have been cleaned up in OnThreadEnd of CleanupTitanFallDisablingEntity
+ titanFallDisablingEntity.Destroy()
+
+ if ( !IsValid( player ) )
+ return
+
+ player.s.replacementDropInProgress = false
+ player.ClearHotDropImpactTime()
+ }
+ )
+
+ player.EndSignal( "OnDestroy" )
+
+ if ( GAMETYPE != SST )
+ {
+ #if FACTION_DIALOGUE_ENABLED
+ PlayFactionDialogueToPlayer( "mp_titanInbound" , player )
+ #else
+ if ( player.titansBuilt )
+ PlayConversationToPlayer( "TitanReplacement", player )
+ else
+ PlayConversationToPlayer( "FirstTitanInbound", player )
+ #endif
+ }
+
+ vector origin = spawnPoint.origin
+ vector angles
+ if ( spawnPoint.angles != < 0.0, 0.0, 0.0 > )
+ angles = spawnPoint.angles
+ else
+ angles = VectorToAngles( FlattenVector( player.GetViewVector() ) * -1 ) // face the player
+
+ printt( "Dropping replacement titan at " + origin + " with angles " + angles )
+
+ #if HAS_STATS
+ UpdatePlayerStat( player, "misc_stats", "titanFalls" )
+ #endif
+ #if SERVER && MP
+ PIN_AddToPlayerCountStat( player, "titanfalls" )
+ #endif
+
+ if ( !level.firstTitanfall )
+ {
+ AddPlayerScore( player, "FirstTitanfall", player )
+
+ #if HAS_STATS
+ UpdatePlayerStat( player, "misc_stats", "titanFallsFirst" )
+ #endif
+
+ level.firstTitanfall = true
+ }
+ else
+ {
+ AddPlayerScore( player, "Titanfall", player )
+ }
+
+
+ player.Signal( "CalledInReplacementTitan" )
+
+ int playerTeam = player.GetTeam()
+
+ TryAnnounceTitanfallWarningToEnemyTeam( playerTeam, origin )
+
+ titanFallDisablingEntity.SetOrigin( origin )
+ DisableTitanfallForLifetimeOfEntityNearOrigin( titanFallDisablingEntity, origin, TITANHOTDROP_DISABLE_ENEMY_TITANFALL_RADIUS )
+
+ entity titan
+ string animation
+
+ string regularTitanfallAnim = "at_hotdrop_drop_2knee_turbo"
+
+ TitanLoadoutDef loadout
+ if ( overrideLoadout == null )
+ {
+ loadout = GetTitanLoadoutForPlayer( player )
+ }
+ else
+ {
+ loadout = expect TitanLoadoutDef( overrideLoadout )
+ }
+ bool hasWarpfall = loadout.passive3 == "pas_warpfall"
+ if ( hasWarpfall || Flag( "LevelHasRoof" ) )
+ {
+ ClearTitanAvailable( player ) //Normally this is done when the Titan is spawned, but for warpfall the Titan isn't spawned instaneously after requesting it.
+
+ file.warpFallDebounce[ player ] <- Time()
+ animation = "at_hotdrop_drop_2knee_turbo_upgraded"
+ string settings = loadout.setFile
+ asset model = GetPlayerSettingsAssetForClassName( settings, "bodymodel" )
+ Attachment warpAttach = GetAttachmentAtTimeFromModel( model, animation, "offset", origin, angles, 0 )
+
+ entity fakeTitan = CreatePropDynamic( model )
+ float impactTime = GetHotDropImpactTime( fakeTitan, animation )
+
+ float diff = 0.0
+
+ if ( !hasWarpfall ) // this means the level requested the warpfall
+ {
+ float regularImpactTime = GetHotDropImpactTime( fakeTitan, regularTitanfallAnim ) - (WARPFALL_SOUND_DELAY + WARPFALL_FX_DELAY)
+ diff = ( regularImpactTime - impactTime )
+ impactTime = regularImpactTime
+ }
+
+ fakeTitan.Kill_Deprecated_UseDestroyInstead()
+
+ local impactStartTime = Time()
+ impactTime += (WARPFALL_SOUND_DELAY + WARPFALL_FX_DELAY)
+ player.SetHotDropImpactDelay( impactTime )
+ Remote_CallFunction_Replay( player, "ServerCallback_ReplacementTitanSpawnpoint", origin.x, origin.y, origin.z, Time() + impactTime )
+
+ EmitDifferentSoundsAtPositionForPlayerAndWorld( "Titan_1P_Warpfall_CallIn", "Titan_3P_Warpfall_CallIn", origin, player, playerTeam )
+
+ wait diff
+
+ wait WARPFALL_SOUND_DELAY
+
+ // "Titan_1P_Warpfall_Start" - for first person warp calls, starting right on the button press
+ // "Titan_3P_Warpfall_Start" - for any 3P other player or NPC when they call in a warp, starting right on their button press
+ EmitSoundAtPositionOnlyToPlayer( playerTeam, origin, player, "Titan_1P_Warpfall_Start" )
+ EmitSoundAtPositionExceptToPlayer( playerTeam, origin, player, "Titan_3P_Warpfall_Start" )
+
+ PlayFX( TURBO_WARP_FX, warpAttach.position + Vector(0,0,-104), warpAttach.angle )
+
+ wait WARPFALL_FX_DELAY
+
+ titan = CreateAutoTitanForPlayer_FromTitanLoadout( player, loadout, origin, angles )
+ DispatchSpawn( titan )
+ thread PlayFXOnEntity( TURBO_WARP_COMPANY, titan, "offset" )
+ }
+ else
+ {
+ animation = regularTitanfallAnim
+
+ titan = CreateAutoTitanForPlayer_FromTitanLoadout( player, loadout, origin, angles )
+ DispatchSpawn( titan )
+
+ float impactTime = GetHotDropImpactTime( titan, animation )
+ player.SetHotDropImpactDelay( impactTime )
+ Remote_CallFunction_Replay( player, "ServerCallback_ReplacementTitanSpawnpoint", origin.x, origin.y, origin.z, Time() + impactTime )
+ }
+
+ SetActiveTitanLoadoutIndex( player, GetPersistentSpawnLoadoutIndex( player, "titan" ) )
+ #if MP
+ SetActiveTitanLoadout( player )
+ #endif
+ if ( player in file.warpFallDebounce )
+ delete file.warpFallDebounce[ player ]
+
+ titan.EndSignal( "OnDeath" )
+ Assert( IsAlive( titan ) )
+
+ // dont let AI titan get enemies while dropping. Don't do trigger checks
+ titan.SetEfficientMode( true )
+ titan.SetTouchTriggers( false )
+ titan.SetNoTarget( true )
+ titan.SetAimAssistAllowed( false )
+
+#if R1_VGUI_MINIMAP
+ thread PingMinimapDuringHotdrop( player, titan, origin )
+#endif
+
+ thread CleanupTitanFallDisablingEntity( titanFallDisablingEntity, titan ) //needs to be here after titan is created
+ waitthread PlayersTitanHotdrops( titan, origin, angles, player, animation ) //Note that this function returns after the titan has played the landing anim, not when the titan hits the ground
+
+ titan.SetEfficientMode( false )
+ titan.SetTouchTriggers( true )
+ titan.SetAimAssistAllowed( true )
+
+ player.Signal( "titan_impact" )
+
+ thread TitanNPC_WaitForBubbleShield_StartAutoTitanBehavior( titan )
+}
+
+void function CleanupTitanFallDisablingEntity( entity titanFallDisablingEntity, entity titan )
+{
+ titanFallDisablingEntity.EndSignal( "OnDestroy" ) //titanFallDisablingEntity can be destroyed multiple ways
+ titan.EndSignal( "ClearDisableTitanfall" ) //This is awkward, CreateBubbleShield() and OnHotDropImpact() signals this to deestroy CleanupTitanFallDisablingEntity
+ titan.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( titanFallDisablingEntity )
+ {
+ if( IsValid( titanFallDisablingEntity ) )
+ titanFallDisablingEntity.Destroy()
+
+ }
+ )
+
+ WaitForever()
+}
+
+void function DrawReplacementTitanLocation( entity player, vector origin, float delay )
+{
+ // have to keep resending this info because a dead player won't see it
+ player.EndSignal( "OnDestroy" )
+ float endTime = Time() + delay
+
+ for ( ;; )
+ {
+ if ( !IsAlive( player ) )
+ {
+ player.WaitSignal( "OnRespawned" )
+ continue
+ }
+
+ float remainingTime = endTime - Time()
+ if ( remainingTime <= 0 )
+ return
+
+ player.SetHotDropImpactDelay( remainingTime )
+ Remote_CallFunction_Replay( player, "ServerCallback_ReplacementTitanSpawnpoint", origin.x, origin.y, origin.z, Time() + remainingTime )
+ player.WaitSignal( "OnDeath" )
+ }
+}
+
+void function TryAnnounceTitanfallWarningToEnemyTeam( int team, vector origin )
+{
+ float innerDistance = TITANFALL_OUTER_RADIUS * TITANFALL_OUTER_RADIUS
+ float outerDistance = innerDistance * 4.0
+
+ array<entity> enemies = GetPlayerArrayOfEnemies( team )
+ foreach ( entity enemyPlayer in enemies )
+ {
+ float distSqr = DistanceSqr( origin, enemyPlayer.GetOrigin() )
+ if ( distSqr > outerDistance )
+ continue
+
+ if ( distSqr < innerDistance )
+ Remote_CallFunction_NonReplay( enemyPlayer, "ServerCallback_TitanFallWarning", true )
+ else
+ Remote_CallFunction_NonReplay( enemyPlayer, "ServerCallback_TitanFallWarning", false )
+ }
+}
+
+TitanSettings function GetTitanForPlayer( entity player )
+{
+ string ornull currentTitanSettings
+ array<string> currentTitanMods
+
+ if ( player.IsBot() )
+ {
+ string botTitanSettings = GetConVarString( "bot_titan_settings" )
+ array<string> legalLoadouts = GetAllowedTitanSetFiles()
+ if ( legalLoadouts.contains( botTitanSettings ) )
+ currentTitanSettings = botTitanSettings
+ else
+ currentTitanSettings = legalLoadouts.getrandom()
+ }
+
+ if ( currentTitanSettings == null )
+ {
+ TitanLoadoutDef loadout = GetTitanLoadoutForPlayer( player )
+ currentTitanSettings = loadout.setFile
+ foreach ( mod in loadout.setFileMods )
+ {
+ currentTitanMods.append( mod )
+ }
+ }
+
+ if ( DebugNewTitanModels() )
+ {
+ switch ( currentTitanSettings )
+ {
+ case "titan_atlas":
+ currentTitanSettings = "titan_medium_ajax"
+ break
+ case "titan_stryder":
+ currentTitanSettings = "titan_light_locust"
+ break
+ case "titan_ogre":
+ currentTitanSettings = "titan_heavy_ogre"
+ break
+ }
+ }
+
+ TitanSettings titanSettings
+ titanSettings.titanSetFile = expect string( currentTitanSettings )
+ titanSettings.titanSetFileMods = currentTitanMods
+ return titanSettings
+}
+
+Attachment function GetAttachmentAtTimeFromModel( asset model, string animation, string attachment, vector origin, vector angles, float time )
+{
+ entity dummy = CreatePropDynamic( model, origin, angles )
+ Attachment start = dummy.Anim_GetAttachmentAtTime( animation, attachment, time )
+ dummy.Destroy()
+ return start
+}
+
+#if R1_VGUI_MINIMAP
+function PingMinimapDuringHotdrop( player, titan, impactOrigin )
+{
+ expect entity( player )
+ expect entity( titan )
+
+ player.EndSignal( "titan_impact" )
+ player.EndSignal( "OnDestroy" )
+ titan.EndSignal( "OnDeath" )
+
+ titan.Minimap_Hide( TEAM_IMC, null )
+ titan.Minimap_Hide( TEAM_MILITIA, null )
+
+ OnThreadEnd(
+ function() : ( player, titan )
+ {
+ if ( !IsAlive( titan ) )
+ return
+
+ titan.Minimap_DisplayDefault( TEAM_IMC, null )
+ titan.Minimap_DisplayDefault( TEAM_MILITIA, null )
+ }
+ )
+
+ while ( true )
+ {
+ Minimap_CreatePingForPlayer( player, impactOrigin, $"vgui/HUD/threathud_titan_friendlyself", 0.5 )
+ wait 0.4
+ }
+}
+#endif
+
+function EmptyTitanPlaysAnim( titan )
+{
+ local idleAnimAlias = "at_atlas_getin_idle"
+ if ( titan.HasKey( "idleAnim" ) )
+ idleAnimAlias = titan.GetValueForKey( "idleAnim" )
+
+ thread PlayAnim( titan, idleAnimAlias )
+}
+
+function FreeSpawnpointOnEnterTitan( spawnpoint, titan )
+{
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "TitanEntered" )
+
+ OnThreadEnd(
+ function() : ( spawnpoint, titan )
+ {
+ Assert( IsValid( titan ) )
+ spawnpoint.e.spawnPointInUse = false
+ }
+ )
+
+ titan.WaitSignal( "TitanBeingEntered" )
+}
+
+
+function DebugText( origin, text, time )
+{
+ local endTime = Time() + time
+
+ while( Time() < endTime )
+ {
+ DebugDrawText( origin, text, true, 1.0 )
+ wait 1
+ }
+}
+
+
+
+bool function ReplacementTitanTimerFinished( player, timeBuffer = 0 )
+{
+ local nextTitanTime = player.GetNextTitanRespawnAvailable()
+ if ( nextTitanTime < 0 )
+ return false
+
+ return nextTitanTime - Time() <= timeBuffer
+}
+
+
+struct
+{
+ float titanTimerPauseTime = 0
+ table<entity, float> playerPauseStartTimes
+
+} protoFile
+
+
+void function PauseTitansThink()
+{
+ bool titan
+ while ( true )
+ {
+ array<entity> players = GetPlayerArray()
+
+ bool foundTitan = false
+ foreach ( player in players )
+ {
+ if ( player.IsTitan() || IsValid( player.GetPetTitan() ) )
+ {
+ foundTitan = true
+ break
+ }
+ }
+
+ if ( foundTitan && protoFile.titanTimerPauseTime == 0 )
+ thread PauseTitanTimers()
+ else if ( !foundTitan && protoFile.titanTimerPauseTime != 0 )
+ thread PauseTitanTimers()
+
+ WaitFrame()
+ }
+}
+
+
+void function PauseTitanTimers()
+{
+ RegisterSignal( "PauseTitanTimers" )
+ svGlobal.levelEnt.Signal( "PauseTitanTimers" )
+ svGlobal.levelEnt.EndSignal( "PauseTitanTimers" )
+
+ if ( protoFile.titanTimerPauseTime != 0 )
+ {
+ protoFile.playerPauseStartTimes = {}
+ protoFile.titanTimerPauseTime = 0
+ return
+ }
+
+ protoFile.titanTimerPauseTime = Time()
+ float lastTime = Time()
+
+ while ( true )
+ {
+ array<entity> players = GetPlayerArray()
+
+ float addTime = Time() - protoFile.titanTimerPauseTime
+
+ foreach ( player in players )
+ {
+ if ( player.IsTitan() )
+ {
+ if ( player in protoFile.playerPauseStartTimes )
+ delete protoFile.playerPauseStartTimes[player]
+
+ continue
+ }
+
+ if ( IsValid( player.GetPetTitan() ) )
+ {
+ if ( player in protoFile.playerPauseStartTimes )
+ delete protoFile.playerPauseStartTimes[player]
+
+ continue
+ }
+
+ if ( Time() > player.GetNextTitanRespawnAvailable() )
+ {
+ if ( player in protoFile.playerPauseStartTimes )
+ delete protoFile.playerPauseStartTimes[player]
+
+ continue
+ }
+
+ if ( !(player in protoFile.playerPauseStartTimes) )
+ {
+ protoFile.playerPauseStartTimes[player] <- player.GetNextTitanRespawnAvailable()
+ }
+
+ protoFile.playerPauseStartTimes[player] += Time() - lastTime
+
+ player.SetNextTitanRespawnAvailable( protoFile.playerPauseStartTimes[player] )
+ }
+
+ lastTime = Time()
+ wait 0.1
+ }
+}
+
+bool function ShouldDoTitanfall()
+{
+ if ( svGlobal.forceDisableTitanfalls )
+ return false
+
+ return ( GetCurrentPlaylistVarInt( "enable_titanfalls", 1 ) == 1 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/titan/_replacement_titans_drop.gnut b/Northstar.CustomServers/scripts/vscripts/titan/_replacement_titans_drop.gnut
new file mode 100644
index 00000000..5970f7ea
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/titan/_replacement_titans_drop.gnut
@@ -0,0 +1,443 @@
+global function ReplacementTitansDrop_Init
+global function GetTitanReplacementPoint
+global function HullTraceDropPoint
+global function DebugTitanSpawn
+global function TitanFindDropNodes
+global function TitanHulldropSpawnpoint
+
+global const TITANDROP_LOS_DIST = 2000 // 2D distance at which we do the line of sight check to see where the player wants to call in the titan
+global const TITANDROP_MIN_FOV = 10
+global const TITANDROP_MAX_FOV = 80
+global const TITANDROP_FOV_PENALTY = 8
+global const TITANDROP_PATHNODESEARCH_EXACTDIST = 500 // within this distance, we use the position the player is looking for the pathnode search
+global const TITANDROP_PATHNODESEARCH_DISTFRAC = 0.8 // beyond that distance, we use this fraction of how far the player is looking.
+global const TITANDROP_GROUNDSEARCH_ZDIR = -1.0 // if the player's not looking at anything, we search downward for ground at this slope
+global const TITANDROP_GROUNDSEARCH_FORWARDDIST = 350 // if the player's not looking at anything, we search for ground starting this many units in front of the player
+global const TITANDROP_GROUNDSEARCH_DIST = 1000 // if the player's not looking at anything, we search for ground this many units forward (max)
+global const TITANDROP_FALLBACK_DIST = 150 // if the ground search hits, we go this many units forward from it
+
+struct
+{
+ int replacementSpawnpointsID
+} file
+
+void function ReplacementTitansDrop_Init()
+{
+ AddSpawnCallback( "info_spawnpoint_titan", AddDroppoint )
+ AddSpawnCallback( "info_spawnpoint_titan_start", AddDroppoint )
+ AddSpawnCallback( "info_replacement_titan_spawn", AddDroppoint )
+
+ AddCallback_EntitiesDidLoad( EntitiesDidLoad )
+ file.replacementSpawnpointsID = CreateScriptManagedEntArray()
+}
+
+void function EntitiesDidLoad()
+{
+}
+
+
+void function AddDroppoint( entity ent )
+{
+ AddToScriptManagedEntArray( file.replacementSpawnpointsID, ent )
+}
+
+void function DebugTitanSpawn()
+{
+ thread DebugTitanSpawnThread()
+}
+
+void function DebugTitanSpawnThread()
+{
+ entity player = GetPlayerArray()[0]
+
+ float interval = 0.1
+
+ FlightPath flightPath = GetAnalysisForModel( GetFlightPathModel( "fp_titan_model" ), HOTDROP_TURBO_ANIM )
+ int dataIndex = GetAnalysisDataIndex( flightPath )
+
+ for ( ;; )
+ {
+ if ( !IsValid( player ) )
+ {
+ wait interval
+ continue
+ }
+
+ vector playerOrg = player.GetOrigin()
+ vector playerEyeForward = player.GetViewVector()
+ vector playerEyePos = player.EyePosition()
+ vector playerEyeAngles = player.EyeAngles()
+ float yaw = playerEyeAngles.y
+ vector ornull desiredPos = GetReplacementTrace( playerEyePos, playerEyeForward )
+ vector pathNodeSearchPos
+ if ( desiredPos == null )
+ {
+ pathNodeSearchPos = GetPathNodeSearchPos( playerOrg, playerEyePos, playerEyeForward, true )
+ }
+ else
+ {
+ expect vector( desiredPos )
+ DebugDrawCircle( desiredPos, Vector(0,0,0), 10, 128, 255, 128, true, interval )
+ DebugDrawText( desiredPos + Vector(0,0,60), "Looking here", false, interval )
+ pathNodeSearchPos = GetPathNodeSearchPosWithLookPos( playerOrg, playerEyePos, playerEyeForward, desiredPos, true )
+ }
+
+ DebugDrawCircle( pathNodeSearchPos, Vector(0,0,0), 10, 128, 128, 255, true, interval )
+ DebugDrawText( pathNodeSearchPos + Vector(0,0,40), "Searching from here", false, interval )
+
+ DebugDrawLine( playerOrg, playerOrg + AnglesToForward( Vector( 0, yaw - TITANDROP_MIN_FOV, 0 ) ) * 500, 200, 200, 200, true, interval )
+ DebugDrawLine( playerOrg, playerOrg + AnglesToForward( Vector( 0, yaw + TITANDROP_MIN_FOV, 0 ) ) * 500, 200, 200, 200, true, interval )
+ DebugDrawLine( playerOrg, playerOrg + AnglesToForward( Vector( 0, yaw - TITANDROP_MAX_FOV, 0 ) ) * 500, 128, 128, 128, true, interval )
+ DebugDrawLine( playerOrg, playerOrg + AnglesToForward( Vector( 0, yaw + TITANDROP_MAX_FOV, 0 ) ) * 500, 128, 128, 128, true, interval )
+
+ int node = GetBestNodeForPosInWedge( pathNodeSearchPos, playerEyePos, yaw, TITANDROP_MIN_FOV, TITANDROP_MAX_FOV, TITANDROP_FOV_PENALTY, dataIndex, /*ANALYSIS_STEPS*/ 8 )
+
+ if ( node >= 0 )
+ {
+ Assert( NodeHasFlightPath( dataIndex, node ) )
+
+ vector pos = GetNodePos( node )
+ DebugDrawCircle( pos, Vector(0,0,0), 25, 255, 255, 128, true, interval )
+ DebugDrawText( pos + Vector(0,0,20), "Best node", false, interval )
+ }
+
+ Point actualResult = GetTitanReplacementPoint( player, true )
+ vector actualPos = actualResult.origin
+ DebugDrawCircle( actualPos, Vector(0,0,0), 32, 255, 255, 255, true, interval )
+ DebugDrawLine( actualPos, actualPos + AnglesToForward( actualResult.angles ) * 40, 255, 255, 255, true, interval )
+ DebugDrawText( actualPos, "Final location", false, interval )
+
+ wait interval
+ }
+}
+
+Point function GetTitanReplacementPoint( entity player, bool forDebugging = false )
+{
+ vector playerEyePos = player.EyePosition()
+ vector playerEyeAngles = player.EyeAngles()
+ vector playerOrg = player.GetOrigin()
+
+ return CalculateTitanReplacementPoint( playerOrg, playerEyePos, playerEyeAngles, forDebugging )
+}
+
+Point function CalculateTitanReplacementPoint( vector playerOrg, vector playerEyePos, vector playerEyeAngles, bool forDebugging = false )
+{
+ //local playerEyePos = Vector(-281.036224, 34.857925, 860.031250)
+ //local playerEyeAngles = Vector(60.055622, 80.775780, 0.000000)
+ //local playerOrg = Vector(-281.036224, 34.857925, 800.031250)
+
+ if ( !forDebugging )
+ printt( "Requested replacement Titan from eye pos " + playerEyePos + " view angles " + playerEyeAngles + " player origin " + playerOrg + " map " + GetMapName() )
+
+ vector playerEyeForward = AnglesToForward( playerEyeAngles )
+
+ // use the flightPath to find a position
+ FlightPath flightPath = GetAnalysisForModel( GetFlightPathModel( "fp_titan_model" ), HOTDROP_TURBO_ANIM )
+ int dataIndex = GetAnalysisDataIndex( flightPath )
+
+ var dropPoint
+ vector ornull traceOrigin = GetReplacementTrace( playerEyePos, playerEyeForward )
+ bool traceOriginIsNull = traceOrigin == null
+
+ if ( !traceOriginIsNull )
+ {
+ expect vector( traceOrigin )
+
+ dropPoint = TitanHulldropSpawnpoint( flightPath, traceOrigin, 0 )
+ if ( dropPoint != null && !NearTitanfallBlocker( dropPoint ) )
+ {
+ expect vector( dropPoint )
+ if ( EdgeTraceDropPoint( dropPoint ) )
+ {
+ if ( SafeForTitanFall( dropPoint ) && TitanTestDropPoint( dropPoint, flightPath ) )
+ {
+ vector yawVec = playerEyePos - dropPoint
+ vector yawAngles = VectorToAngles( yawVec )
+ yawAngles.x = 0
+ yawAngles.z = 0
+ // add some randomness
+ yawAngles.y += RandomFloatRange( -60, 60 )
+ if ( yawAngles.y < 0 )
+ yawAngles.y += 360
+ else if ( yawAngles.y > 360 )
+ yawAngles.y -= 360
+
+ Point point
+ point.origin = dropPoint
+ point.angles = yawAngles
+ return point
+ }
+ }
+ }
+ }
+
+ vector pathNodeSearchPos
+ if ( !traceOriginIsNull )
+ {
+ expect vector( traceOrigin )
+ pathNodeSearchPos = GetPathNodeSearchPosWithLookPos( playerOrg, playerEyePos, playerEyeForward, traceOrigin, false )
+ }
+ else
+ {
+ pathNodeSearchPos = GetPathNodeSearchPos( playerOrg, playerEyePos, playerEyeForward, false )
+ }
+
+ int node = GetBestNodeForPosInWedge( pathNodeSearchPos, playerEyePos, playerEyeAngles.y, TITANDROP_MIN_FOV, TITANDROP_MAX_FOV, TITANDROP_FOV_PENALTY, dataIndex, /*ANALYSIS_STEPS*/ 8 )
+
+ if ( node < 0 )
+ {
+ // This won't ever happen on a map with any reasonably placed path nodes.
+ entity spawner = FindSpawnpoint_ForReplacementTitan( playerOrg )
+ Assert( spawner )
+ Point point
+ point.origin = spawner.GetOrigin()
+ return point
+ }
+
+ Assert( NodeHasFlightPath( dataIndex, node ) )
+
+ vector nodeOrigin = GetNodePos( node )
+ vector dir = nodeOrigin - playerEyePos
+ vector angles = VectorToAngles( dir )
+ float yaw = angles.y + 180
+
+ if ( yaw < 0 )
+ yaw += 360
+ else if ( yaw > 360 )
+ yaw -= 360
+
+ var yawResult = GetSpawnPoint_ClosestYaw( node, dataIndex, yaw, 360.0 )
+ Assert( yawResult != null )
+ yaw = expect float( yawResult )
+ Assert( yaw >= 0 )
+ Assert( yaw <= 360 )
+
+ Point point
+ point.origin = nodeOrigin
+ point.angles = Vector( 0, yaw, 0 )
+ return point
+}
+
+vector function GetPathNodeSearchPosWithLookPos( vector playerOrg, vector playerEyePos, vector playerEyeForward, vector playerLookPos, bool debug )
+{
+ float dist2DSqr = Distance2DSqr( playerOrg, playerLookPos )
+ if ( dist2DSqr > (TITANDROP_PATHNODESEARCH_EXACTDIST / TITANDROP_PATHNODESEARCH_DISTFRAC) * (TITANDROP_PATHNODESEARCH_EXACTDIST / TITANDROP_PATHNODESEARCH_DISTFRAC) )
+ {
+ return playerOrg + (playerLookPos - playerOrg) * TITANDROP_PATHNODESEARCH_DISTFRAC
+ }
+ else if ( dist2DSqr > TITANDROP_PATHNODESEARCH_EXACTDIST * TITANDROP_PATHNODESEARCH_EXACTDIST )
+ {
+ vector dir = Normalize( playerLookPos - playerOrg )
+ return playerOrg + dir * TITANDROP_PATHNODESEARCH_EXACTDIST
+ }
+ else
+ {
+ return playerLookPos
+ }
+
+ unreachable
+}
+
+vector function GetPathNodeSearchPos( vector playerOrg, vector playerEyePos, vector playerEyeForward, bool debug )
+{
+ vector diagonallyDown = Normalize( <playerEyeForward.x, playerEyeForward.y, 0> )
+ diagonallyDown.z = TITANDROP_GROUNDSEARCH_ZDIR
+
+ vector startPos = playerEyePos + playerEyeForward * TITANDROP_GROUNDSEARCH_FORWARDDIST
+ vector endPos = startPos + diagonallyDown * TITANDROP_GROUNDSEARCH_DIST
+
+ TraceResults result = TraceLine( startPos, endPos, null, TRACE_MASK_SOLID_BRUSHONLY, TRACE_COLLISION_GROUP_NONE )
+
+ if ( debug )
+ {
+ DebugDrawLine( playerEyePos, startPos, 128,128,200, true, 0.1 )
+ DebugDrawLine( startPos, result.endPos, 128,128,200, true, 0.1 )
+ if ( result.fraction < 1 )
+ DebugDrawLine( result.endPos, result.endPos + playerEyeForward * TITANDROP_FALLBACK_DIST, 128,128,200, true, 0.1 )
+ }
+
+ if ( result.fraction < 1 )
+ return result.endPos + playerEyeForward * TITANDROP_FALLBACK_DIST
+
+ return playerEyePos + playerEyeForward * TITANDROP_FALLBACK_DIST
+}
+
+// Returns a position vector or null
+vector ornull function GetReplacementTrace( vector startPos, vector viewVector )
+{
+ float viewDirLen2D = Length2D( viewVector )
+ if ( viewDirLen2D < 0.1 )
+ viewDirLen2D = 0.1
+
+ vector endPos = startPos + ( viewVector * ( TITANDROP_LOS_DIST / viewDirLen2D ) )
+ int mask = TRACE_MASK_SOLID & (~CONTENTS_WINDOW)
+ TraceResults result = TraceLine( startPos, endPos, null, mask, TRACE_COLLISION_GROUP_NONE )
+ //DebugDrawLine( result.endPos, endPos, 255, 0, 0, true, 20.0 )
+ //DebugDrawLine( startPos, result.endPos, 0, 255, 0, true, 20.0 )
+
+ if ( result.fraction == 1 )
+ return null
+
+ entity hitEnt = result.hitEnt
+ if ( IsValid( hitEnt ) && ( hitEnt.IsTitan() || hitEnt.IsPlayer() || hitEnt.IsNPC() ) )
+ {
+ endPos = OriginToGround( hitEnt.GetOrigin() )
+ }
+ else
+ {
+ endPos = result.endPos
+
+ if ( result.surfaceNormal.Dot( <0.0, 0.0, 1.0> ) < 0.7 )
+ {
+ //DebugDrawLine( endPos, Vector(0,0,0), 0, 200, 0, true, 5.0 )
+ // pull it back towards player
+ float titanRadius = GetBoundsMax( HULL_TITAN ).x * 1.2
+ endPos -= viewVector * titanRadius
+ endPos += result.surfaceNormal * titanRadius
+
+ endPos = OriginToGround( endPos )
+ }
+ }
+
+ vector ornull clampedEndPos = NavMesh_ClampPointForHullWithExtents( endPos, HULL_TITAN, <160.0, 160.0, 80.0> )
+
+ if ( !clampedEndPos )
+ return null
+
+ expect vector( clampedEndPos )
+
+ vector dir = clampedEndPos - startPos
+ if ( DotProduct2D( dir, viewVector ) < 0 )
+ return null
+
+ return clampedEndPos
+}
+
+var function HullTraceDropPoint( FlightPath flightPath, vector baseOrigin, float heightCapMax = 190 )
+{
+ float heightCapMin = -512
+ vector startOrigin = baseOrigin + Vector( 0,0,1000 )
+ vector endOrigin = baseOrigin + Vector( 0,0, heightCapMin )
+
+ int mask = flightPath.traceMask
+
+ TraceResults result = TraceHull( startOrigin, endOrigin, flightPath.mins, flightPath.maxs, null, mask, TRACE_COLLISION_GROUP_NONE )
+ //DebugDrawLine( startOrigin, result.endPos, 0, 255, 0, true, 5.0 )
+ //DebugDrawLine( result.endPos, endOrigin, 255, 0, 0, true, 5.0 )
+
+// DebugDrawLine( startOrigin, baseOrigin, 0, 255, 0, true, 5.0 )
+// DebugDrawLine( baseOrigin, endOrigin, 255, 0, 0, true, 5.0 )
+// local offset = Vector(0.15, 0.15, 0.0 )
+// DebugDrawLine( startOrigin + offset, result.endPos + offset, 0, 255, 0, true, 5.0 )
+// DebugDrawLine( result.endPos + offset, endOrigin + offset, 255, 0, 0, true, 5.0 )
+// DrawArrow( baseOrigin, Vector(0,0,0), 5.0, 50 )
+// DebugDrawLine( result.endPos, baseOrigin, 255, 255, 255, true, 4.5 )
+
+/*
+ printt( " " )
+ printt( "Hull drop " )
+ printt( "start " + startOrigin )
+ printt( "end " + endOrigin )
+ printt( "hit " + result.endPos )
+ printt( "mins " + flightPath.mins + " maxs " + flightPath.maxs )
+ printt( "mask " + mask )
+*/
+ if ( result.allSolid || result.startSolid || result.hitSky )
+ return null
+
+ if ( result.fraction == 0 || result.fraction == 1 )
+ return null
+
+ if ( fabs( result.endPos.z - baseOrigin.z ) > heightCapMax )
+ return null
+
+ return result.endPos
+}
+
+
+entity function FindSpawnpoint_ForReplacementTitan( vector origin )
+{
+ Assert( GetScriptManagedEntArrayLen( file.replacementSpawnpointsID ) > 0 )
+
+ array<entity> spawnpoints = GetScriptManagedEntArray( file.replacementSpawnpointsID )
+ entity selectedSpawnpoint = spawnpoints[0]
+
+ float closestDist = -1
+ foreach ( spawnpoint in spawnpoints )
+ {
+ if ( spawnpoint.e.spawnPointInUse )
+ continue
+ if ( spawnpoint.IsOccupied() )
+ continue
+
+ float dist = DistanceSqr( spawnpoint.GetOrigin(), origin )
+ if ( closestDist == -1 || dist < closestDist )
+ {
+ closestDist = dist
+ selectedSpawnpoint = spawnpoint
+ }
+
+ }
+
+ Assert( selectedSpawnpoint )
+ return selectedSpawnpoint
+}
+
+bool function TitanFindDropNodes( FlightPath flightPath, vector baseOrigin, float yaw )
+{
+// return TitanFindDropNodesReloadable( flightPath, baseOrigin, yaw )
+//}
+//function TitanFindDropNodesReloadable( flightPath, baseOrigin, yaw )
+//{
+ if ( NearTitanfallBlocker( baseOrigin ) )
+ return false
+
+ asset model = flightPath.model
+ string animation = flightPath.anim
+ //local flightPath = GetAnalysisForModel( model, animation )
+
+ vector origin = baseOrigin
+ vector angles = Vector(0,yaw,0)
+ //entity titan = CreatePropDynamic( model, origin, Vector(0,0,0) )
+ //entity titan = CreateNPCTitanFromSettings( "titan_atlas", TEAM_IMC, origin, angles )
+
+ entity titan = expect entity( level.ainTestTitan )
+
+ titan.SetModel( model )
+ titan.SetAngles( angles )
+ titan.SetOrigin( origin )
+
+ float impactTime = GetHotDropImpactTime( titan, animation )
+ Attachment result = titan.Anim_GetAttachmentAtTime( animation, "OFFSET", impactTime )
+ vector maxs = titan.GetBoundingMaxs()
+ vector mins = titan.GetBoundingMins()
+ int mask = titan.GetPhysicsSolidMask()
+ origin = ModifyOriginForDrop( origin, mins, maxs, result.position, mask )
+ titan.SetOrigin( origin )
+
+ // Don't use nodes on top of the roof in kodai
+ if ( GetMapName() == "mp_forwardbase_kodai" && origin.z > 1200 )
+ return false
+
+ if ( !TitanTestDropPoint( origin, flightPath ) )
+ return false
+
+ if ( !TitanCanStand( titan ) )
+ return false
+
+ if ( TitanHulldropSpawnpoint( flightPath, origin, 0 ) == null )
+ return false
+
+ if ( !EdgeTraceDropPoint( origin ) )
+ return false
+
+ return true
+}
+
+
+var function TitanHulldropSpawnpoint( FlightPath flightPath, vector origin, float _ )
+{
+ return HullTraceDropPoint( flightPath, origin, 20 )
+}
+
+
diff --git a/Northstar.CustomServers/scripts/vscripts/titan/_titan_commands.gnut b/Northstar.CustomServers/scripts/vscripts/titan/_titan_commands.gnut
new file mode 100644
index 00000000..06232c08
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/titan/_titan_commands.gnut
@@ -0,0 +1,49 @@
+untyped
+
+global function TitanCommands_Init
+
+
+function TitanCommands_Init()
+{
+ if ( GetCurrentPlaylistVarInt( "titan_move_command_enabled", 0 ) == 0 )
+ return
+
+ AddClientCommandCallback( "PrototypeOrderTitanMove", Prototype_OrderTitanMove )
+ RegisterSignal( "Prototype_TitanMove" )
+}
+
+bool function Prototype_OrderTitanMove( entity player, array<string> args )
+{
+ Assert( args.len() == 3 )
+ vector pos = Vector( args[0].tofloat(), args[1].tofloat(), args[2].tofloat() )
+
+ DebugDrawLine( pos, pos + Vector(0,0,500), 255, 0, 0, true, 5.0 )
+ entity titan = player.GetPetTitan()
+ if ( !IsAlive( titan ) )
+ return true
+
+ thread Prototype_TitanMove( player, titan, pos )
+
+ return true
+}
+
+void function Prototype_TitanMove( entity player, entity titan, vector origin )
+{
+ titan.Signal( "Prototype_TitanMove" )
+ titan.EndSignal( "Prototype_TitanMove" )
+ titan.EndSignal( "ChangedTitanMode" )
+ titan.EndSignal( "OnDeath" )
+ local mode = player.GetPetTitanMode()
+ if ( mode != eNPCTitanMode.STAY ) // assuming there are only 2 modes
+ {
+ player.SetPetTitanMode( eNPCTitanMode.STAY )
+ titan.DisableBehavior( "Follow" )
+ #if R1_VGUI_MINIMAP
+ titan.Minimap_SetBossPlayerMaterial( $"vgui/HUD/threathud_titan_friendlyself_guard" )
+ #endif
+
+ titan.AssaultSetOrigin( origin )
+ }
+
+ AssaultOrigin( titan, origin, 100 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/titan/_titan_health.gnut b/Northstar.CustomServers/scripts/vscripts/titan/_titan_health.gnut
new file mode 100644
index 00000000..d600cb03
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/titan/_titan_health.gnut
@@ -0,0 +1,1072 @@
+global function TitanHealth_Init
+
+global function Titan_PlayerTookDamage
+global function Titan_NPCTookDamage
+
+global function GetShieldRegenTime
+global function GetShieldRegenDelay
+global function PlayerHasAutoEject
+global function SetTitanCoreTimer
+global function GetTitanCoreTimer
+
+global function AddCreditToTitanCoreBuilderForTitanDamageInflicted
+global function AddCreditToTitanCoreBuilderForTitanDamageReceived
+global function AddCreditToTitanCoreBuilderForDoomInflicted
+global function AddCreditToTitanCoreBuilderForDoomEntered
+global function AddCreditToTitanCoreBuilder
+
+global function TitanShieldRegenThink
+
+global function IsRodeoDamageFromBatteryPack
+global function IsKillshot
+
+global function DoomedHealthThink
+global function UndoomTitan
+global function RestoreTitan
+
+global const SIGNAL_TITAN_HEALTH_REGEN = "BeginTitanHealthRegen"
+global const SIGNAL_TITAN_SHIELD_REGEN = "BeginTitanShieldRegen"
+
+global const TITAN_HEALTH_REGEN_DELAY_MAX = 0.7 // 2.2
+
+#if MP
+// PROTO : Was 99, 49 is for test
+global const TITAN_REGEN_MIN_DAMAGE = 49
+global const TITAN_REGEN_MIN_DAMAGE_DELAY = 0.5
+#elseif SP
+global const TITAN_REGEN_MIN_DAMAGE = 70
+global const TITAN_REGEN_MIN_DAMAGE_DELAY = 0.5
+#endif
+
+// titan health system
+const TITAN_HEALTH_HISTORY_FALLOFF_START = 0 // how many seconds until shield begins to regen
+
+const float TITAN_HEALTH_HISTORY_FALLOFF_END = 4.0
+
+struct
+{
+ float earn_meter_titan_multiplier
+} file
+
+void function TitanHealth_Init()
+{
+ RegisterSignal( SIGNAL_TITAN_HEALTH_REGEN )
+ RegisterSignal( SIGNAL_TITAN_SHIELD_REGEN )
+ RegisterSignal( "Doomed" )
+ RegisterSignal( "TitanUnDoomed" )
+ RegisterSignal( "StopShieldRegen" )
+ RegisterSignal( "WeakTitanHealthInitialized" )
+
+ file.earn_meter_titan_multiplier = GetCurrentPlaylistVarFloat( "earn_meter_titan_multiplier", 1.0 )
+
+ if ( IsMenuLevel() )
+ return
+
+ HealthRegenInit()
+ AddSoulInitFunc( TitanShieldRegenThink ) //This runs even if playlist var titan_shield_regen is set to 0 because it also does stuff like give friendly Pilots protection with shield, etc
+ AddSoulDeathCallback( Titan_MonarchCleanup )
+}
+
+void function UndoomTitan( entity titan, int numSegments )
+{
+ entity soul = titan.GetTitanSoul()
+ string settings = GetSoulPlayerSettings( soul )
+
+ soul.DisableDoomed()
+ int maxHealth
+ int segmentHealth = GetSegmentHealthForTitan( titan )
+ if ( titan.IsNPC() )
+ {
+ maxHealth = int( GetPlayerSettingsFieldForClassName_Health( settings ) )
+ if ( titan.ai.titanSpawnLoadout.setFileMods.contains( "fd_health_upgrade" ) )
+ maxHealth += segmentHealth
+ if ( soul.soul.titanLoadout.setFileMods.contains( "core_health_upgrade" ) )
+ maxHealth += segmentHealth
+ }
+ else
+ {
+ maxHealth = int( titan.GetPlayerModHealth() )
+ }
+ titan.SetMaxHealth( maxHealth )
+ titan.SetHealth( segmentHealth * numSegments )
+ SetSoulBatteryCount( soul, numSegments )
+
+ titan.Signal( "TitanUnDoomed" )
+ UndoomTitan_Body( titan )
+ thread TitanShieldRegenThink( soul )
+}
+
+void function RestoreTitan( entity titan, float percent = 0.625 )
+{
+ entity soul = titan.GetTitanSoul()
+ if ( soul.IsDoomed() )
+ UndoomTitan( titan, 1 )
+
+ soul.nextRegenTime = 0.0
+ soul.SetShieldHealth( soul.GetShieldHealthMax() )
+ int minHealth = int( titan.GetMaxHealth() * percent )
+ if ( titan.GetHealth() < minHealth )
+ {
+ titan.SetHealth( minHealth )
+ int segmentHealth = GetSegmentHealthForTitan( titan )
+ int segments = int( minHealth / float( segmentHealth ) )
+ SetSoulBatteryCount( soul, segments )
+ }
+}
+
+bool function IsRodeoDamage( entity soul, var damageInfo )
+{
+ entity titan = soul.GetTitan()
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !attacker.IsPlayer() )
+ {
+ entity rider = GetRodeoPilot( titan )
+ if ( rider == attacker )
+ return true
+ else
+ return false
+ }
+
+ if ( attacker.GetTitanSoulBeingRodeoed() != soul )
+ return false
+
+ return true
+}
+
+bool function IsCoopRodeoDamage( entity soul, var damageInfo )
+{
+ entity titan = soul.GetTitan()
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ entity rider = GetRodeoPilot( titan )
+ if ( rider == attacker )
+ return true
+ else
+ return false
+
+ unreachable
+}
+
+
+void function CheckRodeoRiderHitsTitan( entity soul, var damageInfo )
+{
+ if ( IsRodeoDamage( soul, damageInfo ) )
+ {
+ //Set Last Attack Time so warning is triggered
+ soul.SetLastRodeoHitTime( Time() )
+
+ DamageInfo_AddCustomDamageType( damageInfo, DF_RODEO )
+ }
+}
+
+bool function ShouldMultiplyRodeoDamage( var damageInfo )
+{
+ switch ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) )
+ {
+ case eDamageSourceId.mp_weapon_smr:
+ case eDamageSourceId.mp_titanability_smoke:
+ return false
+
+ case eDamageSourceId.mp_weapon_defender :
+ return true
+ }
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_EXPLOSION )
+ return false
+
+ return true
+}
+
+bool function IsRodeoDamageFromBatteryPack( entity soul, var damageInfo )
+{
+ if ( !IsRodeoDamage( soul, damageInfo ) )
+ return false
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) != damageTypes.rodeoBatteryRemoval )
+ return false
+
+ return true
+}
+
+
+int function ShieldHealthUpdate( entity titan, var damageInfo, bool critHit )
+{
+ entity soul = titan.GetTitanSoul()
+ if ( DamageInfo_GetForceKill( damageInfo ) )
+ {
+ soul.SetShieldHealth( 0 )
+ return 0
+ }
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_BYPASS_SHIELD )
+ return 0
+
+ float damage = DamageInfo_GetDamage( damageInfo )
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+
+ Assert( soul == titan.GetTitanSoul() )
+ int shieldHealth = soul.GetShieldHealth()
+
+ if ( soul.e.forcedRegenTime <= Time() )
+ soul.nextRegenTime = CalculateNextRegenTime( damage, damageType, critHit, expect float( soul.nextRegenTime ), GetShieldRegenDelay( soul ) )
+
+ int result = 0
+ if ( shieldHealth )
+ {
+ DamageInfo_AddCustomDamageType( damageInfo, DF_SHIELD_DAMAGE )
+ result = int( ShieldModifyDamage( titan, damageInfo ) )
+ }
+ else
+ {
+ TakeAwayFriendlyRodeoPlayerProtection( titan )
+ }
+
+ return result
+}
+
+
+void function PlayerOrNPCTitanTookDamage( entity titan, var damageInfo, bool critHit, TitanDamage titanDamage )
+{
+ entity soul = titan.GetTitanSoul()
+
+ if ( !IsValid( soul ) ) //Defensive fix for transient times in frame where Titan can have no soul but be damaged, e.g. immediately after embark
+ return
+
+ // zero out small forces
+ if ( LengthSqr( DamageInfo_GetDamageForce( damageInfo ) ) < 30000 * 30000 )
+ DamageInfo_SetDamageForce( damageInfo, < 0, 0, 0 > )
+
+ titanDamage.shieldDamage = CheckSpecialCaseShieldDamage( soul, titan, damageInfo )
+ if ( titanDamage.shieldDamage < 0 )
+ {
+ CheckRodeoRiderHitsTitan( soul, damageInfo )
+ titanDamage.shieldDamage = ShieldHealthUpdate( titan, damageInfo, critHit )
+ }
+
+ HandleKillshot( titan, damageInfo, titanDamage )
+
+ // health regen based on how much damage dealt to titan
+ float damage = DamageInfo_GetDamage( damageInfo )
+ int damageType = DamageInfo_GetCustomDamageType( damageInfo )
+ bool rodeoDamage = ( ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_RODEO ) > 0 )
+
+ if ( soul.e.forcedRegenTime <= Time() )
+ soul.nextHealthRegenTime = CalculateNextRegenTime( damage, damageType, critHit || rodeoDamage, expect float( soul.nextHealthRegenTime ), GetHealthRegenDelay( soul ) )
+}
+
+int function CheckSpecialCaseShieldDamage( entity soul, entity titan, var damageInfo )
+{
+ if ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) == damagedef_suicide )
+ return 0
+
+ // no protection from doomed health loss
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return 0
+
+ if ( IsTitanWithinBubbleShield( titan ) || TitanHasBubbleShieldWeapon( titan ) )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return 0
+ }
+
+ return -1
+}
+
+void function Titan_NPCTookDamage( entity titan, var damageInfo, TitanDamage titanDamage )
+{
+ Assert( titan.IsTitan() )
+ Assert( DamageInfo_GetDamage( damageInfo ) > 0 )
+
+ // dead entities can take damage
+ if ( !IsAlive( titan ) )
+ return
+
+ entity soul = titan.GetTitanSoul()
+
+ if ( !IsValid( soul ) ) //Defensive fix for transient times in frame where Titan can have no soul but be damaged, e.g. immediately after embark
+ return
+
+ bool critHit = false
+ if ( CritWeaponInDamageInfo( damageInfo ) )
+ critHit = IsCriticalHit( DamageInfo_GetAttacker( damageInfo ), titan, DamageInfo_GetHitBox( damageInfo ), DamageInfo_GetDamage( damageInfo ), DamageInfo_GetDamageType( damageInfo ) )
+
+ if ( critHit )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_CRITICAL )
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( HeavyArmorCriticalHitRequired( damageInfo ) && CritWeaponInDamageInfo( damageInfo ) && !critHit && IsValid( attacker ) && !attacker.IsTitan())
+ {
+ float shieldHealth = float( titan.GetTitanSoul().GetShieldHealth() )
+ float damage = DamageInfo_GetDamage( damageInfo )
+ if ( shieldHealth - damage <= 0 )
+ {
+ if ( shieldHealth > 0 )
+ DamageInfo_SetDamage( damageInfo, shieldHealth )
+ else
+ DamageInfo_SetDamage( damageInfo, 0 )
+ }
+ }
+
+ PlayerOrNPCTitanTookDamage( titan, damageInfo, critHit, titanDamage )
+
+ RecordDamageToNPCTitanSoul( soul, damageInfo )
+
+ entity owner = GetPetTitanOwner( titan )
+ if ( IsValid( owner ) )
+ AutoTitan_TryMultipleTitanCallout( titan, damageInfo )
+
+ if ( GetDoomedState( titan ) )
+ titanDamage.shieldDamage = 0
+}
+
+void function Titan_PlayerTookDamage( entity player, var damageInfo, entity attacker, bool critHit, TitanDamage titanDamage )
+{
+ Assert( player.IsTitan() )
+
+ float damage = DamageInfo_GetDamage( damageInfo )
+
+ if ( !IsAlive( player ) )
+ return
+
+ entity soul = player.GetTitanSoul()
+ if ( !IsValid( soul ) ) //Defensive fix for transient times in frame where Titan can have no soul but be damaged, e.g. immediately after embark
+ return
+
+ if ( damage > 0 )
+ AdjustVelocityFromHit( player, damageInfo, attacker, damage, critHit )
+
+ if ( IsDemigod( player ) )
+ EntityDemigod_TryAdjustDamageInfo( player, damageInfo )
+
+ bool critHit = false
+ if ( CritWeaponInDamageInfo( damageInfo ) )
+ critHit = IsCriticalHit( attacker, player, DamageInfo_GetHitBox( damageInfo ), DamageInfo_GetDamage( damageInfo ), DamageInfo_GetDamageType( damageInfo ) )
+
+ if ( critHit )
+ DamageInfo_AddCustomDamageType( damageInfo, DF_CRITICAL )
+
+ #if MP
+ if ( HeavyArmorCriticalHitRequired( damageInfo ) && CritWeaponInDamageInfo( damageInfo ) && !critHit && IsValid( attacker ) && !attacker.IsTitan())
+ {
+ float shieldHealth = float( player.GetTitanSoul().GetShieldHealth() )
+ if ( shieldHealth - damage <= 0 )
+ {
+ if ( shieldHealth > 0 )
+ DamageInfo_SetDamage( damageInfo, shieldHealth )
+ else
+ DamageInfo_SetDamage( damageInfo, 0 )
+ }
+ }
+ #endif
+
+ PlayerOrNPCTitanTookDamage( player, damageInfo, critHit, titanDamage )
+}
+
+bool function IsKillshot( entity ent, var damageInfo, entity titanSoul )
+{
+ float damage = DamageInfo_GetDamage( damageInfo )
+ int health = ent.GetHealth()
+
+ if ( health - damage > DOOMED_MIN_HEALTH )
+ return false
+
+ return true
+}
+
+bool function ShouldDoomTitan( entity ent, var damageInfo )
+{
+ if ( DoomStateDisabled() )
+ return false
+
+ if ( GetDoomedState( ent ) )
+ return false
+
+ if ( DamageInfo_GetForceKill( damageInfo ) )
+ return false
+
+ float doomedHealth = GetTitanSoulDoomedHealth( ent.GetTitanSoul() )
+ if ( doomedHealth <= 0 )
+ return false
+
+ entity soul = ent.GetTitanSoul()
+ if ( soul.soul.skipDoomState )
+ return false
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_SKIP_DAMAGE_PROT )
+ return doomedHealth > ( DamageInfo_GetDamage( damageInfo ) - ent.GetHealth() )
+
+ bool skipDoom = ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_SKIPS_DOOMED_STATE ) > 0
+ return !skipDoom
+}
+
+bool function HandleKillshot( entity ent, var damageInfo, TitanDamage titanDamage )
+{
+ #if NPC_TITAN_PILOT_PROTOTYPE
+ if ( TitanHasNpcPilot( ent ) ) //an npc titan that was dropped by an npc human
+ {
+ float damage = DamageInfo_GetDamage( damageInfo )
+ int health = ent.GetHealth()
+
+ if ( health - damage <= 0 )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ thread TitanEjectPlayer( ent )
+ }
+
+ return
+ }
+ #endif
+
+ if ( ent.IsPlayer() && ent.IsBuddhaMode() )
+ return false
+
+ entity titanSoul = ent.GetTitanSoul()
+
+ if ( IsKillshot( ent, damageInfo, titanSoul ) )
+ {
+ entity boss = titanSoul.GetBossPlayer()
+ Soul_SetLastAttackInfo( titanSoul, damageInfo )
+
+ if ( ShouldDoomTitan( ent, damageInfo ) )
+ {
+ // Added via AddCallback_OnTitanDoomed
+ foreach ( callbackFunc in svGlobal.onTitanDoomedCallbacks )
+ {
+ callbackFunc( ent, damageInfo )
+ }
+
+ if ( IsMultiplayer() )
+ {
+ entity attacker = expect entity( expect table( titanSoul.lastAttackInfo ).attacker )
+ if ( IsValid( attacker ) )
+ {
+ entity bossPlayer = attacker.GetBossPlayer()
+ if ( attacker.IsNPC() && IsValid( bossPlayer ) )
+ attacker = bossPlayer
+
+ if ( attacker.IsPlayer() )
+ ScoreEvent_TitanDoomed( ent, attacker, damageInfo )
+ }
+ }
+
+ thread DoomedHealthThink( titanSoul, damageInfo )
+
+ titanDamage.doomedNow = true
+ titanDamage.doomedDamage = int( DamageInfo_GetDamage( damageInfo ) )
+
+ int health = ent.GetHealth()
+ DamageInfo_SetDamage( damageInfo, health - 1 )
+ return true
+ }
+ else
+ {
+ // handle auto eject here
+ if ( ent.IsPlayer() && PlayerHasAutoEject( ent ) )
+ {
+ int health = ent.GetHealth()
+ DamageInfo_SetDamage( damageInfo, health - 1 )
+ thread HandleAutoEject( ent, titanSoul )
+ return false
+ }
+ }
+ }
+
+ // Handle doom state damage
+ if ( GetDoomedState( ent ) )
+ {
+ // as long as we're dying but not yet ejecting, the last player to damage us gets credit
+ if ( titanSoul.IsEjecting() )
+ {
+ Soul_SetLastAttackInfo( titanSoul, damageInfo )
+ }
+ else if ( ent.IsPlayer() && PlayerHasAutoEject( ent ) ) //Handle auto eject for when the frame in which Titan became doomed was not valid for ejecting, e.g. melee
+ {
+ int health = ent.GetHealth()
+ DamageInfo_SetDamage( damageInfo, health - 1 )
+ thread HandleAutoEject( ent, titanSoul )
+ return false
+ }
+
+ // protect players who eject early
+ // if ( ent.IsPlayer() && IsEjectProtected( ent, damageInfo ) )
+ // DamageInfo_SetDamage( damageInfo, 0 )
+
+ // slight protection to prevent multiple rapid damage events from eating through doomed state health
+ if ( Time() - titanSoul.soul.doomedStartTime < TITAN_DOOMED_INVUL_TIME && !DamageInfo_GetForceKill( damageInfo ) )
+ DamageInfo_SetDamage( damageInfo, 0 )
+ }
+ else
+ {
+ Soul_SetLastAttackInfo( titanSoul, damageInfo )
+ }
+
+ return false
+}
+
+bool function PlayerHasAutoEject( entity player )
+{
+ if ( player.IsBot() )
+ return false
+
+ if ( !PlayerHasPassive( player, ePassives.PAS_AUTO_EJECT ) )
+ return false
+
+ return true
+}
+
+
+void function AdjustVelocityFromHit( entity player, var damageInfo, entity attacker, float damage, bool critHit )
+{
+/*
+ if ( DamageInfo_GetDamageCriticalHitScale( damageInfo ) > 1.0 )
+ {
+ // if you can crit, you have to crit!
+ if ( !critHit )
+ return
+ }
+*/
+
+ //printt( " " )
+ //printt( "damage: " + damage )
+
+ vector damageForward = DamageInfo_GetDamageForce( damageInfo )
+ damageForward.z = 0
+ //printt( "damageForward " + damageForward )
+
+ damageForward.Norm()
+
+ //vector org = DamageInfo_GetDamagePosition( damageInfo )
+ //DebugDrawLine( org, org + damageForward * 250, 255, 0, 0, true, 5.0 )
+
+ vector velocity = player.GetVelocity()
+ vector velForward = player.GetVelocity()
+ velForward.z = 0
+ velForward.Norm()
+
+ //DebugDrawLine( org, org + velForward * 250, 0, 255, 0, true, 5.0 )
+
+ float dot = DotProduct( velForward, damageForward )
+
+ // only stop from the ~front cone
+ if ( dot >= -0.5 )
+ return
+
+ float speedPercent
+
+ switch ( DamageInfo_GetDamageSourceIdentifier( damageInfo ) )
+ {
+ //case eDamageSourceId.mp_titanweapon_40mm:
+ // speedPercent = GraphCapped( damage, 0, 750, 1, 0 )
+ // break
+
+ case eDamageSourceId.mp_titanweapon_xo16:
+ speedPercent = 0.075
+ break
+
+ default:
+ speedPercent = GraphCapped( damage, 0, 2500, 0, 1.0 )
+ }
+
+ //float dif = GraphCapped( dot, -1, -0.5, 1, 0 )
+ //speedPercent = speedPercent * dif + ( 1.0 - dif )
+
+ speedPercent *= GraphCapped( dot, -1.0, -0.5, 1, 0 )
+
+ //printt( " " )
+ //printt( "Damage: " + damage )
+ //printt( "dot: " + dot )
+ //printt( "speedPercent: " + speedPercent )
+ speedPercent = 1.0 - speedPercent
+ // make the dot into a tighter range
+ //dot += 0.5
+ //dot *= -2.0
+
+ //printt( "modifier: " + ( speedPercent ) )
+ velocity *= ( speedPercent )
+ player.SetVelocity( velocity )
+}
+
+
+
+void function DoomedHealthThink( entity titanSoul, var damageInfo )
+{
+ Assert( expect table( titanSoul.lastAttackInfo ).attacker, "Player entered reserve health with no attacker" )
+
+ entity soulOwner = titanSoul.GetTitan()
+ Assert( IsValid( soulOwner ), "Invalid owner " + soulOwner )
+
+ titanSoul.soul.doomedStartTime = Time()
+
+ // kill any existing health regen thread
+ titanSoul.Signal( SIGNAL_TITAN_HEALTH_REGEN )
+ titanSoul.Signal( SIGNAL_TITAN_SHIELD_REGEN )
+
+ titanSoul.EndSignal( "OnDestroy" )
+ titanSoul.EndSignal( "OnTitanDeath" )
+
+ float tickRate = 0.15
+ float maxDoomedHealth = GetTitanSoulDoomedHealth( titanSoul )
+ float doomedHealth = maxDoomedHealth
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_SKIP_DAMAGE_PROT )
+ doomedHealth = min( doomedHealth + soulOwner.GetHealth() - DamageInfo_GetDamage( damageInfo ), doomedHealth )
+
+ float DPS = (doomedHealth / TITAN_DOOMED_MAX_DURATION )
+
+ titanSoul.EnableDoomed()
+ titanSoul.doomedTime = Time()
+ soulOwner.SetDoomed()
+ DoomTitan( soulOwner )
+ soulOwner.Signal( "Doomed" )
+ titanSoul.Signal( "Doomed" )
+
+ // allow the damage to go through before resetting the health, so that we get proper damage indicators, etc...
+ // this process should also be in code
+ WaitEndFrame()
+
+ // grab the soul owner again since there was a wait
+ soulOwner = titanSoul.GetTitan()
+ if ( !IsValid( soulOwner ) )
+ return
+
+ if ( PROTO_AlternateDoomedState() )
+ {
+ //printt( soulOwner.GetHealth() )
+ soulOwner.SetHealth( doomedHealth )
+ soulOwner.SetMaxHealth( maxDoomedHealth )
+ //soulOwner.SetHealthPerSegment( 0 )
+
+ soulOwner.ClearDoomed()
+
+ if ( soulOwner.IsPlayer() && PlayerHasAutoEject( soulOwner ) )
+ {
+ HandleAutoEject( soulOwner, titanSoul )
+ }
+ else
+ {
+ //If it's an auto-titan with auto-eject, this just instantly kills it.
+ var attacker = ( "attacker" in titanSoul.lastAttackInfo ) ? expect table( titanSoul.lastAttackInfo ).attacker : null
+ expect entity( attacker )
+ var inflictor = ( "inflictor" in titanSoul.lastAttackInfo ) ? expect table( titanSoul.lastAttackInfo ).inflictor : null
+ expect entity( inflictor )
+ var damageSource = ( "damageSourceId" in titanSoul.lastAttackInfo ) ? expect table( titanSoul.lastAttackInfo ).damageSourceId : -1
+ int damageFlags = expect int( expect table( titanSoul.lastAttackInfo ).scriptType )
+ if ( SoulHasPassive( titanSoul, ePassives.PAS_AUTO_EJECT ) )
+ {
+ int scriptDamageType = damageTypes.titanEjectExplosion | damageFlags
+ soulOwner.Die( attacker, inflictor, { scriptType = scriptDamageType, damageSourceId = damageSource } )
+ }
+ }
+ return
+ }
+ soulOwner.SetHealth( doomedHealth )
+ soulOwner.SetMaxHealth( maxDoomedHealth )
+ //soulOwner.SetHealthPerSegment( 0 )
+
+ string settings = GetSoulPlayerSettings( titanSoul )
+ float damageMod = 1.0
+ while ( true )
+ {
+ table lastAttackInfo = expect table( titanSoul.lastAttackInfo )
+
+ table extraDeathInfo = {}
+ extraDeathInfo.scriptType <- (DF_NO_INDICATOR | DF_DOOMED_HEALTH_LOSS)
+ if ( expect int( lastAttackInfo.scriptType ) & DF_BURN_CARD_WEAPON )
+ extraDeathInfo.scriptType = expect int( extraDeathInfo.scriptType ) | DF_BURN_CARD_WEAPON
+ if ( expect int( lastAttackInfo.scriptType ) & DF_VORTEX_REFIRE )
+ extraDeathInfo.scriptType = expect int( extraDeathInfo.scriptType ) | DF_VORTEX_REFIRE
+
+ extraDeathInfo.damageSourceId <- lastAttackInfo.damageSourceId
+
+ entity soulOwner = titanSoul.GetTitan()
+ if ( !IsValid( soulOwner ) )
+ return
+ if ( soulOwner.IsPlayer() )
+ {
+ //if ( PlayerHasPassive( soulOwner, ePassives.PAS_DOOMED_TIME ) )
+ // damageMod = 0.4
+ //else
+ // damageMod = 1.0
+
+ if ( PlayerHasAutoEject( soulOwner ) )
+ {
+ //printt( "About to Auto Eject" )
+ // do it in the loop cause player could somehow get in a titan in doomed state
+ HandleAutoEject( soulOwner, titanSoul )
+ }
+ }
+
+ float dmgAmount = DPS * tickRate * damageMod
+
+ soulOwner.TakeDamage( dmgAmount, expect entity( lastAttackInfo.attacker ), expect entity( lastAttackInfo.inflictor ), extraDeathInfo )
+
+ wait tickRate
+ }
+}
+
+void function HandleAutoEject( entity rider, entity soul )
+{
+ soul.EndSignal( "OnDestroy" )
+ soul.EndSignal( "OnTitanDeath" )
+
+ thread TitanEjectPlayer( rider )
+ if ( soul.IsEjecting() )
+ {
+ // so we don't cloak the titan during the ejection animation
+ if ( GetNuclearPayload( rider ) > 0 )
+ wait 2.0
+ else
+ wait 1.0
+
+ EnableCloak( rider, 7.0 )
+ return
+ }
+}
+
+void function TitanShieldRegenThink( entity soul )
+{
+ thread TitanShieldRegenThink_Internal( soul )
+}
+
+// HACK: this technically doesn't work properly because server framerate and all that jazz. Should really be in code.
+void function TitanShieldRegenThink_Internal( entity soul )
+{
+ soul.EndSignal( "OnDestroy" )
+ soul.EndSignal( "Doomed" )
+ soul.EndSignal( "StopShieldRegen" )
+
+ //Shield starts at 0 health for now
+ string settings = GetSoulPlayerSettings( soul )
+ bool hasShield = Dev_GetPlayerSettingByKeyField_Global( settings, "start_with_shields" ) == 1
+
+ if ( !hasShield )
+ soul.SetShieldHealth( 0 )
+
+ int lastShieldHealth = soul.GetShieldHealth()
+ bool shieldHealthSound = false
+ int maxShield = soul.GetShieldHealthMax()
+ float lastTime = Time()
+
+ while ( true )
+ {
+ entity titan = soul.GetTitan()
+ if ( !IsValid( titan ) )
+ return
+
+ int shieldHealth = soul.GetShieldHealth()
+ Assert( titan )
+
+ if ( lastShieldHealth <= 0 && shieldHealth && titan.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( titan, titan, "titan_energyshield_up_1P" )
+ shieldHealthSound = true
+ if ( titan.IsTitan() )
+ {
+ GiveFriendlyRodeoPlayerProtection( titan )
+ }
+ else
+ {
+ if ( titan.IsPlayer() )
+ {
+ printt( "Player was " + titan.GetPlayerSettings() )
+ }
+
+ printt( "ERROR! Expected Titan, but got " + titan )
+ }
+ }
+ else if ( shieldHealthSound && shieldHealth == soul.GetShieldHealthMax() )
+ {
+ shieldHealthSound = false
+ }
+ else if ( lastShieldHealth > shieldHealth && shieldHealthSound )
+ {
+ StopSoundOnEntity( titan, "titan_energyshield_up_1P" )
+ shieldHealthSound = false
+ }
+
+ if ( Time() >= soul.nextRegenTime && TitanHasRegenningShield( soul ) )
+ {
+ float shieldRegenRate = maxShield / ( GetShieldRegenTime( soul ) / SHIELD_REGEN_TICK_TIME )
+
+ if ( SoulHasPassive( soul, ePassives.PAS_SHIELD_BOOST ) )
+ shieldRegenRate = SHIELD_BEACON_REGEN_RATE
+
+ float frameTime = max( 0.0, Time() - lastTime )
+ shieldRegenRate = shieldRegenRate * frameTime / SHIELD_REGEN_TICK_TIME
+ // Faster shield recharge if we have Fusion Core active ability ( Stryder Signature )
+ //if ( titan.IsPlayer() && PlayerHasPassive( titan, ePassives.PAS_FUSION_CORE ) )
+ // shieldRegenRate *= 1.25
+
+ soul.SetShieldHealth( minint( soul.GetShieldHealthMax(), int( shieldHealth + shieldRegenRate ) ) )
+ }
+
+ lastShieldHealth = shieldHealth
+ lastTime = Time()
+ WaitFrame()
+ }
+}
+
+float function GetShieldRegenTime( entity soul )
+{
+ float time
+ if ( SoulHasPassive( soul, ePassives.PAS_SHIELD_REGEN ) )
+ time = TITAN_SHIELD_REGEN_TIME * 0.5
+ else
+ time = TITAN_SHIELD_REGEN_TIME
+
+ return time
+}
+
+float function GetHealthRegenDelay( entity soul )
+{
+ if ( GetDoomedState( soul.GetTitan() ) )
+ return TITAN_DOOMED_REGEN_DELAY
+
+ return GetShieldRegenDelay( soul )
+}
+
+float function GetShieldRegenDelay( entity soul )
+{
+ float regenDelay = TITAN_SHIELD_REGEN_DELAY
+
+ string settings = GetSoulPlayerSettings( soul )
+ regenDelay = expect float( Dev_GetPlayerSettingByKeyField_Global( settings, "titan_regen_delay" ) )
+
+ float delay
+ if ( SoulHasPassive( soul, ePassives.PAS_SHIELD_REGEN ) )
+ delay = regenDelay - 1.0
+ else
+ delay = regenDelay
+
+ if ( SoulHasPassive( soul, ePassives.PAS_SHIELD_BOOST ) )
+ delay = 2.0
+
+ return delay
+}
+
+void function RecordDamageToNPCTitanSoul( entity soul, var damageInfo )
+{
+ float damage = DamageInfo_GetDamage( damageInfo )
+
+ vector inflictOrigin = <0.0,0.0,0.0>
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+ if ( IsValid( inflictor ) )
+ inflictOrigin = inflictor.GetOrigin()
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ entity weapon = DamageInfo_GetWeapon( damageInfo )
+ array<string> weaponMods
+ if ( IsValid( weapon ) )
+ weaponMods = weapon.GetMods()
+
+ StoreDamageHistoryAndUpdate( soul, TITAN_HEALTH_HISTORY_FALLOFF_END, damage, inflictOrigin, DamageInfo_GetCustomDamageType( damageInfo ), DamageInfo_GetDamageSourceIdentifier( damageInfo ), attacker, weaponMods )
+}
+
+void function AutoTitan_TryMultipleTitanCallout( entity titan, var damageInfo )
+{
+ array<entity> titans = GetTitansHitMeInTime( titan.GetTitanSoul(), 5 )
+ entity enemy = titan.GetEnemy()
+ if ( IsAlive( enemy ) && enemy.IsTitan() && !titans.contains( enemy ) )
+ titans.append( enemy )
+
+ int totalEngagedTitans = titans.len()
+
+ if ( totalEngagedTitans == 1 )
+ PlayAutoTitanConversation( titan, "autoEngageTitan" )
+ else if ( totalEngagedTitans > 1 )
+ PlayAutoTitanConversation( titan, "autoEngageTitans" )
+}
+
+float function CalculateNextRegenTime( float damage, int damageType, bool critHit, float oldNextRegenTime, float maxRegenDelay )
+{
+ if ( damage >= TITAN_REGEN_MIN_DAMAGE || critHit || damageType & DF_STOPS_TITAN_REGEN )
+ {
+ if ( PROTO_VariableRegenDelay() )
+ {
+ // regen delay based on damage dealt
+ float minRegenDelay = 1.0
+ float regenDelay = GraphCapped( damage, 100, 1000, minRegenDelay, maxRegenDelay )
+
+ float nextRegenTime = oldNextRegenTime
+ float delayBasedOnCurrentTime = Time() + regenDelay
+ float delayBasedOnPreviousDelay = nextRegenTime + regenDelay
+ maxRegenDelay = Time() + maxRegenDelay
+
+ delayBasedOnCurrentTime = min( delayBasedOnCurrentTime, maxRegenDelay )
+ delayBasedOnPreviousDelay = min( delayBasedOnPreviousDelay, maxRegenDelay )
+ nextRegenTime = max( delayBasedOnCurrentTime, delayBasedOnPreviousDelay )
+
+ return nextRegenTime
+ }
+ else
+ {
+ // old style
+ return Time() + maxRegenDelay
+ }
+ }
+ else
+ {
+ float addTime = TITAN_REGEN_MIN_DAMAGE_DELAY
+
+ if ( oldNextRegenTime <= Time() + addTime )
+ return Time() + addTime
+ }
+
+ return oldNextRegenTime
+}
+
+void function AddCreditToTitanCoreBuilderForTitanDamageInflicted( entity titanAttacker, float damage )
+{
+ Assert( TitanDamageRewardsTitanCoreTime() )
+
+ float rateRaw = CORE_BUILD_PERCENT_FROM_TITAN_DAMAGE_INFLICTED
+ float rate = (rateRaw * 0.01)
+ float credit = (rate * damage)
+ if ( credit > 0.0 )
+ AddCreditToTitanCoreBuilder( titanAttacker, credit )
+}
+
+void function AddCreditToTitanCoreBuilderForTitanDamageReceived( entity titanVictim, float damage )
+{
+ Assert( TitanDamageRewardsTitanCoreTime() )
+
+ float rateRaw = CORE_BUILD_PERCENT_FROM_TITAN_DAMAGE_RECEIVED
+ float rate = (rateRaw * 0.01)
+ float credit = (rate * damage)
+ if ( credit > 0.0 )
+ AddCreditToTitanCoreBuilder( titanVictim, credit )
+}
+
+void function AddCreditToTitanCoreBuilderForDoomInflicted( entity titanAttacker )
+{
+ Assert( TitanDamageRewardsTitanCoreTime() )
+
+ float valueRaw = CORE_BUILD_PERCENT_FROM_DOOM_INFLICTED
+ float credit = (valueRaw * 0.01)
+ if ( credit > 0.0 )
+ AddCreditToTitanCoreBuilder( titanAttacker, credit )
+}
+
+void function AddCreditToTitanCoreBuilderForDoomEntered( entity titanVictim )
+{
+ Assert( TitanDamageRewardsTitanCoreTime() )
+
+ float valueRaw = CORE_BUILD_PERCENT_FROM_DOOM_ENTERED
+ float credit = (valueRaw * 0.01)
+ if ( credit > 0.0 )
+ AddCreditToTitanCoreBuilder( titanVictim, credit )
+}
+
+void function AddCreditToTitanCoreBuilder( entity titan, float credit )
+{
+ Assert( TitanDamageRewardsTitanCoreTime() )
+
+ entity soul = titan.GetTitanSoul()
+ if ( !IsValid( soul ) )
+ return
+
+ entity bossPlayer = soul.GetBossPlayer()
+
+ if ( titan.IsPlayer() )
+ {
+ if ( !IsValid( bossPlayer ) )
+ return
+
+ if ( bossPlayer.IsTitan() && TitanCoreInUse( bossPlayer ) )
+ return
+ }
+ else
+ {
+ Assert( titan.IsNPC() )
+ if ( TitanCoreInUse( titan ) )
+ return
+ }
+
+ if ( !IsAlive( titan ) )
+ return
+
+ if ( SoulHasPassive( soul, ePassives.PAS_VANGUARD_COREMETER ) )
+ credit *= 1.10
+
+ credit *= file.earn_meter_titan_multiplier
+ #if MP
+ if ( titan.IsPlayer() )
+ {
+ float coreModifier = titan.GetPlayerNetFloat( "coreMeterModifier" )
+ if ( coreModifier >= 0.5 )
+ credit *= FD_HOT_STREAK_MULTIPLIER
+ }
+ #endif
+
+ bool coreWasAvailable = false
+
+ if ( IsValid( bossPlayer ) )
+ coreWasAvailable = IsCoreChargeAvailable( bossPlayer, soul )
+
+ float oldTotalCredit = SoulTitanCore_GetNextAvailableTime( soul )
+ float newTotalCredit = (credit + oldTotalCredit)
+ if ( newTotalCredit >= 0.998 ) //JFS - the rui has a +0.001 for showing the meter as full. This fixes the case where the core meter displays 100 but can't be fired.
+ newTotalCredit = 1.0
+ SoulTitanCore_SetNextAvailableTime( soul, newTotalCredit )
+
+ if ( IsValid( bossPlayer ) && !coreWasAvailable && IsCoreChargeAvailable( bossPlayer, soul ) )
+ {
+ AddPlayerScore( bossPlayer, "TitanCoreEarned" )
+ #if MP
+ UpdateTitanCoreEarnedStat( bossPlayer, titan )
+ PIN_PlayerAbilityReady( bossPlayer, "core" )
+ #endif
+ }
+
+ #if MP
+ if ( IsValid( bossPlayer ) )
+ JFS_PlayerEarnMeter_CoreRewardUpdate( titan, oldTotalCredit, newTotalCredit )
+ #endif
+
+ #if HAS_TITAN_TELEMETRY
+ if ( titan.IsPlayer() )
+ {
+ if ( IsCoreChargeAvailable( titan, soul ) )
+ {
+ TitanHints_TryShowHint( titan, [OFFHAND_EQUIPMENT] )
+ }
+ }
+ #endif
+}
+
+float function GetTitanCoreTimer( entity titan )
+{
+ Assert( titan.IsTitan() )
+ entity soul = titan.GetTitanSoul()
+ Assert( soul )
+
+ return SoulTitanCore_GetNextAvailableTime( soul ) - Time()
+}
+
+
+
+void function SetTitanCoreTimer( entity titan, float timeDiff )
+{
+ Assert( !TitanDamageRewardsTitanCoreTime() )
+
+ Assert( titan.IsTitan() )
+ entity soul = titan.GetTitanSoul()
+ Assert( soul )
+
+ float newTime = Time() + timeDiff
+ SoulTitanCore_SetNextAvailableTime( soul, max( Time() - 1, newTime ) )
+}
+
+
+void function Titan_MonarchCleanup( entity soul, var damageInfo )
+{
+ entity titan = soul.GetTitan()
+
+ if ( !IsValid( titan ) )
+ return
+
+ int statesIndex = titan.FindBodyGroup( "states" )
+ if ( statesIndex <= -1 )
+ return
+
+ titan.SetBodygroup( statesIndex, 2 )
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/titan/_titan_hints.gnut b/Northstar.CustomServers/scripts/vscripts/titan/_titan_hints.gnut
new file mode 100644
index 00000000..0e8b4b5b
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/titan/_titan_hints.gnut
@@ -0,0 +1,267 @@
+global function TitanHints_Init
+global function TitanHints_NotifyUsedOffhand
+global function TitanHints_ResetThresholds
+global function TitanHints_TryShowHint
+global function TitanHints_ShowHint
+
+const float FIGHT_START_THRESHOLD = 10.0
+const float FIGHT_HINT_THRESHOLD = 8.0
+const float TITAN_HINT_COOLDOWN = 15.0
+
+struct
+{
+ float titanFightStartTime = -99
+ float lastDidDamageTime = -99
+ float lastTookDamageTime = -99
+ float lastShowHintTime = -99
+ float lastDodgeTime = -99
+ table<int,float> titanHintThresholds
+ table<int,float> titanHintThresholdAdd
+ table<int,float> lastShowHintTimes
+} file
+
+void function TitanHints_Init()
+{
+ AddDamageCallback( "player", TitanHint_Player_OnDamaged )
+ AddDamageCallback( "npc_titan", TitanHint_NPC_OnDamaged )
+ AddDamageCallback( "npc_super_spectre", TitanHint_NPC_OnDamaged )
+
+ file.titanHintThresholds[ TITAN_HINT_DASH ] <- 5.0
+ file.titanHintThresholds[ OFFHAND_ORDNANCE ] <- 5.0
+ file.titanHintThresholds[ OFFHAND_SPECIAL ] <- 5.0
+ file.titanHintThresholds[ OFFHAND_ANTIRODEO ] <- 10.0
+ file.titanHintThresholds[ OFFHAND_EQUIPMENT ] <- 1.0
+
+ file.lastShowHintTimes[ TITAN_HINT_DASH ] <- -99.0
+ file.lastShowHintTimes[ OFFHAND_ORDNANCE ] <- -99.0
+ file.lastShowHintTimes[ OFFHAND_SPECIAL ] <- -99.0
+ file.lastShowHintTimes[ OFFHAND_ANTIRODEO ] <- -99.0
+ file.lastShowHintTimes[ OFFHAND_EQUIPMENT ] <- -99.0
+
+ file.titanHintThresholdAdd[ TITAN_HINT_DASH ] <- 0
+ file.titanHintThresholdAdd[ OFFHAND_ORDNANCE ] <- 0
+ file.titanHintThresholdAdd[ OFFHAND_SPECIAL ] <- 0
+ file.titanHintThresholdAdd[ OFFHAND_ANTIRODEO ] <- 0
+ file.titanHintThresholdAdd[ OFFHAND_EQUIPMENT ] <- 0
+
+ AddCallback_OnPlayerInventoryChanged( TitanHints_ResetThresholds )
+ AddSpawnCallback( "player", PlayerDidLoad )
+}
+
+void function PlayerDidLoad( entity player )
+{
+ AddPlayerMovementEventCallback( player, ePlayerMovementEvents.DODGE, OnPlayerDodge )
+}
+
+void function TitanHint_Player_OnDamaged( entity player, var damageInfo )
+{
+ if ( !player.IsTitan() )
+ return
+
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !attacker.IsTitan() && !IsSuperSpectre(attacker) )
+ return
+
+ if ( attacker.GetTeam() == player.GetTeam() )
+ return
+
+ TrySetFightTime()
+
+ file.lastTookDamageTime = Time()
+
+ array<int> hintsToShow = [ TITAN_HINT_DASH, OFFHAND_EQUIPMENT, OFFHAND_SPECIAL, OFFHAND_ORDNANCE, OFFHAND_ANTIRODEO ]
+
+ if ( GetDoomedState( player ) || GetTitanCurrentRegenTab( player ) < 2 )
+ hintsToShow = [ TITAN_HINT_DASH, OFFHAND_SPECIAL ]
+
+ TitanHints_TryShowHint( player, hintsToShow, attacker )
+}
+
+void function TitanHint_NPC_OnDamaged( entity victim, var damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !attacker.IsPlayer() )
+ return
+
+ if ( !attacker.IsTitan() )
+ return
+
+ TrySetFightTime()
+
+ file.lastDidDamageTime = Time()
+
+ TitanHints_TryShowHint( attacker, [ OFFHAND_EQUIPMENT, OFFHAND_ORDNANCE, OFFHAND_ANTIRODEO ], victim )
+}
+
+// reset thresholds
+void function TitanHints_ResetThresholds( entity player )
+{
+ if ( !player.IsTitan() )
+ return
+
+ foreach ( index, value in file.titanHintThresholdAdd )
+ {
+ if ( index != TITAN_HINT_DASH ) // don't reset dash
+ file.titanHintThresholdAdd[ index ] = 0.0
+ }
+}
+
+// increase threshold for hints every time the player uses it
+void function TitanHints_NotifyUsedOffhand( int index )
+{
+ // never increment for core
+ if ( index == OFFHAND_EQUIPMENT )
+ return
+
+ if ( index in file.titanHintThresholds )
+ {
+ file.titanHintThresholdAdd[ index ] += TITAN_HINT_COOLDOWN
+ }
+}
+
+bool function TrySetFightTime()
+{
+ if (
+ Time() - file.lastTookDamageTime > FIGHT_START_THRESHOLD &&
+ Time() - file.lastDidDamageTime > FIGHT_START_THRESHOLD
+ )
+ {
+ file.titanFightStartTime = Time()
+ return true
+ }
+
+ return false
+}
+
+void function TitanHints_TryShowHint( entity player, array<int> indexes, entity enemy = null )
+{
+ if ( GetConVarInt( "hud_setting_showTips" ) == 0 )
+ return
+
+ float fightDuration = Time() - file.titanFightStartTime
+ if ( fightDuration < FIGHT_HINT_THRESHOLD )
+ return
+
+ if ( TitanCoreInUse( player ) )
+ return
+
+ foreach ( idx in indexes )
+ {
+ float threshold = file.titanHintThresholds[idx] + file.titanHintThresholdAdd[idx]
+
+ // have we been fighting for a while?
+ if ( fightDuration < max( threshold, TITAN_HINT_COOLDOWN ) )
+ continue
+
+ // have we already shown this hint?
+ if ( Time() - file.lastShowHintTimes[idx] < max( threshold, TITAN_HINT_COOLDOWN ) )
+ continue
+
+ // have we already shown a hint?
+ if ( Time() - file.lastShowHintTime < TITAN_HINT_COOLDOWN )
+ continue
+
+ if ( idx != TITAN_HINT_DASH )
+ {
+ // when did you last use this ability?
+ if ( Time() - player.p.lastTitanOffhandUseTime[idx] < threshold )
+ continue
+
+ entity weapon = player.GetOffhandWeapon( idx )
+
+ if ( weapon == null )
+ continue
+
+ // has this ability been available for a while?
+ if ( weapon.GetNextAttackAllowedTime() + threshold > Time() )
+ continue
+
+ var requiresLocks = weapon.GetWeaponInfoFileKeyField( "requires_lock" )
+
+ if ( requiresLocks != null )
+ {
+ expect int( requiresLocks )
+ if ( requiresLocks == 1 )
+ {
+ if ( weapon.SmartAmmo_IsEnabled() && !SmartAmmo_CanWeaponBeFired( weapon ) )
+ continue
+ }
+ }
+
+
+ int curEnergyCost = weapon.GetWeaponCurrentEnergyCost()
+ if ( !player.CanUseSharedEnergy( curEnergyCost ) )
+ continue
+
+ if ( weapon.IsChargeWeapon() )
+ {
+ if ( weapon.GetWeaponChargeFraction() > 0.0 )
+ continue
+ }
+
+ if ( weapon.GetWeaponPrimaryClipCount() < weapon.GetWeaponSettingInt( eWeaponVar.ammo_min_to_fire ) )
+ continue
+
+ // special core check
+ if ( idx == OFFHAND_EQUIPMENT )
+ {
+ if( !CheckCoreAvailable( weapon ) )
+ continue
+ if ( IsConversationPlaying() )
+ continue
+ }
+
+ var hintType = weapon.GetWeaponInfoFileKeyField( "hint_type" )
+ if ( hintType != null )
+ {
+ if ( hintType == "range_toggle" )
+ {
+ if ( enemy != null )
+ {
+ float dist = Distance2D( enemy.GetOrigin(), player.GetOrigin() )
+
+ if ( weapon.HasMod( "ammo_swap_ranged_mode" ) )
+ { // has long range mode, will tell to swap to short range
+ if ( dist > 2500 )
+ {
+ continue
+ }
+ }
+ else
+ { // has short range mode, will tell to swap to long range
+ if ( dist < 1500 )
+ {
+ continue
+ }
+ }
+ }
+ }
+ }
+
+ }
+ else
+ {
+ if ( Time() - file.lastDodgeTime < threshold )
+ continue
+
+ // should check if dodge is available here, but we can't seem to do that
+ }
+
+ // show hint
+ TitanHints_ShowHint( player, idx )
+ break
+ }
+}
+
+void function TitanHints_ShowHint( entity player, int idx )
+{
+ Remote_CallFunction_Replay( player, "ServerCallback_ShowOffhandWeaponHint", idx )
+ file.lastShowHintTimes[idx] = Time()
+ file.lastShowHintTime = Time()
+}
+
+void function OnPlayerDodge( entity player )
+{
+ file.lastDodgeTime = Time()
+ file.titanHintThresholdAdd[ TITAN_HINT_DASH ] += TITAN_HINT_COOLDOWN
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/titan/_titan_hotdrop.gnut b/Northstar.CustomServers/scripts/vscripts/titan/_titan_hotdrop.gnut
new file mode 100644
index 00000000..e3410de8
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/titan/_titan_hotdrop.gnut
@@ -0,0 +1,778 @@
+untyped
+
+global function TitanHotdrop_Init
+
+global function TitanHotDrop
+global function PlayersTitanHotdrops
+global function NPCTitanHotdrops
+global function NPCPrespawnWarpfallSequence
+global function WaitTillHotDropComplete
+global function OnTitanHotdropImpact
+global function PlayHotdropImpactFX
+global function TitanTestDropPoint
+global function EdgeTraceDropPoint
+
+
+global function GetHotDropImpactTime
+
+global function ModifyOriginForDrop
+
+global function NearTitanfallBlocker
+
+global function DevCheckInTitanfallBlocker
+
+global function DrawTitanfallBlockers
+
+global function DropPodFindDropNodes
+
+global function PlayDeathFromTitanFallSounds
+
+global const HOTDROP_FP_WARP = $"P_warpjump_FP"
+global const HOTDROP_TRAIL_FX = $"hotdrop_hld_warp"
+global int BUBBLE_SHIELD_FX_PARTICLE_SYSTEM_INDEX
+
+function TitanHotdrop_Init()
+{
+
+ RegisterSignal( "titan_impact" )
+ RegisterSignal( "TitanHotDropComplete" )
+ RegisterSignal( "BubbleShieldStatusUpdate" )
+
+ PrecacheEffect( HOTDROP_TRAIL_FX )
+ PrecacheEffect( HOTDROP_FP_WARP )
+
+ AddDamageCallbackSourceID( damagedef_titan_fall, TitanFall_DamagedPlayerOrNPC )
+
+ PrecacheImpactEffectTable( HOTDROP_IMPACT_FX_TABLE )
+
+ PrecacheModel( $"models/fx/xo_shield.mdl" )
+ PrecacheModel( $"models/fx/xo_shield_wall.mdl" )
+ BUBBLE_SHIELD_FX_PARTICLE_SYSTEM_INDEX = PrecacheParticleSystem( $"P_shield_hld_01_CP" )
+
+ BubbleShield_Init()
+}
+
+void function TitanHotDrop( entity titan, string animation, vector origin, vector angles, entity player, entity camera )
+{
+ Assert( titan.IsTitan(), titan + " is not a titan" )
+
+ titan.EndSignal( "OnDeath" )
+
+ HideName( titan )
+
+ array<entity> cleanup = [] // ents that will be deleted upon completion
+
+ OnThreadEnd(
+ function() : ( cleanup, titan, player, camera )
+ {
+ printt( "Post impact,anim is done" )
+ if ( IsValid( titan ) )
+ {
+ delete titan.s.hotDropPlayer
+ titan.e.isHotDropping = false
+ titan.Signal( "TitanHotDropComplete" )
+ if ( !IsFFAGame() )
+ titan.Minimap_DisplayDefault( titan.GetTeam(), null )
+ }
+
+ if ( IsValid( camera ) )
+ camera.ClearParent()
+
+ foreach ( entity ent in cleanup )
+ {
+ if ( IsValid_ThisFrame( ent ) )
+ {
+ // Delay enough seconds to allow titan hot drop smokeTrail FX to play fully
+ ent.Kill_Deprecated_UseDestroyInstead()
+ }
+ }
+
+ if ( IsValid( player ) )
+ ScreenFadeFromBlack( player, 0.2, 0.2 )
+ }
+ )
+
+ titan.s.hotDropPlayer <- player
+ titan.e.isHotDropping = true
+
+ origin += Vector(0,0,8 ) // work around for currently busted animation
+
+ entity ref = CreateScriptRef()
+ ref.SetOrigin( origin )
+ ref.SetAngles( angles )
+ ref.Show()
+ cleanup.append( ref )
+
+ // add smoke fx
+
+ TitanHotDrop_Smoke( cleanup, titan, titan.GetBossPlayer() )
+
+// "Titan_1P_Warpfall_Hotdrop" - for first person drops while inside the titan dropping into the level
+// "Titan_1P_Warpfall_Start" - for first person warp calls, starting right on the button press
+// "Titan_1P_Warpfall_WarpToLanding" - for first person from the visual of the titan appearing and falling
+// "Titan_3P_Warpfall_Start" - for any 3P other player or NPC when they call in a warp, starting right on their button press
+// "Titan_3P_Warpfall_WarpToLanding" - for any 3P other player or NPC from the visual of the titan appearing and falling
+ int teamNum = TEAM_UNASSIGNED
+ if ( IsValid( player ) )
+ teamNum = player.GetTeam();
+
+ EmitSoundAtPositionOnlyToPlayer( teamNum, origin, player, "Titan_1P_Warpfall_Hotdrop" )
+ EmitSoundAtPositionOnlyToPlayer( teamNum, origin, player, "Titan_1P_Warpfall_Start" )
+ EmitSoundAtPositionExceptToPlayer( teamNum, origin, player, "Titan_3P_Warpfall_Start" )
+ EmitSoundAtPositionExceptToPlayer( teamNum, origin, player, "Titan_3P_Warpfall_WarpToLanding" )
+
+ float duration = titan.GetSequenceDuration( animation )
+
+ Minimap_PingForTeam( titan.GetTeam(), origin, 64.0, duration, TEAM_COLOR_FRIENDLY / 255.0, 4, false )
+ if ( !IsFFAGame() )
+ titan.Minimap_Hide( titan.GetTeam(), null )
+
+ titan.NotSolid();
+ thread PlayAnimTeleport( titan, animation, ref )
+ titan.EndSignal( "OnAnimationDone" )
+
+ if ( player )
+ {
+ player.PlayerCone_SetMinYaw( -70 )
+ player.PlayerCone_SetMaxYaw( 70 )
+ player.PlayerCone_SetMinPitch( -90 )
+ player.PlayerCone_SetMaxPitch( 90 )
+ }
+
+ titan.WaitSignal( "titan_impact" )
+ player.ClearHotDropImpactTime()
+// wait duration - 1.25
+
+ titan.Solid();
+
+ ShowName( titan )
+
+ vector sourcePosition = origin
+ sourcePosition.z = sourcePosition.z + 5.0
+
+ Explosion_DamageDefSimple(
+ damagedef_titan_hotdrop,
+ origin,
+ titan, // attacker
+ titan, // inflictor
+ origin )
+
+ float zoomTime = 2.0
+ float rotateTime = 0.5
+
+ //printt( "Post impact, before anim is done" )
+
+ if ( IsValid( camera ) )
+ {
+ camera.ClearParent()
+
+ entity mover = CreateExpensiveScriptMover()
+ mover.SetOrigin( camera.GetOrigin() )
+ mover.SetAngles( camera.GetAngles() )
+ camera.SetParent( mover )
+
+ mover.NonPhysicsMoveTo( titan.GetWorldSpaceCenter(), zoomTime, zoomTime * 0.4, zoomTime * 0.4 )
+ cleanup.append( mover )
+
+ wait 0.5
+
+ ScreenFadeToBlackForever( player, 0.8 )
+
+ wait 0.6
+
+ mover.RotateTo( angles, rotateTime, rotateTime*0.2, rotateTime*0.2 )
+ }
+
+ WaittillAnimDone( titan )
+}
+
+entity function TitanHotDrop_Smoke( array<entity> cleanup, entity titan, entity player )
+{
+ entity smokeTrail = CreateEntity( "info_particle_system" )
+ if ( IsValid( player ) )
+ {
+ smokeTrail.SetOwner( player )
+ smokeTrail.kv.VisibilityFlags = ENTITY_VISIBLE_TO_OWNER
+ }
+
+ smokeTrail.SetValueForEffectNameKey( HOTDROP_TRAIL_FX ) // HOTDROP_FP_WARP
+ smokeTrail.kv.start_active = 1
+ DispatchSpawn( smokeTrail )
+ smokeTrail.SetParent( titan, "HATCH_HEAD" )
+ cleanup.append( smokeTrail )
+
+
+ smokeTrail = CreateEntity( "info_particle_system" )
+ if ( IsValid( player ) )
+ {
+ smokeTrail.SetOwner( player )
+ smokeTrail.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) //owner cant see
+ }
+
+ smokeTrail.SetValueForEffectNameKey( HOTDROP_TRAIL_FX ) // HOTDROP_FP_WARP
+ smokeTrail.kv.start_active = 1
+ DispatchSpawn( smokeTrail )
+ smokeTrail.SetParent( titan, "HATCH_HEAD" )
+ cleanup.append( smokeTrail )
+
+ return smokeTrail
+}
+
+void function PlayersTitanHotdrops( entity titan, vector origin, vector angles, entity player, string animation )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.s.disableAutoTitanConversation <- true // refactor: Should be created on spawn, and always exist -mackey
+
+ OnThreadEnd(
+ function() : ( titan, player )
+ {
+ if ( !IsValid( titan ) )
+ return
+
+ // removed so that model highlight always works for you autotitan
+// titan.DisableRenderAlways()
+
+ delete titan.s.hotDropPlayer
+ titan.e.isHotDropping = false
+ titan.Signal( "TitanHotDropComplete" )
+ DeleteAnimEvent( titan, "titan_impact" )
+ DeleteAnimEvent( titan, "second_stage" )
+ DeleteAnimEvent( titan, "set_usable" )
+ }
+ )
+
+ HideName( titan )
+ titan.s.hotDropPlayer <- player
+ titan.e.isHotDropping = true
+ titan.UnsetUsable() //Stop titan embark before it lands
+ AddAnimEvent( titan, "titan_impact", OnTitanHotdropImpact )
+ AddAnimEvent( titan, "second_stage", OnReplacementTitanSecondStage, origin )
+ AddAnimEvent( titan, "set_usable", SetTitanUsableByOwner )
+
+ string sfxFirstPerson
+ string sfxThirdPerson
+
+ switch ( animation )
+ {
+ case "at_hotdrop_drop_2knee_turbo_upgraded":
+ sfxFirstPerson = "Titan_1P_Warpfall_WarpToLanding_fast"
+ sfxThirdPerson = "Titan_3P_Warpfall_WarpToLanding_fast"
+ break
+
+ case "bt_hotdrop_skyway":
+ sfxFirstPerson = "titan_hot_drop_turbo_begin"
+ sfxThirdPerson = "titan_hot_drop_turbo_begin_3P"
+ break
+
+ case "at_hotdrop_drop_2knee_turbo":
+ sfxFirstPerson = "titan_hot_drop_turbo_begin"
+ sfxThirdPerson = "titan_hot_drop_turbo_begin_3P"
+ break
+
+ default:
+ Assert( 0, "Unknown anim " + animation )
+ }
+
+ float impactTime = GetHotDropImpactTime( titan, animation )
+ Attachment result = titan.Anim_GetAttachmentAtTime( animation, "OFFSET", impactTime )
+ vector maxs = titan.GetBoundingMaxs()
+ vector mins = titan.GetBoundingMins()
+ int mask = titan.GetPhysicsSolidMask()
+ origin = ModifyOriginForDrop( origin, mins, maxs, result.position, mask )
+
+ titan.SetInvulnerable() //Make Titan invulnerable until bubble shield is up. Cleared in OnTitanHotdropImpact
+
+ if ( SoulHasPassive( titan.GetTitanSoul(), ePassives.PAS_BUBBLESHIELD ) )
+ {
+ delaythread( impactTime ) CreateBubbleShield( titan, origin, angles )
+ }
+ else if ( SoulHasPassive( titan.GetTitanSoul(), ePassives.PAS_WARPFALL ) )
+ {
+ angles = AnglesCompose( angles, Vector( 0.0, 180.0, 0.0) )
+ }
+
+ //DrawArrow( origin, angles, 10, 150 )
+ // HACK: not really a hack, but this could be optimized to only render always for a given client
+ titan.EnableRenderAlways()
+
+ int teamNum = TEAM_UNASSIGNED
+ if ( IsValid( player ) )
+ teamNum = player.GetTeam()
+
+ EmitDifferentSoundsAtPositionForPlayerAndWorld( sfxFirstPerson, sfxThirdPerson, origin, player, teamNum )
+
+ SetStanceKneel( titan.GetTitanSoul() )
+
+ waitthread PlayAnimTeleport( titan, animation, origin, angles )
+
+ TitanCanStand( titan )
+ if ( !titan.GetCanStand() )
+ {
+ titan.SetOrigin( origin )
+ titan.SetAngles( angles )
+ }
+
+ titan.ClearInvulnerable() //Make Titan vulnerable again once he's landed
+
+ if ( !Flag( "DisableTitanKneelingEmbark" ) )
+ {
+ if ( IsValid( GetEmbarkPlayer( titan ) ) )
+ {
+ titan.SetTouchTriggers( true ) //Hack, potential fix for triggers bug. See bug 212751
+ //A player is trying to get in before the hotdrop animation has finished
+ //Wait until the embark animation has finished
+ WaittillAnimDone( titan )
+ return
+ }
+
+ titan.s.standQueued = false // SetStanceKneel should set this
+ SetStanceKneel( titan.GetTitanSoul() )
+ thread PlayAnim( titan, "at_MP_embark_idle_blended" )
+ }
+}
+
+float function GetHotDropImpactTime( entity titan, string animation )
+{
+ float impactTime = titan.GetScriptedAnimEventCycleFrac( animation, "titan_impact" )
+ if ( impactTime < 0.0 )
+ impactTime = titan.GetScriptedAnimEventCycleFrac( animation, "signal:titan_impact" )
+
+ Assert( impactTime > -1.0, "No event titan_impact in " + animation )
+
+ float duration = titan.GetSequenceDuration( animation )
+
+ impactTime *= duration
+
+ return impactTime
+}
+
+function NPCTitanHotdrops( entity titan, bool standImmediately, string titanfallAnim = "at_hotdrop_drop_2knee_turbo" )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ titan.e.isHotDropping = true
+ titan.s.bubbleShieldStatus <- 0
+
+ titan.SetEfficientMode( true )
+ titan.SetTouchTriggers( false )
+ titan.SetAimAssistAllowed( false )
+
+ float impactTime = GetHotDropImpactTime( titan, titanfallAnim )
+ vector origin = titan.GetOrigin()
+ vector angles = titan.GetAngles()
+
+ #if GRUNTCHATTER_ENABLED
+ GruntChatter_TryIncomingSpawn( titan, origin )
+ #endif
+
+ #if MP
+ TryAnnounceTitanfallWarningToEnemyTeam( titan.GetTeam(), origin )
+ #endif
+
+ if ( NPCShouldDoBubbleShieldAfterHotdrop( titan ) )
+ {
+ titan.SetNoTarget( true )
+ thread CreateGenericBubbleShield_Delayed( titan, origin, angles, impactTime - 0.1 )
+ }
+
+ waitthread PlayersTitanHotdrops( titan, origin, angles, null, titanfallAnim )
+
+ if ( standImmediately )
+ {
+ SetStanceStand( titan.GetTitanSoul() )
+ waitthread PlayAnimGravity( titan, "at_hotdrop_quickstand" )
+ }
+
+ titan.SetEfficientMode( false )
+ titan.SetTouchTriggers( true )
+ titan.SetAimAssistAllowed( true )
+
+ titan.e.isHotDropping = false
+ titan.Signal( "TitanHotDropComplete" )
+
+ titan.SetNoTarget( false )
+
+ while( titan.s.bubbleShieldStatus == 1 )
+ titan.WaitSignal( "BubbleShieldStatusUpdate" )
+}
+
+void function NPCPrespawnWarpfallSequence( string aiSettings, vector spawnOrigin, vector spawnAngle )
+{
+ string animation = "at_hotdrop_drop_2knee_turbo_upgraded"
+// string settings = GetTitanForPlayer( player ).titanSetFile
+ string playerSettings = expect string( Dev_GetAISettingByKeyField_Global( aiSettings, "npc_titan_player_settings" ) )
+ asset model = GetPlayerSettingsAssetForClassName( playerSettings, "bodymodel" )
+ Attachment warpAttach = GetAttachmentAtTimeFromModel( model, animation, "offset", spawnOrigin, spawnAngle, 0 )
+
+ entity fakeTitan = CreatePropDynamic( model )
+ float impactTime = GetHotDropImpactTime( fakeTitan, animation )
+
+ #if SP //MP AI already call DisableTitanfallForLifetimeOfEntityNearOrigin() in SpawnNeutralAI()/SpawnTeamAI() functions. Pretty sure can just remove this for SP too
+ thread TemporarilyDisableTitanfallAroundRadius( spawnOrigin, 72, WARPFALL_SOUND_DELAY + WARPFALL_FX_DELAY ) //TODO: Look into getting rid of this. Doesn't play well with DisableTitanfallForLifetimeOfEntityNearOrigin. Only used in Beacon
+ #endif
+
+ fakeTitan.Kill_Deprecated_UseDestroyInstead()
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, spawnOrigin, "Titan_3P_Warpfall_CallIn" )
+
+ wait WARPFALL_SOUND_DELAY
+
+ // "Titan_1P_Warpfall_Start" - for first person warp calls, starting right on the button press
+ // "Titan_3P_Warpfall_Start" - for any 3P other player or NPC when they call in a warp, starting right on their button press
+ EmitSoundAtPosition( TEAM_UNASSIGNED, spawnOrigin, "Titan_3P_Warpfall_Start" )
+
+ PlayFX( TURBO_WARP_FX, warpAttach.position + Vector(0,0,-104), warpAttach.angle )
+
+ wait WARPFALL_FX_DELAY
+}
+
+void function WaitTillHotDropComplete( entity titan )
+{
+ titan.EndSignal( "OnDeath" )
+ titan.EndSignal( "OnDestroy" )
+
+ // waits for him to drop in from the sky AND stand up
+ if ( titan.e.isHotDropping )
+ WaitSignal( titan, "TitanHotDropComplete" )
+}
+
+function CreateGenericBubbleShield_Delayed( entity titan, vector origin, vector angles, float delay = 0.0 )
+{
+ titan.EndSignal( "OnDestroy" )
+
+ if ( delay > 0 )
+ wait delay
+
+ titan.s.bubbleShieldStatus = 1
+ CreateGenericBubbleShield( titan, origin, angles )
+ titan.s.bubbleShieldStatus = 0
+ titan.Signal( "BubbleShieldStatusUpdate" )
+}
+
+
+vector function ModifyOriginForDrop( vector origin, vector mins, vector maxs, vector resultPos, int mask )
+{
+ TraceResults trace = TraceHull( resultPos + Vector(0,0,20), resultPos + Vector(0,0,-20), mins, maxs, null, mask, TRACE_COLLISION_GROUP_NONE )
+ float zDif = trace.endPos.z - resultPos.z
+ origin.z += zDif
+ origin.z += 3.0
+
+ return origin
+}
+
+void function OnReplacementTitanSecondStage( entity titan )
+{
+ vector origin = expect vector( GetOptionalAnimEventVar( titan, "second_stage" ) )
+
+ string sfxFirstPerson = "titan_drop_pod_turbo_landing"
+ string sfxThirdPerson = "titan_drop_pod_turbo_landing_3P"
+ entity player = titan.GetBossPlayer()
+ EmitDifferentSoundsAtPositionForPlayerAndWorld( sfxFirstPerson, sfxThirdPerson, origin, player, titan.GetTeam() )
+}
+
+void function OnTitanHotdropImpact( entity titan )
+{
+ ShowName( titan )
+ PlayHotdropImpactFX( titan )
+ titan.Signal( "ClearDisableTitanfall" )
+}
+
+function SetTitanUsable( titan )
+{
+ titan.SetUsableByGroup( "friendlies pilot" )
+}
+
+void function SetTitanUsableByOwner( entity titan )
+{
+ titan.SetUsableByGroup( "owner pilot" )
+}
+
+function PlayHotdropImpactFX( titan )
+{
+ expect entity( titan )
+ if ( !IsAlive( titan ) || !titan.IsTitan() )
+ return
+
+ local origin = titan.GetOrigin()
+
+ Explosion_DamageDefSimple(
+ damagedef_titan_fall,
+ origin,
+ titan, // attacker
+ titan, // inflictor
+ origin )
+
+
+ CreateShake( titan.GetOrigin(), 16, 150, 2, 1500 )
+ // No Damage - Only Force
+ // Push players
+ // Push radially - not as a sphere
+ // Test LOS before pushing
+ int flags = 15
+ vector impactOrigin = titan.GetOrigin() + Vector( 0,0,10 )
+ float impactRadius = 512
+ CreatePhysExplosion( impactOrigin, impactRadius, PHYS_EXPLOSION_LARGE, flags )
+}
+
+function NearTitanfallBlocker( baseOrigin )
+{
+ foreach ( hardpoint in level.testHardPoints )
+ {
+ local hpOrigin = hardpoint.GetOrigin()
+ hpOrigin.z -= 100 // why are hardpoints not really at the origin?
+ if ( Distance( hpOrigin, baseOrigin ) < SAFE_TITANFALL_DISTANCE )
+ return true
+ }
+
+ foreach ( flagSpawnPoint in level.testFlagSpawnPoints )
+ {
+ local fspOrigin = flagSpawnPoint.GetOrigin()
+ if ( Distance( fspOrigin, baseOrigin ) < SAFE_TITANFALL_DISTANCE_CTF )
+ return true
+ }
+
+ foreach ( blocker in level.titanfallBlockers )
+ {
+ if ( Distance2D( baseOrigin, blocker.origin ) > blocker.radius )
+ continue
+
+ if ( baseOrigin.z < blocker.origin.z )
+ continue
+
+ if ( baseOrigin.z > blocker.maxHeight )
+ continue
+
+ return true
+ }
+
+ return false
+}
+
+function DevCheckInTitanfallBlocker()
+{
+ if ( "toggleBlocker" in svGlobal.levelEnt.s )
+ {
+ svGlobal.levelEnt.s.toggleBlocker.Kill_Deprecated_UseDestroyInstead()
+ delete svGlobal.levelEnt.s.toggleBlocker
+ return
+ }
+
+ svGlobal.levelEnt.s.toggleBlocker <- CreateScriptRef()
+ svGlobal.levelEnt.s.toggleBlocker.EndSignal( "OnDestroy" )
+
+ entity player = GetPlayerArray()[0]
+ for ( ;; )
+ {
+ printt( "Inside Titanfall blocker: " + NearTitanfallBlocker( player.GetOrigin() ) )
+ DrawTitanfallBlockers()
+ wait 0.5
+ }
+}
+
+function DrawTitanfallBlockers()
+{
+ foreach ( hardpoint in level.testHardPoints )
+ {
+ vector hpOrigin = expect entity( hardpoint ).GetOrigin()
+ DebugDrawCircle( hpOrigin, Vector(0,0,0), SAFE_TITANFALL_DISTANCE, 255, 255, 0, true, 1.0 )
+ }
+
+ foreach ( flagSpawnPoint in level.testFlagSpawnPoints )
+ {
+ vector fspOrigin = expect entity( flagSpawnPoint ).GetOrigin()
+ DebugDrawCircle( fspOrigin, Vector(0,0,0), SAFE_TITANFALL_DISTANCE_CTF, 255, 255, 0, true, 1.0 )
+ }
+
+ foreach ( blocker in level.titanfallBlockers )
+ {
+ DebugDrawCircle( expect vector( blocker.origin ), Vector(0,0,0), expect float( blocker.radius ), 255, 255, 0, true, 1.0 )
+ vector org = Vector( blocker.origin.x, blocker.origin.y, blocker.maxHeight )
+ DebugDrawCircle( org, Vector(0,0,0), expect float( blocker.radius ), 255, 255, 0, true, 1.0 )
+ }
+}
+
+
+
+bool function EdgeTraceDropPoint( vector dropPoint )
+{
+ local offsetArray = [
+ Vector( 64,64,0 ),
+ Vector( -64,64,0 ),
+ Vector( 64,-64,0 ),
+ Vector( -64,-64,0 ),
+ ]
+ local maxDif = 48
+ local mask = TRACE_MASK_TITANSOLID | TRACE_MASK_PLAYERSOLID | TRACE_MASK_SOLID | TRACE_MASK_NPCSOLID
+ local totalDif = 0
+
+ foreach ( offset in offsetArray )
+ {
+ local startPos = dropPoint + Vector( 0, 0, 64 ) + offset
+ local endPos = dropPoint + Vector( 0, 0, -64 ) + offset
+ TraceResults result = TraceLine( startPos, endPos, null, mask, TRACE_COLLISION_GROUP_NONE )
+ local dif = fabs( result.endPos.z - dropPoint.z )
+ totalDif += dif
+
+ if ( dif > maxDif )
+ {
+ //DebugDrawLine( startPos, result.endPos, 200, 50, 50, true, 3 )
+ return false
+ }
+ //DebugDrawLine( startPos, result.endPos, 50, 50, 200, true, 3 )
+ }
+
+ if ( totalDif > ( maxDif * 2 ) )
+ {
+ // this should catch cases where a small item like a box or barrel stops the hull collision trace above the ground.
+ return false
+ }
+
+ return true
+}
+
+
+bool function DropPodFindDropNodes( FlightPath flightPath, vector origin, float yaw )
+{
+ if ( NearTitanfallBlocker( origin ) )
+ return false
+
+ //level.drawAnalysisPreview = true
+ if ( !TitanTestDropPoint( origin, flightPath ) )
+ return false
+
+ return EdgeTraceDropPoint( origin )
+}
+
+bool function TitanTestDropPoint( vector start, FlightPath flightPath )
+{
+ local draw = level.drawAnalysisPreview
+ local end = start + Vector(0,0,8000)
+
+ TraceResults result = TraceHull( start, end, flightPath.mins, flightPath.maxs, null, flightPath.traceMask, TRACE_COLLISION_GROUP_NONE )
+ if ( result.startSolid )
+ {
+ if ( draw )
+ {
+ DrawArrow( start, Vector(0,0,0), 5.0, 80 )
+ DebugDrawLine( start, result.endPos, 0, 255, 0, true, 5.0 )
+ DebugDrawLine( result.endPos, end, 255, 0, 0, true, 5.0 )
+ //local newstart = start + Vector(0,0,150)
+ //local reresult = TraceHull( newstart, start, flightPath.mins, flightPath.maxs, null, flightPath.traceMask, TRACE_COLLISION_GROUP_NONE )
+ //printt( "surface " + reresult.surfaceName )
+ //DebugDrawLine( newstart, reresult.endPos, 155, 0, 0, true, ANALYSIS_PREVIEW_TIME )
+ //DrawArrow( reresult.endPos, Vector(0,0,0), ANALYSIS_PREVIEW_TIME, 15 )
+ //
+// //DrawArrow( start, Vector(0,0,0), ANALYSIS_PREVIEW_TIME, 15 )
+ //DebugDrawLine( start, result.endPos, 255, 0, 0, true, ANALYSIS_PREVIEW_TIME )
+ //printt( "length " + Length( start - result.endPos ) )
+ }
+ return false
+ }
+
+ if ( result.fraction < 1 )
+ {
+ if ( result.hitSky )
+ {
+ if ( draw )
+ {
+ DebugDrawLine( start, end, 0, 0, 255, true, ANALYSIS_PREVIEW_TIME )
+ //DrawArrow( start, Vector(0,0,0), 1.0, 100 )
+ }
+ return true
+ }
+
+// if ( draw )
+// DebugDrawLine( orgs[i-1] + Vector(10,10,10), orgs[i]+ Vector(10,10,10), 255, 255, 0, true, ANALYSIS_PREVIEW_TIME )
+
+ // some fudge factor
+ if ( Distance( result.endPos, end ) > 16 )
+ {
+ if ( draw )
+ {
+ local offset = Vector(-0.1, -0.1, 0 )
+ DebugDrawLine( start + offset, result.endPos + offset, 0, 255, 0, true, ANALYSIS_PREVIEW_TIME )
+ DebugDrawLine( result.endPos + offset, end + offset, 255, 0, 0, true, ANALYSIS_PREVIEW_TIME )
+ //DebugDrawLine( start, end, 255, 0, 0, true, ANALYSIS_PREVIEW_TIME )
+ }
+ return false
+ }
+ }
+
+// DebugDrawLine( orgs[i-1], orgs[i], 0, 255, 0, true, ANALYSIS_PREVIEW_TIME )
+
+ if ( draw )
+ DebugDrawLine( start, end, 0, 255, 0, true, 0.2 )
+ return true
+}
+
+
+
+
+
+void function TitanFall_DamagedPlayerOrNPC( entity ent, var damageInfo )
+{
+ if ( !ent.IsPlayer() )
+ return
+
+ if ( !ent.IsTitan() )
+ return
+
+ vector damageOrigin = DamageInfo_GetDamagePosition( damageInfo )
+ vector entityOrigin = ent.GetOrigin()
+ local distance = Distance( entityOrigin, damageOrigin )
+
+ // on top of them, let the titans fall where they may
+ if ( distance < TITANFALL_INNER_RADIUS )
+ return
+
+ if ( IsTitanWithinBubbleShield( ent ) )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+
+ vector pushVector = Normalize( entityOrigin - damageOrigin )
+
+ vector traceEndOrigin = damageOrigin + (pushVector * TITANFALL_OUTER_RADIUS)
+ TraceResults traceResult = TraceHull( damageOrigin, traceEndOrigin, ent.GetBoundingMins(), ent.GetBoundingMins(), ent, TRACE_MASK_NPCSOLID_BRUSHONLY, TRACE_COLLISION_GROUP_NONE )
+
+ // no room to push them
+ if ( traceResult.fraction < 0.85 )
+ return
+
+ DamageInfo_ScaleDamage( damageInfo, 0.15 )
+
+ ent.SetVelocity( pushVector * 400 )
+ ent.SetStaggering()
+}
+
+function PlayDeathFromTitanFallSounds( player )
+{
+ if ( player.IsTitan() )
+ {
+ //printt( "Playing titanfall_on_titan at: "+ player.GetOrigin() )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, player.GetOrigin(), "titanfall_on_titan" )
+ }
+ else
+ {
+ //printt( "Playing titanfall_on_human at " + player.GetOrigin() )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, player.GetOrigin(), "titanfall_on_human" )
+ }
+}
+
+bool function NPCShouldDoBubbleShieldAfterHotdrop( entity titan )
+{
+ if ( titan.HasKey( "script_hotdrop" ) )
+ {
+ switch ( titan.kv.script_hotdrop )
+ {
+ case "4":
+ case "3":
+ printt( "DROP WITH NO BUBBLE" )
+ return false
+ }
+ }
+
+ return true
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/titan/_titan_triple_health.gnut b/Northstar.CustomServers/scripts/vscripts/titan/_titan_triple_health.gnut
new file mode 100644
index 00000000..7515b868
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/titan/_titan_triple_health.gnut
@@ -0,0 +1,524 @@
+untyped
+
+global function HealthRegenInit
+
+global function TitanLoseSegementFX //JFS: Only being used for Rodeo now, rename later if needed
+global function GibBodyPart
+
+const SEGMENT_DOWN_SOUNDS_3P = [
+ "titan_healthbar_tier3_down_3P_vs_3P", // 0 left (doom)
+ "titan_healthbar_tier2_down_3P_vs_3P", // 1 left
+ "titan_healthbar_tier1_down_3P_vs_3P", // 2 left
+ "titan_healthbar_tier1_down_3P_vs_3P" // shield gone
+]
+
+const SEGMENT_DOWN_SOUNDS_3P_ATTACKER = [
+ "titan_healthbar_tier3_down_1P_vs_3P", // 0 left (doom)
+ "titan_healthbar_tier2_down_1P_vs_3P", // 1 left
+ "titan_healthbar_tier1_down_1P_vs_3P", // 2 left
+ "titan_healthbar_tier1_down_1P_vs_3P" // shield gone
+]
+
+const SEGMENT_DOWN_SOUNDS_1P = [
+ "titan_healthbar_tier3_down_1P", // 0 left (doom)
+ "titan_healthbar_tier2_down_1P", // 1 left
+ "titan_healthbar_tier1_down_1P", // 2 left
+ "titan_healthbar_tier1_down_1P" // shield gone
+]
+
+const DAMAGE_FORGIVENESS_CEILING = 1200.0
+const DAMAGE_FORGIVENESS_FLOOR = 500.0
+const LOW_HEALTH_WARNING_SOUND = "Weapon_Vortex_Gun.ExplosiveWarningBeep"
+
+const TITAN_DAMAGE_MITIGATION_DAMAGESCALE = 0.5
+
+struct {
+ int shieldDecayRate = 2
+
+ table< entity, table< entity, float > > soulToSoulDamageMemory
+} file;
+
+function HealthRegenInit()
+{
+ if ( GetCurrentPlaylistVarInt( "titan_health_decay_amount", 0 ) > 0 )
+ {
+ AddSoulInitFunc( TitanHealthDecayThink )
+ }
+
+ AddSoulInitFunc( TitanHealthRegenThink )
+
+ if ( TitanShieldDecayEnabled() )
+ {
+ AddSoulInitFunc( TitanShieldDecayThink )
+ }
+
+ AddDamageCallback( "player", TitanSegmentedHealth_OnDamage )
+ AddDamageCallback( "npc_titan", TitanSegmentedHealth_OnDamage )
+ AddCallback_OnTitanDoomed( OnTitanDoomed )
+
+ RegisterSignal( "HealthSegmentLost" )
+}
+
+
+void function TitanHealthDecayThink( entity soul )
+{
+ thread TitanHealthDecayThinkInternal( soul )
+}
+
+void function TitanHealthDecayThinkInternal( entity soul )
+{
+ soul.EndSignal( "OnDestroy" )
+ soul.EndSignal( "OnTitanDeath" )
+
+ soul.SetShieldHealth( 0 )
+
+ while ( 1 )
+ {
+ entity titan = soul.GetTitan()
+ int damageAmout = GetCurrentPlaylistVarInt( "titan_health_decay_amount", 0 )
+ titan.TakeDamage( damageAmout, null, null, { scriptType = DF_DOOMED_HEALTH_LOSS, damageSourceId = damagedef_suicide } )
+ WaitFrame()
+ }
+}
+
+void function TitanHealthRegenThink( entity soul )
+{
+ thread TitanHealthRegenThink_Internal( soul )
+}
+
+void function TitanHealthRegenThink_Internal( entity soul )
+{
+ soul.EndSignal( SIGNAL_TITAN_HEALTH_REGEN )
+ soul.EndSignal( "OnTitanDeath" )
+ soul.EndSignal( "OnDestroy" )
+
+ if ( !soul.soul.regensHealth )
+ return
+
+ entity titan = soul.GetTitan()
+
+ if ( !IsValid( titan ) )
+ return
+
+ int healthPerTab = GetSegmentHealthForTitan( titan )
+
+ // set this if AI titans need to be aware of segment health. Not used currently
+ //titan.SetHealthPerSegment( healthPerTab )
+
+ int lastTitanHealth = titan.GetHealth()
+ bool regenSound = false
+ int maxHealth = titan.GetMaxHealth()
+ float lastTime = Time()
+
+ while ( 1 )
+ {
+ titan = soul.GetTitan()
+ if ( !IsAlive( titan ) )
+ return
+ int titanHealth = titan.GetHealth()
+ Assert( titan )
+
+ if ( !titan.IsTitan() )
+ return
+
+ if ( !soul.soul.regensHealth )
+ return
+
+ int currentRegenTab = GetTitanCurrentRegenTab( titan )
+
+ if ( currentRegenTab != GetSoulBatteryCount( soul ) )
+ SetSoulBatteryCount( soul, GetTitanCurrentRegenTab( titan ) )
+
+ int maxHealthForCurrentTab = currentRegenTab * healthPerTab
+
+ if ( titanHealth == maxHealthForCurrentTab )
+ {
+ if ( regenSound )
+ {
+ StopSoundOnEntity( titan, "titan_energyshield_up" )
+ regenSound = false
+ }
+ }
+
+ lastTitanHealth = titanHealth
+ lastTime = Time()
+ WaitFrame()
+ }
+}
+
+void function TitanSegmentedHealth_OnDamage( entity titan, var damageInfo )
+{
+ if ( !titan.IsTitan() )
+ return
+
+ entity soul = titan.GetTitanSoul()
+
+ if ( !IsValid( soul ) )
+ return
+
+ if ( ShouldReduceDamageForSegmentedHealth( soul, damageInfo ) )
+ DamageInfo_ScaleDamage( damageInfo, 0.3 )
+
+ thread TitanSegmentedHealth_OnDamage_Thread( soul, damageInfo )
+}
+
+bool function ShouldReduceDamageForSegmentedHealth( entity soul, damageInfo )
+{
+ if ( !soul.soul.rebooting )
+ return false
+
+ if ( IsRodeoDamageFromBatteryPack( soul, damageInfo ) )
+ return false
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS )
+ return false
+
+ if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOM_FATALITY )
+ return false
+
+ return true
+}
+
+function TitanSegmentedHealth_OnDamage_Thread( entity soul, damageInfo )
+{
+ soul.EndSignal( "OnTitanDeath" )
+ soul.EndSignal( "OnDestroy" )
+ soul.EndSignal( "Doomed" )
+
+ entity titan = soul.GetTitan()
+
+ vector damageOrigin = GetDamageOrigin( damageInfo, titan )
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ int hitBox = DamageInfo_GetHitBox( damageInfo )
+
+ int healthFloor = CalculateHealthFloorForDamage( soul, titan, damageInfo )
+
+ bool skipDoom = ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_SKIPS_DOOMED_STATE ) > 0
+
+
+ WaitEndFrame()
+
+ titan = soul.GetTitan()
+ Assert( IsValid( titan ) )
+
+ if ( soul.soul.lastSegmentLossTime >= Time() )
+ return
+
+ if ( GetDoomedState( titan ) )
+ return
+
+ if ( titan.GetHealth() > healthFloor )
+ return
+
+ if ( !IsAlive( titan ) )
+ return
+
+ string settings = GetSoulPlayerSettings( soul )
+ if ( Dev_GetPlayerSettingByKeyField_Global( settings, "use_damage_states" ) == 1 )
+ UpdateDamageStateForTab( titan, GetTitanCurrentRegenTab( titan ), hitBox )
+
+ thread TitanLoseSegement( soul, titan, damageOrigin, damageAmount, attacker )
+}
+
+int function CalculateHealthFloorForDamage( entity soul, entity titan, damageInfo )
+{
+ //Lets you bypass the health segment limitation and remove an entire health segment.
+ /*if ( IsRodeoDamageFromBatteryPack( soul, damageInfo ) )
+ return minint( 0, int ( titan.GetHealth() - DamageInfo_GetDamage( damageInfo ) ) )
+ */
+ int oldTab = GetTitanCurrentRegenTab( titan )
+ return ( oldTab - 1 ) * GetSegmentHealthForTitan( titan )
+}
+
+void function TitanLoseSegement( entity soul, entity titan, vector damageOrigin, float damageAmount, entity attacker )
+{
+ if ( !IsValid( soul ) )
+ return
+
+ if ( !IsValid( titan ) )
+ return
+
+ if ( soul.soul.lastSegmentLossTime >= Time() )
+ return
+
+ soul.soul.lastSegmentLossTime = Time()
+
+ entity player
+ if ( titan.IsPlayer() )
+ player = titan
+
+ foreach ( callbackFunc in svGlobal.onTitanHealthSegmentLostCallbacks )
+ {
+ callbackFunc( titan, attacker )
+ }
+
+ // Added via AddTitanCallback_OnHealthSegmentLost
+ foreach ( callbackFunc in titan.e.entSegmentLostCallbacks )
+ {
+ callbackFunc( titan, attacker )
+ }
+
+ GiveDefenderAmmo( titan )
+
+ titan.Signal( "HealthSegmentLost" )
+
+ soul.EndSignal( "OnTitanDeath" )
+ soul.EndSignal( "OnDestroy" )
+
+ if ( GetCurrentPlaylistVarInt( "titan_health_chicklet_fx", 0 ) == 1 )
+ TitanLoseSegementFX( titan, attacker, damageOrigin )
+
+ SetSoulBatteryCount( soul, GetTitanCurrentRegenTab( titan ) )
+}
+
+void function TitanLoseSegementFX( entity titan, entity attacker, vector damageOrigin )
+{
+ int handle = titan.GetEncodedEHandle()
+ int handleAttacker = -1
+
+ if ( IsValid( attacker ) )
+ handleAttacker = attacker.GetEncodedEHandle()
+
+ array<entity> players = GetPlayerArray()
+ foreach ( player in players )
+ {
+ Remote_CallFunction_Replay( player, "ServerCallback_TitanLostHealthSegment", handle, handleAttacker, damageOrigin.x, damageOrigin.y, damageOrigin.z )
+ }
+
+ if ( !IsAlive( titan ) )
+ return
+
+ int currentRegenTab = minint( GetTitanCurrentRegenTab( titan ), SEGMENT_DOWN_SOUNDS_3P_ATTACKER.len()-1 )
+ if ( currentRegenTab < SEGMENT_DOWN_SOUNDS_3P_ATTACKER.len() )
+ {
+ if ( titan.IsPlayer() && IsAlive( attacker ) && attacker.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( titan, attacker, SEGMENT_DOWN_SOUNDS_3P_ATTACKER[ currentRegenTab ] )
+ EmitSoundOnEntityOnlyToPlayer( titan, titan, SEGMENT_DOWN_SOUNDS_1P[ currentRegenTab ] )
+
+ // need a command here to play for not victim and not attacker
+ EmitSoundOnEntityExceptToPlayer( titan, titan, SEGMENT_DOWN_SOUNDS_3P[ currentRegenTab ] )
+ }
+ else if ( IsAlive( attacker ) && attacker.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( titan, attacker, SEGMENT_DOWN_SOUNDS_3P_ATTACKER[ currentRegenTab ] )
+ EmitSoundOnEntityExceptToPlayer( titan, attacker, SEGMENT_DOWN_SOUNDS_3P[ currentRegenTab ] )
+ }
+ else if ( titan.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( titan, titan, SEGMENT_DOWN_SOUNDS_1P[ currentRegenTab ] )
+ EmitSoundOnEntityExceptToPlayer( titan, titan, SEGMENT_DOWN_SOUNDS_3P[ currentRegenTab ] )
+ }
+ else
+ {
+ EmitSoundOnEntity( titan, SEGMENT_DOWN_SOUNDS_3P[ currentRegenTab ] )
+ }
+ }
+}
+
+
+void function UpdateDamageStateForTab( entity titan, int tab, int hitBox )
+{
+ if ( hitBox == -1 ) // not every hitbox has data defined
+ return
+
+ var bodyGroup = titan.GetBodyGroupNameFromHitboxId( hitBox ) // can be null
+
+ if ( bodyGroup == null )
+ {
+ printt( "bodyGroup was null" )
+ return
+ }
+
+#if MP
+ // these are flipped on purpose to prevent both legs or arms from being blown up
+ switch ( bodyGroup )
+ {
+ case "left_leg":
+ if ( IsBroken( titan, "right_leg" ) )
+ return
+ break
+
+ case "right_leg":
+ if ( IsBroken( titan, "left_leg" ) )
+ return
+ break
+
+ case "left_arm":
+ if ( IsBroken( titan, "right_arm" ) )
+ return
+ break
+
+ case "right_arm":
+ if ( IsBroken( titan, "left_arm" ) )
+ return
+ break
+
+ default:
+ return
+ }
+
+ GibBodyPart( titan, bodyGroup )
+#else
+ int maxTab = 3
+ int count = maxTab - tab
+
+ RecursiveGibBodyPart( titan, bodyGroup, count )
+#endif
+}
+
+void function RecursiveGibBodyPart( entity titan, var bodyGroup, int count )
+{
+ GibBodyPart( titan, bodyGroup )
+
+ count -= 1
+ if ( count <= 0 )
+ return
+
+ foreach ( siblingName in titan.s.skeletonData[bodyGroup].siblings )
+ {
+ // printt( count + " recurse: " + siblingName )
+ RecursiveGibBodyPart( titan, siblingName, count )
+ }
+}
+
+bool function IsBroken( entity titan, var bodyGroup )
+{
+ local bodyGroupIndex = titan.FindBodyGroup( bodyGroup )
+ local stateCount = GetStateCountForBodyGroup( titan, bodyGroup )
+ local bodyGroupState = titan.GetBodyGroupState( bodyGroupIndex )
+
+ //return ( bodyGroupState >= (stateCount - 1) )
+ return ( bodyGroupState > 0 )
+}
+
+void function GibBodyPart( entity titan, var bodyGroup )
+{
+ // if ( IsBodyGroupBroken( titan, bodyGroup ) )
+ // return
+
+ // titan.s.damageStateInfo[bodyGroup] = 1
+
+ local bodyGroupIndex = titan.FindBodyGroup( bodyGroup )
+ local stateCount = GetStateCountForBodyGroup( titan, bodyGroup )
+ local bodyGroupState = titan.GetBodyGroupState( bodyGroupIndex )
+
+ if ( bodyGroupState >= (stateCount - 1) )
+ return
+
+ titan.SetBodygroup( bodyGroupIndex, bodyGroupState + 1 )
+ // printt( "break: " + bodyGroup )
+}
+
+function GiveAttackerAmmo( entity titan )
+{
+}
+
+void function TemporaryInvul( entity titan )
+{
+ titan.EndSignal( "OnDestroy" )
+ titan.EndSignal( "OnDeath" )
+ if ( titan.IsPlayer() )
+ {
+ titan.EndSignal( "DisembarkingTitan" )
+ titan.EndSignal( "TitanEjectionStarted" )
+ }
+
+ OnThreadEnd(
+ function() : ( titan )
+ {
+ if ( IsValid( titan ) )
+ titan.ClearInvulnerable()
+ }
+ )
+
+ titan.SetInvulnerable()
+ wait 0.25
+}
+
+void function GiveDefenderAmmo( entity titan )
+{
+ entity soul = titan.GetTitanSoul()
+
+ if ( IsSingleplayer() )
+ {
+ if ( titan.IsNPC() )
+ {
+ soul.SetNextCoreChargeAvailable( soul.GetNextCoreChargeAvailable() + 0.5 ) // shave time off core timer
+ }
+ }
+}
+
+void function OnTitanDoomed( entity titan, var damageInfo )
+{
+
+ if ( !IsAlive( titan ) )
+ return
+
+ entity soul = titan.GetTitanSoul()
+
+ if ( titan.IsPlayer() )
+ {
+ if ( SoulHasPassive( soul, ePassives.PAS_RONIN_AUTOSHIFT ) )
+ PhaseShift( titan, 0, 3.0 )
+
+ if ( SoulHasPassive( soul, ePassives.PAS_AUTO_EJECT ) )
+ return
+ }
+
+ soul.nextHealthRegenTime = Time()
+
+ vector damageOrigin = GetDamageOrigin( damageInfo, titan )
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ if ( TitanDamageRewardsTitanCoreTime() && (titan != attacker) )
+ {
+ AddCreditToTitanCoreBuilderForDoomEntered( titan )
+ if ( attacker.IsTitan() )
+ AddCreditToTitanCoreBuilderForDoomInflicted( attacker )
+ }
+
+ thread TitanLoseSegement( soul, titan, damageOrigin, damageAmount, attacker )
+
+ if ( SoulHasPassive( soul, ePassives.PAS_DOOMED_TIME ) )
+ return
+
+ if ( NoWeaponDoomState() )
+ TakeAllWeapons( titan )
+}
+
+void function OnTitanDeath( entity titan, var damageInfo )
+{
+ if ( !titan.IsTitan() )
+ return
+
+ if ( !PROTO_AlternateDoomedState() )
+ return
+
+ entity soul = titan.GetTitanSoul()
+ vector damageOrigin = GetDamageOrigin( damageInfo, titan )
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ thread TitanLoseSegement( soul, titan, damageOrigin, damageAmount, attacker )
+}
+
+void function TitanShieldDecayThink( entity soul )
+{
+ thread TitanShieldDecayThinkInternal( soul )
+}
+
+void function TitanShieldDecayThinkInternal( entity soul )
+{
+ soul.EndSignal( "OnDestroy" ) //This needs to be OnDestroy instead of OnDeath because souls don't have a death animation
+ soul.EndSignal( "OnTitanDeath" )
+
+ while ( 1 )
+ {
+ if ( Time() >= soul.e.nextShieldDecayTime && !TitanHasRegenningShield( soul ) )
+ soul.SetShieldHealth( maxint( soul.GetShieldHealth() - file.shieldDecayRate, 0 ) )
+ WaitFrame()
+ }
+} \ No newline at end of file
diff --git a/Northstar.CustomServers/scripts/vscripts/titan/class_titan.gnut b/Northstar.CustomServers/scripts/vscripts/titan/class_titan.gnut
new file mode 100644
index 00000000..5f72385e
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/titan/class_titan.gnut
@@ -0,0 +1,77 @@
+untyped
+
+global function ClassTitan_Init
+
+global function Titan_AddPlayer
+global function Titan_OnPlayerDeath
+global function ClientCommand_TitanEject
+global function ApplyTitanLoadoutModifiers
+
+
+const TITAN_HATCHCOMMANDANIMTIME = 1.5 // cooldown time between toggling the cockpit state. Will be needed when we have animations to play
+
+const COCKPIT_JOLT_DAMAGE_MIN = 1
+const COCKPIT_JOLT_DAMAGE_MAX = 200
+const TITAN_STUMBLE_HEALTH_PERCENTAGE = 0.5
+
+string thisClassName = "titan"
+
+function ClassTitan_Init()
+{
+
+ AddClientCommandCallback( "TitanEject", ClientCommand_TitanEject ) //
+}
+
+function Titan_AddPlayer( player )
+{
+ player.playerClassData[thisClassName] <- {}
+ player.s.lastStaggerTime <- 0
+}
+
+
+// TODO: There should be an equivalent function for pilots
+TitanLoadoutDef function ApplyTitanLoadoutModifiers( entity player, TitanLoadoutDef loadout )
+{
+ return loadout
+}
+
+void function Titan_OnPlayerDeath( entity player, var damageInfo )
+{
+ player.p.storedWeapons.clear()
+}
+
+bool function PlayerCanEject( entity player )
+{
+ if ( !IsAlive( player ) )
+ return false
+
+ if ( !player.IsTitan() )
+ return false
+
+ if ( Riff_TitanExitEnabled() == eTitanExitEnabled.Never )
+ return false
+
+ //if ( !CanDisembark( player ) )
+ // return false
+
+ if ( IsPlayerDisembarking( player ) )
+ return false
+
+ if ( TitanEjectIsDisabled() )
+ return false
+
+ return true
+}
+
+bool function ClientCommand_TitanEject( entity player, array<string> args )
+{
+ if ( !PlayerCanEject( player ) )
+ return true
+
+ int ejectPressCount = args[ 0 ].tointeger()
+ if ( ejectPressCount < 3 )
+ return true
+
+ thread TitanEjectPlayer( player )
+ return true
+}