From 207facbc402f5639cbcd31f079214351ef605cf2 Mon Sep 17 00:00:00 2001 From: BobTheBob <32057864+BobTheBob9@users.noreply.github.com> Date: Tue, 22 Jun 2021 14:30:49 +0100 Subject: initial commit after moving to new repo --- .../scripts/vscripts/titan/_battery_generator.gnut | 128 +++ .../vscripts/titan/_replacement_titans.gnut | 1183 ++++++++++++++++++++ .../vscripts/titan/_replacement_titans_drop.gnut | 443 ++++++++ .../scripts/vscripts/titan/_titan_commands.gnut | 49 + .../scripts/vscripts/titan/_titan_health.gnut | 1072 ++++++++++++++++++ .../scripts/vscripts/titan/_titan_hints.gnut | 267 +++++ .../scripts/vscripts/titan/_titan_hotdrop.gnut | 778 +++++++++++++ .../vscripts/titan/_titan_triple_health.gnut | 524 +++++++++ .../scripts/vscripts/titan/class_titan.gnut | 77 ++ 9 files changed, 4521 insertions(+) create mode 100644 Northstar.CustomServers/scripts/vscripts/titan/_battery_generator.gnut create mode 100644 Northstar.CustomServers/scripts/vscripts/titan/_replacement_titans.gnut create mode 100644 Northstar.CustomServers/scripts/vscripts/titan/_replacement_titans_drop.gnut create mode 100644 Northstar.CustomServers/scripts/vscripts/titan/_titan_commands.gnut create mode 100644 Northstar.CustomServers/scripts/vscripts/titan/_titan_health.gnut create mode 100644 Northstar.CustomServers/scripts/vscripts/titan/_titan_hints.gnut create mode 100644 Northstar.CustomServers/scripts/vscripts/titan/_titan_hotdrop.gnut create mode 100644 Northstar.CustomServers/scripts/vscripts/titan/_titan_triple_health.gnut create mode 100644 Northstar.CustomServers/scripts/vscripts/titan/class_titan.gnut (limited to 'Northstar.CustomServers/scripts/vscripts/titan') 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 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 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 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 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 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 currentTitanMods + + if ( player.IsBot() ) + { + string botTitanSettings = GetConVarString( "bot_titan_settings" ) + array 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 playerPauseStartTimes + +} protoFile + + +void function PauseTitansThink() +{ + bool titan + while ( true ) + { + array 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 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( ) + 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 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 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 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 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 titanHintThresholds + table titanHintThresholdAdd + table 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 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 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 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 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 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 args ) +{ + if ( !PlayerCanEject( player ) ) + return true + + int ejectPressCount = args[ 0 ].tointeger() + if ( ejectPressCount < 3 ) + return true + + thread TitanEjectPlayer( player ) + return true +} -- cgit v1.2.3