445 lines
12 KiB
Plaintext
445 lines
12 KiB
Plaintext
global function OnWeaponPrimaryAttack_holopilot
|
|
global function PlayerCanUseDecoy
|
|
|
|
global const int DECOY_FADE_DISTANCE = 16000 //Really just an arbitrarily large number
|
|
global const float DECOY_DURATION = 10.0
|
|
|
|
global const vector HOLOPILOT_ANGLE_SEGMENT = < 0, 25, 0 >
|
|
|
|
#if SERVER
|
|
global function CodeCallback_PlayerDecoyDie
|
|
global function CodeCallback_PlayerDecoyDissolve
|
|
global function CodeCallback_PlayerDecoyRemove
|
|
global function CodeCallback_PlayerDecoyStateChange
|
|
global function CreateHoloPilotDecoys
|
|
global function SetupDecoy_Common
|
|
|
|
global function Decoy_Init
|
|
|
|
#if MP
|
|
global function GetDecoyActiveCountForPlayer
|
|
#endif //if MP
|
|
|
|
#endif //if server
|
|
|
|
const float PHASE_REWIND_PATH_SNAPSHOT_INTERVAL = 0.1
|
|
struct
|
|
{
|
|
table< entity, int > playerToDecoysActiveTable //Mainly used to track stat for holopilot unlock
|
|
}
|
|
file
|
|
|
|
global table< entity > playerDecoyList //CUSTOM used to track the decoy the user will be teleported to
|
|
|
|
|
|
|
|
#if SERVER
|
|
|
|
void function Decoy_Init()
|
|
{
|
|
#if MP
|
|
RegisterSignal( "CleanupFXAndSoundsForDecoy" )
|
|
#endif
|
|
}
|
|
|
|
void function CleanupExistingDecoy( entity decoy )
|
|
{
|
|
if ( IsValid( decoy ) ) //This cleanup function is called from multiple places, so check that decoy is still valid before we try to clean it up again
|
|
{
|
|
decoy.Decoy_Dissolve()
|
|
CleanupFXAndSoundsForDecoy( decoy )
|
|
}
|
|
}
|
|
|
|
void function CleanupFXAndSoundsForDecoy( entity decoy )
|
|
{
|
|
if ( !IsValid( decoy ) )
|
|
return
|
|
|
|
decoy.Signal( "CleanupFXAndSoundsForDecoy" )
|
|
|
|
foreach( fx in decoy.decoy.fxHandles )
|
|
{
|
|
if ( IsValid( fx ) )
|
|
{
|
|
fx.ClearParent()
|
|
EffectStop( fx )
|
|
}
|
|
}
|
|
|
|
decoy.decoy.fxHandles.clear() //probably not necessary since decoy is already being cleaned up, just for throughness.
|
|
|
|
foreach ( loopingSound in decoy.decoy.loopingSounds )
|
|
{
|
|
StopSoundOnEntity( decoy, loopingSound )
|
|
}
|
|
|
|
decoy.decoy.loopingSounds.clear()
|
|
}
|
|
|
|
void function OnHoloPilotDestroyed( entity decoy )
|
|
{
|
|
EmitSoundAtPosition( TEAM_ANY, decoy.GetOrigin(), "holopilot_end_3P" )
|
|
|
|
entity bossPlayer = decoy.GetBossPlayer()
|
|
if ( IsValid( bossPlayer ) ){
|
|
if(bossPlayer in playerDecoyList){
|
|
if(decoy == playerDecoyList[bossPlayer])
|
|
delete playerDecoyList[bossPlayer]
|
|
}
|
|
EmitSoundOnEntityOnlyToPlayer( bossPlayer, bossPlayer, "holopilot_end_1P" )
|
|
}
|
|
CleanupFXAndSoundsForDecoy( decoy )
|
|
}
|
|
|
|
void function CodeCallback_PlayerDecoyDie( entity decoy, int currentState ) //All Die does is play the death animation. Eventually calls CodeCallback_PlayerDecoyDissolve too
|
|
{
|
|
//PrintFunc()
|
|
OnHoloPilotDestroyed( decoy )
|
|
}
|
|
|
|
void function CodeCallback_PlayerDecoyDissolve( entity decoy, int currentState )
|
|
{
|
|
//PrintFunc()
|
|
OnHoloPilotDestroyed( decoy )
|
|
}
|
|
|
|
|
|
void function CodeCallback_PlayerDecoyRemove( entity decoy, int currentState )
|
|
{
|
|
//PrintFunc()
|
|
}
|
|
|
|
|
|
void function CodeCallback_PlayerDecoyStateChange( entity decoy, int previousState, int currentState )
|
|
{
|
|
//PrintFunc()
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
var function OnWeaponPrimaryAttack_holopilot( entity weapon, WeaponPrimaryAttackParams attackParams )
|
|
{
|
|
entity weaponOwner = weapon.GetWeaponOwner()
|
|
Assert( weaponOwner.IsPlayer() )
|
|
|
|
if ( !PlayerCanUseDecoy( weaponOwner ) )
|
|
return 0
|
|
|
|
#if SERVER
|
|
if ( weaponOwner in playerDecoyList ){
|
|
CreateHoloPilotDecoys( weaponOwner, 1 )
|
|
entity decoy = playerDecoyList[ weaponOwner ]
|
|
if(weaponOwner.GetOffhandWeapon(1).GetWeaponClassName() == "mp_ability_holopilot"){
|
|
weaponOwner.GetOffhandWeapon(1).SetWeaponPrimaryClipCount(0)
|
|
}
|
|
PlayerUsesHoloRewind(weaponOwner, decoy)
|
|
// print("teleporting to"+decoy)
|
|
// print("Origin"+decoy.GetOrigin())
|
|
// print("Angles"+decoy.GetAngles())
|
|
// print("Velocity"+decoy.GetVelocity())
|
|
// weaponOwner.SetOrigin(decoy.GetOrigin())
|
|
// weaponOwner.SetAngles(decoy.GetAngles())
|
|
// weaponOwner.SetVelocity(decoy.GetVelocity())
|
|
}else{
|
|
entity decoy = CreateHoloPilotDecoys( weaponOwner, 1 )
|
|
playerDecoyList[ weaponOwner ] <- decoy
|
|
}
|
|
#else
|
|
Rumble_Play( "rumble_holopilot_activate", {} )
|
|
#endif
|
|
|
|
PlayerUsedOffhand( weaponOwner, weapon )
|
|
|
|
return weapon.GetWeaponSettingInt( eWeaponVar.ammo_min_to_fire )
|
|
}
|
|
|
|
#if SERVER
|
|
|
|
entity function CreateHoloPilotDecoys( entity player, int numberOfDecoysToMake = 1 )
|
|
{
|
|
Assert( numberOfDecoysToMake > 0 )
|
|
Assert( player )
|
|
|
|
float displacementDistance = 30.0
|
|
|
|
bool setOriginAndAngles = numberOfDecoysToMake > 1
|
|
|
|
float stickPercentToRun = 0.65
|
|
if ( setOriginAndAngles )
|
|
stickPercentToRun = 0.0
|
|
|
|
entity decoy
|
|
for( int i = 0; i < numberOfDecoysToMake; ++i )
|
|
{
|
|
decoy = player.CreatePlayerDecoy( stickPercentToRun )
|
|
decoy.SetMaxHealth( 50 )
|
|
decoy.SetHealth( 50 )
|
|
decoy.EnableAttackableByAI( 50, 0, AI_AP_FLAG_NONE )
|
|
SetObjectCanBeMeleed( decoy, true )
|
|
decoy.SetTimeout( DECOY_DURATION )
|
|
|
|
if ( setOriginAndAngles )
|
|
{
|
|
vector angleToAdd = CalculateAngleSegmentForDecoy( i, HOLOPILOT_ANGLE_SEGMENT )
|
|
vector normalizedAngle = player.GetAngles() + angleToAdd
|
|
normalizedAngle.y = AngleNormalize( normalizedAngle.y ) //Only care about changing the yaw
|
|
decoy.SetAngles( normalizedAngle )
|
|
|
|
vector forwardVector = AnglesToForward( normalizedAngle )
|
|
forwardVector *= displacementDistance
|
|
decoy.SetOrigin( player.GetOrigin() + forwardVector ) //Using player origin instead of decoy origin as defensive fix, see bug 223066
|
|
PutEntityInSafeSpot( decoy, player, null, player.GetOrigin(), decoy.GetOrigin() )
|
|
}
|
|
|
|
SetupDecoy_Common( player, decoy )
|
|
|
|
#if MP
|
|
thread MonitorDecoyActiveForPlayer( decoy, player )
|
|
#endif
|
|
}
|
|
|
|
if(numberOfDecoysToMake == 1){
|
|
return decoy
|
|
}
|
|
#if BATTLECHATTER_ENABLED
|
|
PlayBattleChatterLine( player, "bc_pHolo" )
|
|
#endif
|
|
}
|
|
|
|
void function SetupDecoy_Common( entity player, entity decoy ) //functioned out mainly so holopilot execution can call this as well
|
|
{
|
|
decoy.SetDeathNotifications( true )
|
|
decoy.SetPassThroughThickness( 0 )
|
|
decoy.SetNameVisibleToOwner( true )
|
|
decoy.SetNameVisibleToFriendly( true )
|
|
decoy.SetNameVisibleToEnemy( true )
|
|
decoy.SetDecoyRandomPulseRateMax( 0.5 ) //pulse amount per second
|
|
decoy.SetFadeDistance( DECOY_FADE_DISTANCE )
|
|
|
|
int friendlyTeam = decoy.GetTeam()
|
|
EmitSoundOnEntityToTeam( decoy, "holopilot_loop", friendlyTeam ) //loopingSound
|
|
EmitSoundOnEntityToEnemies( decoy, "holopilot_loop_enemy", friendlyTeam ) ///loopingSound
|
|
decoy.decoy.loopingSounds = [ "holopilot_loop", "holopilot_loop_enemy" ]
|
|
|
|
Highlight_SetFriendlyHighlight( decoy, "friendly_player_decoy" )
|
|
Highlight_SetOwnedHighlight( decoy, "friendly_player_decoy" )
|
|
decoy.e.hasDefaultEnemyHighlight = true
|
|
SetDefaultMPEnemyHighlight( decoy )
|
|
|
|
int attachID = decoy.LookupAttachment( "CHESTFOCUS" )
|
|
|
|
#if MP
|
|
var childEnt = player.FirstMoveChild()
|
|
while ( childEnt != null )
|
|
{
|
|
expect entity( childEnt )
|
|
|
|
bool isBattery = false
|
|
bool createHologram = false
|
|
switch( childEnt.GetClassName() )
|
|
{
|
|
case "item_titan_battery":
|
|
{
|
|
isBattery = true
|
|
createHologram = true
|
|
break
|
|
}
|
|
|
|
case "item_flag":
|
|
{
|
|
createHologram = true
|
|
break
|
|
}
|
|
}
|
|
|
|
asset modelName = childEnt.GetModelName()
|
|
if ( createHologram && modelName != $"" && childEnt.GetParentAttachment() != "" )
|
|
{
|
|
entity decoyChildEnt = CreatePropDynamic( modelName, <0, 0, 0>, <0, 0, 0>, 0 )
|
|
decoyChildEnt.Highlight_SetInheritHighlight( true )
|
|
decoyChildEnt.SetParent( decoy, childEnt.GetParentAttachment() )
|
|
|
|
if ( isBattery )
|
|
thread Decoy_BatteryFX( decoy, decoyChildEnt )
|
|
else
|
|
thread Decoy_FlagFX( decoy, decoyChildEnt )
|
|
}
|
|
|
|
childEnt = childEnt.NextMovePeer()
|
|
}
|
|
#endif // MP
|
|
|
|
entity holoPilotTrailFX = StartParticleEffectOnEntity_ReturnEntity( decoy, HOLO_PILOT_TRAIL_FX, FX_PATTACH_POINT_FOLLOW, attachID )
|
|
SetTeam( holoPilotTrailFX, friendlyTeam )
|
|
holoPilotTrailFX.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY
|
|
|
|
decoy.decoy.fxHandles.append( holoPilotTrailFX )
|
|
decoy.SetFriendlyFire( false )
|
|
decoy.SetKillOnCollision( false )
|
|
}
|
|
|
|
vector function CalculateAngleSegmentForDecoy( int loopIteration, vector angleSegment )
|
|
{
|
|
if ( loopIteration == 0 )
|
|
return < 0, 0, 0 >
|
|
|
|
if ( loopIteration % 2 == 0 )
|
|
return ( loopIteration / 2 ) * angleSegment * -1
|
|
else
|
|
return ( ( loopIteration / 2 ) + 1 ) * angleSegment
|
|
|
|
unreachable
|
|
}
|
|
|
|
#if MP
|
|
void function Decoy_BatteryFX( entity decoy, entity decoyChildEnt )
|
|
{
|
|
decoy.EndSignal( "OnDeath" )
|
|
decoy.EndSignal( "CleanupFXAndSoundsForDecoy" )
|
|
Battery_StartFX( decoyChildEnt )
|
|
|
|
OnThreadEnd(
|
|
function() : ( decoyChildEnt )
|
|
{
|
|
Battery_StopFX( decoyChildEnt )
|
|
if ( IsValid( decoyChildEnt ) )
|
|
decoyChildEnt.Destroy()
|
|
}
|
|
)
|
|
|
|
WaitForever()
|
|
}
|
|
|
|
void function Decoy_FlagFX( entity decoy, entity decoyChildEnt )
|
|
{
|
|
decoy.EndSignal( "OnDeath" )
|
|
decoy.EndSignal( "CleanupFXAndSoundsForDecoy" )
|
|
|
|
SetTeam( decoyChildEnt, decoy.GetTeam() )
|
|
entity flagTrailFX = StartParticleEffectOnEntity_ReturnEntity( decoyChildEnt, GetParticleSystemIndex( FLAG_FX_ENEMY ), FX_PATTACH_POINT_FOLLOW, decoyChildEnt.LookupAttachment( "fx_end" ) )
|
|
flagTrailFX.kv.VisibilityFlags = ENTITY_VISIBLE_TO_ENEMY
|
|
|
|
OnThreadEnd(
|
|
function() : ( flagTrailFX, decoyChildEnt )
|
|
{
|
|
if ( IsValid( flagTrailFX ) )
|
|
flagTrailFX.Destroy()
|
|
|
|
if ( IsValid( decoyChildEnt ) )
|
|
decoyChildEnt.Destroy()
|
|
}
|
|
)
|
|
|
|
WaitForever()
|
|
}
|
|
|
|
void function MonitorDecoyActiveForPlayer( entity decoy, entity player )
|
|
{
|
|
if ( player in file.playerToDecoysActiveTable )
|
|
++file.playerToDecoysActiveTable[ player ]
|
|
else
|
|
file.playerToDecoysActiveTable[ player ] <- 1
|
|
|
|
decoy.EndSignal( "OnDestroy" ) //Note that we do this OnDestroy instead of the inbuilt OnHoloPilotDestroyed() etc functions so there is a bit of leeway after the holopilot starts to die/is fully invisible before being destroyed
|
|
player.EndSignal( "OnDestroy" )
|
|
|
|
OnThreadEnd(
|
|
function() : ( player )
|
|
{
|
|
if( IsValid( player ) )
|
|
{
|
|
Assert( player in file.playerToDecoysActiveTable )
|
|
--file.playerToDecoysActiveTable[ player ]
|
|
}
|
|
|
|
}
|
|
)
|
|
|
|
WaitForever()
|
|
}
|
|
|
|
int function GetDecoyActiveCountForPlayer( entity player )
|
|
{
|
|
if ( !(player in file.playerToDecoysActiveTable ))
|
|
return 0
|
|
|
|
return file.playerToDecoysActiveTable[player ]
|
|
}
|
|
|
|
#endif // MP
|
|
#endif // SERVER
|
|
|
|
bool function PlayerCanUseDecoy( entity ownerPlayer ) //For holopilot and HoloPilot Nova. No better place to put this for now
|
|
{
|
|
if ( !ownerPlayer.IsZiplining() )
|
|
{
|
|
if ( ownerPlayer.IsTraversing() )
|
|
return false
|
|
|
|
if ( ownerPlayer.ContextAction_IsActive() ) //Stops every single context action from letting decoy happen, including rodeo, melee, embarking etc
|
|
return false
|
|
}
|
|
|
|
if(ownerPlayer.GetOffhandWeapon(1).GetWeaponClassName() == "mp_ability_holopilot" && ownerPlayer.GetOffhandWeapon(1).GetWeaponPrimaryClipCount()<100)
|
|
return false
|
|
|
|
if(ownerPlayer.GetOffhandWeapon(1).GetWeaponClassName() == "mp_ability_holopilot" && ownerPlayer.GetOffhandWeapon(1).GetWeaponPrimaryClipCount()<200 && !(ownerPlayer in playerDecoyList))
|
|
return false
|
|
//Do we need to check isPhaseShifted here? Re-examine when it's possible to get both Phase and Decoy (maybe through burn cards?)
|
|
if(ownerPlayer.IsPhaseShifted())
|
|
return false
|
|
return true
|
|
}
|
|
|
|
void function PlayerUsesHoloRewind( entity player, entity decoy )
|
|
{
|
|
thread PlayerUsesHoloRewindThreaded( player, decoy )
|
|
}
|
|
|
|
void function PlayerUsesHoloRewindThreaded( entity player, entity decoy )
|
|
{
|
|
player.EndSignal( "OnDestroy" )
|
|
player.EndSignal( "OnDeath" )
|
|
decoy.EndSignal("OnDestroy")
|
|
decoy.EndSignal("OnDeath")
|
|
entity mover = CreateScriptMover( player.GetOrigin(), player.GetAngles() )
|
|
player.SetParent( mover, "REF" )
|
|
|
|
OnThreadEnd( function() : ( player, mover, decoy )
|
|
{
|
|
player.SetOrigin(decoy.GetOrigin())
|
|
player.SetAngles(decoy.GetAngles())
|
|
player.SetVelocity(decoy.GetVelocity())
|
|
CancelPhaseShift( player )
|
|
player.DeployWeapon()
|
|
player.SetPredictionEnabled( true )
|
|
player.ClearParent()
|
|
ViewConeFree( player )
|
|
mover.Destroy()
|
|
CleanupExistingDecoy(decoy)
|
|
})
|
|
|
|
vector initial_origin = player.GetOrigin()
|
|
vector initial_angle = player.GetAngles()
|
|
array<PhaseRewindData> positions = clone player.p.burnCardPhaseRewindStruct.phaseRetreatSavedPositions
|
|
|
|
ViewConeZero( player )
|
|
player.HolsterWeapon()
|
|
player.SetPredictionEnabled( false )
|
|
PhaseShift( player, 0.0, 7 * PHASE_REWIND_PATH_SNAPSHOT_INTERVAL * 1.5 )
|
|
|
|
for ( float i = 7; i > 0; i-- )
|
|
{
|
|
initial_origin -= (initial_origin-decoy.GetOrigin())*(1/i)
|
|
initial_angle -= (initial_angle-decoy.GetAngles())*(1/i)
|
|
mover.NonPhysicsMoveTo( initial_origin, PHASE_REWIND_PATH_SNAPSHOT_INTERVAL, 0, 0 )
|
|
mover.NonPhysicsRotateTo( initial_angle, PHASE_REWIND_PATH_SNAPSHOT_INTERVAL, 0, 0 )
|
|
wait PHASE_REWIND_PATH_SNAPSHOT_INTERVAL
|
|
}
|
|
|
|
mover.NonPhysicsMoveTo( decoy.GetOrigin(), PHASE_REWIND_PATH_SNAPSHOT_INTERVAL, 0, 0 )
|
|
mover.NonPhysicsRotateTo( decoy.GetAngles(), PHASE_REWIND_PATH_SNAPSHOT_INTERVAL, 0, 0 )
|
|
player.SetVelocity( decoy.GetVelocity() )
|
|
} |