diff --git a/keyvalues/scripts/weapons/mp_ability_holopilot.txt b/keyvalues/scripts/weapons/mp_ability_holopilot.txt new file mode 100644 index 0000000..0508b59 --- /dev/null +++ b/keyvalues/scripts/weapons/mp_ability_holopilot.txt @@ -0,0 +1,137 @@ +WeaponData +{ + // General + "printname" "#WPN_HOLOPILOT" + "shortprintname" "#WPN_HOLOPILOT" + "description" "#WPN_HOLOPILOT_DESC" + "longdesc" "#WPN_HOLOPILOT_LONGDESC" + + "menu_icon" "rui/pilot_loadout/tactical/pilot_tactical_holo_pilot" + "hud_icon" "rui/pilot_loadout/tactical/pilot_tactical_holo_pilot" + + "weaponClass" "human" + "fire_mode" "offhand_instant" + "offhand_default_inventory_slot" "1" + //"offhand_interupts_weapon_anims" "1" + + "OnWeaponPrimaryAttack" "OnWeaponPrimaryAttack_holopilot" + + // Models + //"viewmodel" "models/weapons/clacker_detonator/ptpov_clacker_detonator.mdl" + "playermodel" "models/weapons/empty_handed/w_empty_handed_human.mdl" + //"projectilemodel" "models/dev/empty_model.mdl" + "leveled_pickup" "1" + "dev_menu_type" "offhand" // fire_mode isn't readable from script + + "ammo_suck_behavior" "offhand_weapons" + + "sound_weapon_ready" "HUD_kit_meter_replenished_1P" + + MP_BASE + { + "ammo_clip_size" "200" + "ammo_default_total" "200" + "ammo_display" "bar" + "ammo_min_to_fire" "0" + "ammo_per_shot" "0" + "ammo_stockpile_max" "200" + "fire_rate" "2" + "regen_ammo_refill_rate" "0" + + "enable_highlight_networking_on_creation" "" + } + + SP_BASE + { + "ammo_clip_size" "200" + "ammo_default_total" "200" + "ammo_display" "bar" + "ammo_min_to_fire" "50" + "ammo_per_shot" "50" + "ammo_stockpile_max" "200" + "fire_rate" "1000" + "regen_ammo_refill_rate" "0" + + "enable_highlight_networking_on_creation" "1" + } + + + // Regen Ammo + "regen_ammo_refill_start_delay" "0.0" + + // Damage + "damage_type" "none" + "explosion_damage_heavy_armor" "0" + "damage_near_distance" "0" + "damage_far_distance" "0" + "damage_near_value" "0" + "damage_far_value" "0" + + // Sounds + "sound_dryfire" "coop_sentrygun_deploymentdeniedbeep" + "battle_chatter_event" "bc_pHolo" + "fire_sound_1_player_1p" "holopilot_deploy_1p" + "fire_sound_1_player_3p" "holopilot_deploy_3p" + + // Rumble + "rumble" "-1" + + // Behavior + "cooldown_type" "ammo_instant" + "holster_time" "0.15" + "deploy_time" "0.25" + "lower_time" "0.25" + "raise_time" "0.2" + "allow_empty_fire" "0" + "reload_enabled" "0" + "empty_reload_only" "0" + "allow_empty_click" "0" + "trigger_snipercam" "0" + "allow_headshots" "0" + "breaks_cloak" "0" + "primary_fire_does_not_block_sprint" "1" + "aimassist_disable_hipfire" "1" + "aimassist_disable_ads" "1" + // Crosshair + + Mods + { + pas_power_cell + { + "regen_ammo_refill_rate" "*1.35" + } + + dev_mod_low_recharge + { + "regen_ammo_refill_rate" "*10" + } + amped_tacticals + { + "ammo_clip_size" "200" + "ammo_default_total" "200" + "ammo_stockpile_max" "200" + "regen_ammo_refill_rate" "12" + } + } + + "ordnance_crosshair_always_on_start_index" "0" + + RUI_CrosshairData + { + DefaultArgs + { + crosshairMovementX crosshair_movement_x + crosshairMovementY crosshair_movement_y + } + + Crosshair_2 + { + "ui" "ui/crosshair_tactical" + Args + { + ammoFrac "progress_weapon_clip_ammo_frac" + dryfireTime "weapon_latest_dryfire_time" + } + } + } +} \ No newline at end of file diff --git a/mod.json b/mod.json new file mode 100644 index 0000000..b80d735 --- /dev/null +++ b/mod.json @@ -0,0 +1,18 @@ +{ + "Name": "Legonzaur.HoloDisplace", + "Description": "Swap positions with your beloved decoy", + "Version": "0.1", + "LoadPriority": 2, + "Scripts": [ + { + "Path": "holodisplace_recharge.gnut", + "RunOn": "MP", + "ClientCallback": { + "After": "HoloDisplace_Init" + }, + "ServerCallback": { + "After": "HoloDisplace_Init" + } + } + ] +} diff --git a/mod/scripts/vscripts/holodisplace_recharge.gnut b/mod/scripts/vscripts/holodisplace_recharge.gnut new file mode 100644 index 0000000..059c0a4 --- /dev/null +++ b/mod/scripts/vscripts/holodisplace_recharge.gnut @@ -0,0 +1,58 @@ +global function HoloDisplace_Init + +void function HoloDisplace_Init() { + #if SERVER + AddCallback_GameStateEnter( eGameState.Playing, HoloRecharge ) + #endif +} + + +int rechargeStep = 1 //defines speed at which counter is added or removed +float rechargeInterval = 1/20 +int charge_size = 100 +int charge_max = 200 +int recharge_remove = 1 + +#if SERVER + void function HoloRecharge(){ + thread HoloRecharge_Threaded() + } + + void function HoloRecharge_Threaded(){ + while(true){ + array players = GetPlayerArray() + foreach (entity player in players) + { + if ( IsValid(player) && IsAlive( player )) + { + Assert(player) + if(!player.IsPhaseShifted()){ + int chargeCount = player.GetOffhandWeapon(1).GetWeaponPrimaryClipCount() + int oldchargeCount = chargeCount + if(player in playerDecoyList){ + chargeCount -= recharge_remove + if(chargeCount < 0) + chargeCount = 0 + }else{ + int max = player.GetOffhandWeapon(1).GetWeaponPrimaryClipCountMax() + chargeCount += rechargeStep + if(chargeCount > charge_size){ + chargeCount = max + } + } + if(oldchargeCount != chargeCount){ + player.GetOffhandWeapon(1).SetWeaponPrimaryClipCount(chargeCount) + } + } + + + + } + } + wait rechargeInterval + } + + } + + +#endif \ No newline at end of file diff --git a/mod/scripts/vscripts/weapons/mp_ability_holopilot.nut b/mod/scripts/vscripts/weapons/mp_ability_holopilot.nut new file mode 100644 index 0000000..5e1fdb5 --- /dev/null +++ b/mod/scripts/vscripts/weapons/mp_ability_holopilot.nut @@ -0,0 +1,437 @@ +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 ] + 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 + + for( int i = 0; i < numberOfDecoysToMake; ++i ) + { + entity 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 + 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).GetWeaponPrimaryClipCount()<100) + 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?) + + 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" ) + + 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) + player.GetOffhandWeapon(1).SetWeaponPrimaryClipCount(0) + }) + + vector initial_origin = player.GetOrigin() + vector initial_angle = player.GetAngles() + array positions = clone player.p.burnCardPhaseRewindStruct.phaseRetreatSavedPositions + + ViewConeZero( player ) + player.HolsterWeapon() + player.SetPredictionEnabled( false ) + PhaseShift( player, 0.0, 10 * PHASE_REWIND_PATH_SNAPSHOT_INTERVAL * 1.5 ) + + for ( float i = 10; 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() ) +} \ No newline at end of file