using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Logging; using GameNetcodeStuff; using Unity.Netcode; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("0.0.0.0")] namespace TCF; [BepInPlugin("com.tonysmoons.tcf", "TCF", "1.0.0")] public class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.tonysmoons.tcf"; public const string PluginName = "TCF"; public const string PluginVersion = "1.0.0"; internal static ManualLogSource Log; private void Awake() { Log = ((BaseUnityPlugin)this).Logger; Log.LogInfo((object)"TCF loaded."); } } public class ScarecrowAI : EnemyAI { private const int StateRoaming = 0; private const int StateInvestigating = 1; private const int StateChasing = 2; private const int StateChokeHold = 3; private const int StateFleeing = 4; private const int StateWatching = 5; private const int StateDead = 6; [Header("Detection")] [Tooltip("Max distance (units) a player can be spotted at. Line-of-sight is always required on top of this - see the class summary.")] [Min(0f)] public float detectionRange = 35f; [Tooltip("Field-of-view cone width (degrees) used for spotting players, same meaning as EnemyAI.CheckLineOfSightForPlayer's Width parameter.")] [Min(0f)] public float detectionWidth = 65f; [Tooltip("AISearchRoutine search width for the random outdoor wander used while Roaming/Watching.")] [Min(0f)] public float wanderSearchWidth = 50f; [Header("Movement Speeds")] [Tooltip("NavMeshAgent speed while Roaming.")] [Min(0f)] public float roamSpeed = 3f; [Tooltip("NavMeshAgent speed while Investigating a lost target's last-known position.")] [Min(0f)] public float investigateSpeed = 4.5f; [Tooltip("NavMeshAgent speed while Chasing a clearly-seen player.")] [Min(0f)] public float chaseSpeed = 7f; [Tooltip("Roam speed used once Enraged (after the first successful kill).")] [Min(0f)] public float enragedRoamSpeed = 5f; [Tooltip("Chase speed used once Enraged.")] [Min(0f)] public float enragedChaseSpeed = 11f; [Header("Facing")] [Tooltip("Visual model transform rotated to face the current target while Chasing/Choke Holding. Falls back to this object's own transform if left unset.")] public Transform modelRoot; [Tooltip("Degrees/sec turned to face the target.")] [Min(0f)] public float turnSpeed = 260f; [Header("Investigating")] [Tooltip("Seconds spent moving toward a lost target's last-known position before giving up and resuming Roaming/Watching.")] [Min(0f)] public float investigateTimeout = 8f; [Header("Choke Hold Attack")] [Tooltip("Distance (units) at which the Scarecrow stops chasing and grabs its target.")] [Min(0f)] public float chokeHoldRange = 2.5f; [Tooltip("Seconds the choke hold sequence lasts before the kill. Not used once Enraged - the kill becomes instant instead.")] [Min(0f)] public float chokeHoldDuration = 15f; [Tooltip("REQUIRED for the hold to visually lock the victim in place. Exact position/rotation the victim's body is pinned to for the whole choke hold - assign a child Transform positioned in the Scarecrow's grip.")] public Transform chokeHoldVictimPosition; [Tooltip("Dedicated Animator driving the choke-hold attack animation. Assign any Animator (with its own Controller) here to swap the attack animation without touching code - can also point at the same Animator as Creature Animator if you'd rather drive it from one Controller.")] public Animator chokeHoldAnimator; [Tooltip("Bool parameter set on Choke Hold Animator for the duration of the hold (true = playing, false = idle/released). Change this if your Animator Controller names the parameter differently.")] public string chokeHoldAnimatorBoolParam = "ChokeHold"; [Tooltip("Cause of death reported for the choke-hold / enraged instant kill.")] public CauseOfDeath killCauseOfDeath = (CauseOfDeath)17; [Tooltip("Death ragdoll animation index passed to KillPlayer (base game's playerRagdolls array index) - pick whichever built-in ragdoll looks best for a decapitation.")] [Min(0f)] public int killDeathAnimation; [Header("Enraged (after the first kill)")] [Tooltip("Trigger fired once on Creature Animator the moment the Scarecrow becomes Enraged.")] public string enragedTriggerParam = "Enraged"; [Header("Fleeing / Watching (after being stunned)")] [Tooltip("NavMeshAgent speed while Fleeing from whoever just stunned it.")] [Min(0f)] public float fleeSpeed = 9f; [Tooltip("Seconds spent Fleeing before settling into Watching.")] [Min(0f)] public float fleeDuration = 5f; [Tooltip("While Watching, the minimum distance the Scarecrow tries to keep from any player who has stunned it before.")] [Min(0f)] public float watchKeepDistance = 20f; [Header("Audio")] [Tooltip("Possible sounds played once (for everyone) the moment this instance spawns - one is picked at random.")] public List spawnSFX = new List(); [Tooltip("AudioSource the spawn sound plays on. Falls back to creatureSFX if left unset.")] public AudioSource spawnAudioSource; [Tooltip("Played once (for everyone) the moment a choke hold begins.")] public AudioClip chokeHoldStartSFX; [Tooltip("Played once (for everyone) the moment a kill (choke hold or enraged instant kill) lands.")] public AudioClip killSFX; private AISearchRoutine wanderSearch; private int lockedTargetId = -1; private Vector3 investigatePosition; private float investigateTimer; private bool isChokeHolding; private bool chokeHoldKillTriggered; private float chokeHoldTimer; private PlayerControllerB chokeHoldVictim; private bool isEnraged; private bool hasBeenStunned; private float fleeTimer; private readonly HashSet blacklistedPlayerIds = new HashSet(); private Transform FacingTransform { get { if (!((Object)(object)modelRoot != (Object)null)) { return ((Component)this).transform; } return modelRoot; } } public override void Start() { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown //IL_0065: Unknown result type (might be due to invalid IL or missing references) ((EnemyAI)this).Start(); if (!base.isOutside) { Debug.LogWarning((object)(base.enemyType.enemyName + ": Scarecrow's EnemyType asset must have \"Is Outside Enemy\" checked - it is designed to never operate inside the facility.")); } wanderSearch = new AISearchRoutine { searchWidth = wanderSearchWidth }; base.agent.speed = roamSpeed; PlaySpawnSound(); if (((NetworkBehaviour)this).IsOwner) { ((EnemyAI)this).StartSearch(((Component)this).transform.position, wanderSearch); } } private void PlaySpawnSound() { AudioSource val = (((Object)(object)spawnAudioSource != (Object)null) ? spawnAudioSource : base.creatureSFX); if (spawnSFX != null && spawnSFX.Count != 0 && !((Object)(object)val == (Object)null)) { AudioClip val2 = spawnSFX[Random.Range(0, spawnSFX.Count)]; val.PlayOneShot(val2); WalkieTalkie.TransmitOneShotAudio(val, val2, 1f); } } public override void Update() { ((EnemyAI)this).Update(); if (base.isEnemyDead) { return; } if (isChokeHolding) { HoldVictimInPlace(); if (((NetworkBehaviour)this).IsOwner && !chokeHoldKillTriggered && (Object)(object)chokeHoldVictim != (Object)null) { chokeHoldTimer -= Time.deltaTime; if (chokeHoldTimer <= 0f) { chokeHoldKillTriggered = true; PerformKillServerRpc((int)chokeHoldVictim.playerClientId); } } } if (base.currentBehaviourStateIndex == 2 || base.currentBehaviourStateIndex == 3) { FaceTarget(base.targetPlayer); } } private void HoldVictimInPlace() { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)chokeHoldVictim == (Object)null) && !((Object)(object)chokeHoldVictimPosition == (Object)null)) { if ((Object)(object)chokeHoldVictim.thisController != (Object)null) { ((Collider)chokeHoldVictim.thisController).enabled = false; } ((Component)chokeHoldVictim).transform.position = chokeHoldVictimPosition.position; ((Component)chokeHoldVictim).transform.rotation = chokeHoldVictimPosition.rotation; } } private void FaceTarget(PlayerControllerB target) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)target == (Object)null)) { Transform facingTransform = FacingTransform; Vector3 val = ((Component)target).transform.position - facingTransform.position; val.y = 0f; if (!(((Vector3)(ref val)).sqrMagnitude < 0.0001f)) { Quaternion val2 = Quaternion.LookRotation(val); facingTransform.rotation = Quaternion.RotateTowards(facingTransform.rotation, val2, turnSpeed * Time.deltaTime); } } } public override void DoAIInterval() { ((EnemyAI)this).DoAIInterval(); if (base.isEnemyDead || !((NetworkBehaviour)this).IsOwner) { return; } switch (base.currentBehaviourStateIndex) { case 0: DoRoamingInterval(); break; case 1: DoInvestigatingInterval(); break; case 2: DoChasingInterval(); break; case 3: if ((Object)(object)chokeHoldVictim == (Object)null || chokeHoldVictim.isPlayerDead) { ReleaseChokeHoldLocal(); ReturnToDefaultState(); } break; case 4: DoFleeingInterval(); break; case 5: DoWatchingInterval(); break; } } private void ReturnToDefaultState() { ((EnemyAI)this).SwitchToBehaviourState(hasBeenStunned ? 5 : 0); } private void DoRoamingInterval() { //IL_0059: Unknown result type (might be due to invalid IL or missing references) base.agent.speed = (isEnraged ? enragedRoamSpeed : roamSpeed); PlayerControllerB val = FindVisiblePlayer(detectionRange, detectionWidth); if ((Object)(object)val != (Object)null) { BeginChase(val); } else if (!wanderSearch.inProgress) { ((EnemyAI)this).StartSearch(((Component)this).transform.position, wanderSearch); } } private void DoInvestigatingInterval() { //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) base.agent.speed = investigateSpeed; if ((Object)(object)base.targetPlayer != (Object)null && HasLineOfSightTo(base.targetPlayer) && ((EnemyAI)this).PlayerIsTargetable(base.targetPlayer, false, false, true)) { BeginChase(base.targetPlayer); return; } investigateTimer -= base.AIIntervalTime; if (investigateTimer <= 0f || Vector3.Distance(((Component)this).transform.position, investigatePosition) < 1.5f) { SetTargetPlayer(-1); ReturnToDefaultState(); return; } PlayerControllerB val = FindVisiblePlayer(detectionRange, detectionWidth); if ((Object)(object)val != (Object)null) { BeginChase(val); } } private void DoChasingInterval() { //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)base.targetPlayer == (Object)null || base.targetPlayer.isPlayerDead || !((EnemyAI)this).PlayerIsTargetable(base.targetPlayer, false, false, true)) { SetTargetPlayer(-1); ReturnToDefaultState(); return; } base.agent.speed = (isEnraged ? enragedChaseSpeed : chaseSpeed); if (!HasLineOfSightTo(base.targetPlayer)) { BeginInvestigate(((Component)base.targetPlayer).transform.position); return; } ((EnemyAI)this).SetDestinationToPosition(((Component)base.targetPlayer).transform.position, true); if (Vector3.Distance(((Component)this).transform.position, ((Component)base.targetPlayer).transform.position) <= chokeHoldRange) { if (isEnraged) { TriggerInstantKill(base.targetPlayer); } else { BeginChokeHold(base.targetPlayer); } } } private void DoFleeingInterval() { //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) base.agent.speed = fleeSpeed; fleeTimer -= base.AIIntervalTime; if (fleeTimer <= 0f) { ((EnemyAI)this).SwitchToBehaviourState(5); return; } PlayerControllerB val = FindNearestBlacklistedPlayer(); if ((Object)(object)val != (Object)null && Vector3.Distance(((Component)this).transform.position, ((Component)val).transform.position) < watchKeepDistance) { Transform val2 = ((EnemyAI)this).ChooseFarthestNodeFromPosition(((Component)val).transform.position, true, 0, false, 50, -1); if ((Object)(object)val2 != (Object)null) { ((EnemyAI)this).SetDestinationToPosition(val2.position, true); } } } private void DoWatchingInterval() { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) base.agent.speed = (isEnraged ? enragedRoamSpeed : roamSpeed); PlayerControllerB val = FindNearestBlacklistedPlayer(); if ((Object)(object)val != (Object)null && Vector3.Distance(((Component)this).transform.position, ((Component)val).transform.position) < watchKeepDistance) { Transform val2 = ((EnemyAI)this).ChooseFarthestNodeFromPosition(((Component)val).transform.position, true, 0, false, 50, -1); if ((Object)(object)val2 != (Object)null) { ((EnemyAI)this).SetDestinationToPosition(val2.position, true); } } else if (!wanderSearch.inProgress) { ((EnemyAI)this).StartSearch(((Component)this).transform.position, wanderSearch); } PlayerControllerB val3 = FindVisiblePlayer(detectionRange, detectionWidth); if ((Object)(object)val3 != (Object)null) { BeginChase(val3); } } private PlayerControllerB FindNearestBlacklistedPlayer() { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) PlayerControllerB result = null; float num = float.MaxValue; for (int i = 0; i < StartOfRound.Instance.allPlayerScripts.Length; i++) { PlayerControllerB val = StartOfRound.Instance.allPlayerScripts[i]; if (!((Object)(object)val == (Object)null) && !val.isPlayerDead && blacklistedPlayerIds.Contains((int)val.playerClientId)) { float num2 = Vector3.Distance(((Component)this).transform.position, ((Component)val).transform.position); if (num2 < num) { num = num2; result = val; } } } return result; } private PlayerControllerB FindVisiblePlayer(float range, float width) { //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) PlayerControllerB result = null; float num = float.MaxValue; for (int i = 0; i < StartOfRound.Instance.allPlayerScripts.Length; i++) { PlayerControllerB val = StartOfRound.Instance.allPlayerScripts[i]; if ((Object)(object)val == (Object)null || !((EnemyAI)this).PlayerIsTargetable(val, false, false, true) || blacklistedPlayerIds.Contains((int)val.playerClientId)) { continue; } Vector3 position = base.eye.position; Vector3 val2 = (((Object)(object)val.gameplayCamera != (Object)null) ? ((Component)val.gameplayCamera).transform.position : ((Component)val).transform.position); float num2 = Vector3.Distance(position, val2); if (!(num2 > range)) { Vector3 val3 = val2 - position; if (!(Vector3.Angle(base.eye.forward, val3) > width) && !Physics.Linecast(position, val2, StartOfRound.Instance.collidersAndRoomMaskAndDefault, (QueryTriggerInteraction)1) && num2 < num) { num = num2; result = val; } } } return result; } private bool HasLineOfSightTo(PlayerControllerB player) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null) { return false; } Vector3 position = base.eye.position; Vector3 val = (((Object)(object)player.gameplayCamera != (Object)null) ? ((Component)player.gameplayCamera).transform.position : ((Component)player).transform.position); return !Physics.Linecast(position, val, StartOfRound.Instance.collidersAndRoomMaskAndDefault, (QueryTriggerInteraction)1); } private void BeginChase(PlayerControllerB player) { ((EnemyAI)this).StopSearch(wanderSearch, false); ((EnemyAI)this).SetMovingTowardsTargetPlayer(player); SetTargetPlayer((int)player.playerClientId); ((EnemyAI)this).SwitchToBehaviourState(2); } private void BeginInvestigate(Vector3 lastKnownPosition) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) base.movingTowardsTargetPlayer = false; investigatePosition = lastKnownPosition; investigateTimer = investigateTimeout; ((EnemyAI)this).SetDestinationToPosition(investigatePosition, true); ((EnemyAI)this).SwitchToBehaviourState(1); } private void SetTargetPlayer(int playerObjectId) { if (lockedTargetId != playerObjectId) { SetTargetPlayerServerRpc(playerObjectId); } } [ServerRpc(RequireOwnership = false)] private void SetTargetPlayerServerRpc(int playerObjectId) { SetTargetPlayerClientRpc(playerObjectId); } [ClientRpc] private void SetTargetPlayerClientRpc(int playerObjectId) { lockedTargetId = playerObjectId; base.targetPlayer = ((playerObjectId >= 0 && (Object)(object)StartOfRound.Instance != (Object)null && playerObjectId < StartOfRound.Instance.allPlayerScripts.Length) ? StartOfRound.Instance.allPlayerScripts[playerObjectId] : null); } private void BeginChokeHold(PlayerControllerB target) { StartChokeHoldServerRpc((int)target.playerClientId); } [ServerRpc(RequireOwnership = false)] private void StartChokeHoldServerRpc(int playerObjectId) { StartChokeHoldClientRpc(playerObjectId); } [ClientRpc] private void StartChokeHoldClientRpc(int playerObjectId) { if ((Object)(object)StartOfRound.Instance == (Object)null || playerObjectId < 0 || playerObjectId >= StartOfRound.Instance.allPlayerScripts.Length) { return; } PlayerControllerB val = StartOfRound.Instance.allPlayerScripts[playerObjectId]; if (!((Object)(object)val == (Object)null) && !val.isPlayerDead) { chokeHoldVictim = val; chokeHoldTimer = chokeHoldDuration; chokeHoldKillTriggered = false; isChokeHolding = true; base.inSpecialAnimation = true; base.inSpecialAnimationWithPlayer = val; val.inAnimationWithEnemy = (EnemyAI)(object)this; val.inSpecialInteractAnimation = true; val.snapToServerPosition = true; if ((Object)(object)chokeHoldAnimator != (Object)null && !string.IsNullOrEmpty(chokeHoldAnimatorBoolParam)) { chokeHoldAnimator.SetBool(chokeHoldAnimatorBoolParam, true); } if ((Object)(object)chokeHoldStartSFX != (Object)null && (Object)(object)base.creatureSFX != (Object)null) { base.creatureSFX.PlayOneShot(chokeHoldStartSFX); WalkieTalkie.TransmitOneShotAudio(base.creatureSFX, chokeHoldStartSFX, 1f); } if (((NetworkBehaviour)this).IsOwner) { base.movingTowardsTargetPlayer = false; base.moveTowardsDestination = false; base.agent.speed = 0f; } ((EnemyAI)this).SwitchToBehaviourStateOnLocalClient(3); } } private void TriggerInstantKill(PlayerControllerB target) { PerformKillServerRpc((int)target.playerClientId); } [ServerRpc(RequireOwnership = false)] private void PerformKillServerRpc(int playerObjectId) { PerformKillClientRpc(playerObjectId); } [ClientRpc] private void PerformKillClientRpc(int playerObjectId) { //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) ReleaseChokeHoldLocal(); if ((Object)(object)StartOfRound.Instance == (Object)null || playerObjectId < 0 || playerObjectId >= StartOfRound.Instance.allPlayerScripts.Length) { return; } PlayerControllerB val = StartOfRound.Instance.allPlayerScripts[playerObjectId]; if ((Object)(object)val == (Object)null || val.isPlayerDead) { return; } if ((Object)(object)killSFX != (Object)null && (Object)(object)base.creatureSFX != (Object)null) { base.creatureSFX.PlayOneShot(killSFX); WalkieTalkie.TransmitOneShotAudio(base.creatureSFX, killSFX, 1f); } val.KillPlayer(Vector3.zero, true, killCauseOfDeath, killDeathAnimation, default(Vector3), false); if (!isEnraged) { isEnraged = true; if ((Object)(object)base.creatureAnimator != (Object)null && !string.IsNullOrEmpty(enragedTriggerParam)) { base.creatureAnimator.SetTrigger(enragedTriggerParam); } } if (((NetworkBehaviour)this).IsOwner) { SetTargetPlayer(-1); base.movingTowardsTargetPlayer = false; base.agent.speed = 0f; ReturnToDefaultState(); } } private void ReleaseChokeHoldLocal() { if (!isChokeHolding) { return; } isChokeHolding = false; if ((Object)(object)chokeHoldAnimator != (Object)null && !string.IsNullOrEmpty(chokeHoldAnimatorBoolParam)) { chokeHoldAnimator.SetBool(chokeHoldAnimatorBoolParam, false); } if ((Object)(object)chokeHoldVictim != (Object)null) { if ((Object)(object)chokeHoldVictim.thisController != (Object)null) { ((Collider)chokeHoldVictim.thisController).enabled = true; } chokeHoldVictim.inSpecialInteractAnimation = false; chokeHoldVictim.snapToServerPosition = false; if ((Object)(object)chokeHoldVictim.inAnimationWithEnemy == (Object)(object)this) { chokeHoldVictim.inAnimationWithEnemy = null; } } if ((Object)(object)base.inSpecialAnimationWithPlayer == (Object)(object)chokeHoldVictim) { base.inSpecialAnimationWithPlayer = null; } base.inSpecialAnimation = false; chokeHoldVictim = null; } public override void SetEnemyStunned(bool setToStunned, float setToStunTime = 1f, PlayerControllerB setStunnedByPlayer = null) { //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) ((EnemyAI)this).SetEnemyStunned(setToStunned, setToStunTime, setStunnedByPlayer); if (!setToStunned || base.isEnemyDead) { return; } hasBeenStunned = true; if ((Object)(object)setStunnedByPlayer != (Object)null) { blacklistedPlayerIds.Add((int)setStunnedByPlayer.playerClientId); } ReleaseChokeHoldLocal(); if (((NetworkBehaviour)this).IsOwner) { SetTargetPlayer(-1); base.movingTowardsTargetPlayer = false; base.moveTowardsDestination = false; Vector3 val = (((Object)(object)setStunnedByPlayer != (Object)null) ? ((Component)setStunnedByPlayer).transform.position : ((Component)this).transform.position); Transform val2 = ((EnemyAI)this).ChooseFarthestNodeFromPosition(val, true, 0, false, 50, -1); if ((Object)(object)val2 != (Object)null) { ((EnemyAI)this).SetDestinationToPosition(val2.position, true); } fleeTimer = fleeDuration; ((EnemyAI)this).SwitchToBehaviourState(4); } } public override void HitEnemy(int force = 1, PlayerControllerB playerWhoHit = null, bool playHitSFX = false, int hitID = -1) { ((EnemyAI)this).HitEnemy(force, playerWhoHit, playHitSFX, hitID); if (!base.isEnemyDead) { base.enemyHP -= force; if (base.enemyHP <= 0 && ((NetworkBehaviour)this).IsOwner) { ((EnemyAI)this).KillEnemyOnOwnerClient(false); } } } public override void KillEnemy(bool destroy = false) { ReleaseChokeHoldLocal(); ((EnemyAI)this).KillEnemy(destroy); base.agent.speed = 0f; ((EnemyAI)this).SwitchToBehaviourStateOnLocalClient(6); } public override void OnDestroy() { ReleaseChokeHoldLocal(); ((EnemyAI)this).OnDestroy(); } } public class SludgeFiendAI : EnemyAI { [Header("Scrap Collection")] [Tooltip("Body sockets carried scrap is parented onto, one per carried item slot (index 0 = first item picked up). Needs at least Scrap Needed To Attack entries - extra items beyond the array length re-use the last socket.")] public Transform[] scrapAttachPoints; [Tooltip("Distance (units) to a targeted scrap item at which it's actually picked up.")] [Min(0f)] public float scrapPickupRange = 1.5f; [Tooltip("Max distance (units) away a piece of scrap can be for this instance to notice and go for it.")] [Min(0f)] public float scrapDetectRange = 25f; [Tooltip("How many scrap items need to be carried before it stops collecting and starts hunting for a player to launch them at.")] [Min(1f)] public int scrapNeededToAttack = 5; [Tooltip("Possible sounds played once (for everyone) each time a piece of scrap is picked up.")] public List pickupSFX = new List(); [Tooltip("AudioSource the pickup sound plays on. Falls back to creatureSFX if left unset.")] public AudioSource pickupAudioSource; [Header("Detection / Fleeing")] [Tooltip("How wide a cone (degrees) counts as \"seeing\"/\"being seen by\" a player - same meaning as CheckLineOfSightForPlayer's own Width parameter.")] [Min(0f)] public float playerDetectWidth = 60f; [Tooltip("Max distance (units) a player can be spotted at while Searching, triggering an immediate flee.")] [Min(0f)] public float playerDetectRange = 25f; [Tooltip("NavMeshAgent speed while running away, either from being spotted or right after launching scrap.")] [Min(0f)] public float fleeSpeed = 8f; [Tooltip("Seconds spent fleeing before it settles back down and resumes searching for scrap.")] [Min(0f)] public float fleeDuration = 6f; [Header("Wandering")] [Tooltip("NavMeshAgent speed while searching for scrap.")] [Min(0f)] public float wanderSpeed = 4f; [Tooltip("Search width passed to the wander AISearchRoutine when no scrap is currently known about.")] [Min(0f)] public float wanderSearchWidth = 60f; [Header("Attack")] [Tooltip("Max distance (units) to a player at which it will stop and launch its carried scrap.")] [Min(0f)] public float launchRange = 15f; [Tooltip("Seconds spent standing still and facing the target before the scrap actually launches.")] [Min(0f)] public float launchWindupTime = 1f; [Tooltip("Damage dealt if the launch connects.")] [Min(0f)] public int launchDamage = 95; [Tooltip("Speed (units/sec) the thrown scrap meshes fly toward the target - cosmetic only, the hit/miss result is decided separately at launch time.")] [Min(0f)] public float launchThrowSpeed = 20f; [Tooltip("Extra upward velocity added to each thrown piece so they arc instead of flying flat.")] [Min(0f)] public float launchArcForce = 4f; [Tooltip("Cause of death reported if a launch brings the player's health to 0.")] public CauseOfDeath launchCauseOfDeath; [Tooltip("Possible sounds played once (for everyone) the moment scrap is launched.")] public List launchSFX = new List(); [Tooltip("AudioSource the launch sound plays on. Falls back to creatureSFX if left unset.")] public AudioSource launchAudioSource; [Header("Facing")] [Tooltip("Visual model transform rotated to face the target while winding up an attack. Falls back to this object's own transform if left unset.")] public Transform modelRoot; [Tooltip("Degrees/sec turned to face the target player during the attack windup.")] [Min(0f)] public float turnSpeed = 240f; [Header("Stun Death (explode)")] [Tooltip("VFX prefab instantiated at this instance's position the moment it's stunned/tased and explodes. Purely cosmetic, not networked - every client independently spawns its own local copy the same way R2AI's spawn lightning does.")] public GameObject explosionEffectPrefab; [Tooltip("Seconds before the spawned explosion VFX instance is destroyed again.")] [Min(0f)] public float explosionEffectLifetime = 3f; [Tooltip("Played once (for everyone) the moment it explodes.")] public AudioClip explosionSFX; [Tooltip("AudioSource the explosion sound plays on. Falls back to creatureSFX if left unset.")] public AudioSource explosionAudioSource; [Header("Spawn")] [Tooltip("Possible sounds played once (for everyone) the moment this instance spawns.")] public List spawnSFX = new List(); [Tooltip("AudioSource the spawn sound plays on. Falls back to creatureSFX if left unset.")] public AudioSource spawnAudioSource; private static readonly List grabbableObjectsInMap = new List(); private readonly List carriedScrap = new List(); private AISearchRoutine searchForItems; private GrabbableObject targetItem; private float fleeTimer; private PlayerControllerB attackTargetPlayer; private bool isWindingUpLaunch; private float launchTimer; private bool sendingCollectRpc; private bool hasExploded; public override void Start() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown ((EnemyAI)this).Start(); searchForItems = new AISearchRoutine { searchWidth = wanderSearchWidth }; RefreshGrabbableObjectsInMap(); AudioSource val = (((Object)(object)spawnAudioSource != (Object)null) ? spawnAudioSource : base.creatureSFX); if (spawnSFX != null && spawnSFX.Count > 0 && (Object)(object)val != (Object)null) { AudioClip val2 = spawnSFX[Random.Range(0, spawnSFX.Count)]; val.PlayOneShot(val2); WalkieTalkie.TransmitOneShotAudio(val, val2, 1f); } } private static void RefreshGrabbableObjectsInMap() { grabbableObjectsInMap.Clear(); GrabbableObject[] array = Object.FindObjectsOfType(); for (int i = 0; i < array.Length; i++) { if (array[i].itemProperties.isScrap && array[i].grabbableToEnemies && !array[i].deactivated) { grabbableObjectsInMap.Add(((Component)array[i]).gameObject); } } } public override void Update() { ((EnemyAI)this).Update(); if (base.isEnemyDead) { return; } switch (base.currentBehaviourStateIndex) { case 0: base.agent.speed = wanderSpeed; break; case 1: base.agent.speed = fleeSpeed; if (((NetworkBehaviour)this).IsOwner) { fleeTimer -= Time.deltaTime; } break; case 2: base.agent.speed = 0f; FaceTargetPlayer(attackTargetPlayer); if (((NetworkBehaviour)this).IsOwner && isWindingUpLaunch) { launchTimer -= Time.deltaTime; if (launchTimer <= 0f) { isWindingUpLaunch = false; LaunchScrapAtTarget(attackTargetPlayer); } } break; } } public override void DoAIInterval() { ((EnemyAI)this).DoAIInterval(); if (!base.isEnemyDead && ((NetworkBehaviour)this).IsOwner) { switch (base.currentBehaviourStateIndex) { case 0: DoSearchingInterval(); break; case 1: DoFleeingInterval(); break; } } } private void DoSearchingInterval() { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Unknown result type (might be due to invalid IL or missing references) //IL_015f: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Unknown result type (might be due to invalid IL or missing references) PlayerControllerB val = ((EnemyAI)this).CheckLineOfSightForPlayer(playerDetectWidth, (int)playerDetectRange, -1); if ((Object)(object)val != (Object)null) { BeginFlee(((Component)val).transform.position); return; } if (carriedScrap.Count >= scrapNeededToAttack) { PlayerControllerB val2 = ((EnemyAI)this).CheckLineOfSightForPlayer(playerDetectWidth, (int)launchRange, -1); if ((Object)(object)val2 != (Object)null) { BeginAttack(val2); return; } } if ((Object)(object)targetItem != (Object)null) { if (!targetItem.isHeld && !targetItem.deactivated && grabbableObjectsInMap.Contains(((Component)targetItem).gameObject)) { if (Vector3.Distance(((Component)this).transform.position, ((Component)targetItem).transform.position) <= scrapPickupRange) { CollectScrap(targetItem); targetItem = null; } else { ((EnemyAI)this).SetDestinationToPosition(((Component)targetItem).transform.position, true); } return; } targetItem = null; } GameObject val3 = FindClosestScrap(); if ((Object)(object)val3 != (Object)null) { targetItem = val3.GetComponent(); ((EnemyAI)this).StopSearch(searchForItems, false); ((EnemyAI)this).SetDestinationToPosition(((Component)targetItem).transform.position, true); } else if (!searchForItems.inProgress) { ((EnemyAI)this).StartSearch(((Component)this).transform.position, searchForItems); } } private GameObject FindClosestScrap() { //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) GameObject result = null; float num = scrapDetectRange; for (int num2 = grabbableObjectsInMap.Count - 1; num2 >= 0; num2--) { GameObject val = grabbableObjectsInMap[num2]; if ((Object)(object)val == (Object)null) { grabbableObjectsInMap.RemoveAt(num2); } else { GrabbableObject component = val.GetComponent(); if (!((Object)(object)component == (Object)null) && !component.isHeld && !component.deactivated && component.grabbableToEnemies) { float num3 = Vector3.Distance(((Component)this).transform.position, val.transform.position); if (num3 < num) { num = num3; result = val; } } } } return result; } private void CollectScrap(GrabbableObject item) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) NetworkObject component = ((Component)item).GetComponent(); AttachScrapLocally(item); sendingCollectRpc = true; CollectScrapServerRpc(NetworkObjectReference.op_Implicit(component)); } [ServerRpc(RequireOwnership = false)] private void CollectScrapServerRpc(NetworkObjectReference itemRef) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) CollectScrapClientRpc(itemRef); } [ClientRpc] private void CollectScrapClientRpc(NetworkObjectReference itemRef) { NetworkObject val = default(NetworkObject); if (sendingCollectRpc) { sendingCollectRpc = false; } else if (((NetworkObjectReference)(ref itemRef)).TryGet(ref val, (NetworkManager)null)) { GrabbableObject component = ((Component)val).GetComponent(); if ((Object)(object)component != (Object)null) { AttachScrapLocally(component); } } } private void AttachScrapLocally(GrabbableObject item) { if (!((Object)(object)item == (Object)null) && !carriedScrap.Contains(item) && scrapAttachPoints != null && scrapAttachPoints.Length != 0) { int num = Mathf.Min(carriedScrap.Count, scrapAttachPoints.Length - 1); Transform parentObject = scrapAttachPoints[num]; item.parentObject = parentObject; item.hasHitGround = false; item.GrabItemFromEnemy((EnemyAI)(object)this); item.EnablePhysics(false); carriedScrap.Add(item); grabbableObjectsInMap.Remove(((Component)item).gameObject); AudioSource val = (((Object)(object)pickupAudioSource != (Object)null) ? pickupAudioSource : base.creatureSFX); if (pickupSFX != null && pickupSFX.Count > 0 && (Object)(object)val != (Object)null) { AudioClip val2 = pickupSFX[Random.Range(0, pickupSFX.Count)]; val.PlayOneShot(val2); WalkieTalkie.TransmitOneShotAudio(val, val2, 1f); } } } private void BeginFlee(Vector3 threatPosition) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) targetItem = null; ((EnemyAI)this).StopSearch(searchForItems, false); Transform val = ((EnemyAI)this).ChooseFarthestNodeFromPosition(threatPosition, true, 0, false, 50, -1); if ((Object)(object)val != (Object)null) { ((EnemyAI)this).SetDestinationToPosition(val.position, true); } fleeTimer = fleeDuration; ((EnemyAI)this).SwitchToBehaviourState(1); } private void DoFleeingInterval() { if (fleeTimer <= 0f) { ((EnemyAI)this).SwitchToBehaviourState(0); } } private void BeginAttack(PlayerControllerB target) { targetItem = null; ((EnemyAI)this).StopSearch(searchForItems, false); attackTargetPlayer = target; isWindingUpLaunch = true; launchTimer = launchWindupTime; ((EnemyAI)this).SwitchToBehaviourState(2); } private void FaceTargetPlayer(PlayerControllerB target) { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)target == (Object)null)) { Transform val = (((Object)(object)modelRoot != (Object)null) ? modelRoot : ((Component)this).transform); Vector3 val2 = ((Component)target).transform.position - val.position; val2.y = 0f; if (!(((Vector3)(ref val2)).sqrMagnitude < 0.0001f)) { Quaternion val3 = Quaternion.LookRotation(val2); val.rotation = Quaternion.RotateTowards(val.rotation, val3, turnSpeed * Time.deltaTime); } } } private void LaunchScrapAtTarget(PlayerControllerB target) { //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)target != (Object)null && carriedScrap.Count > 0) { float num = Vector3.Distance(((Component)this).transform.position, ((Component)target).transform.position); bool flag = !Physics.Linecast(base.eye.position, ((Component)target.gameplayCamera).transform.position, StartOfRound.Instance.collidersAndRoomMaskAndDefault, (QueryTriggerInteraction)1); bool willHit = num <= launchRange && flag; LaunchScrapServerRpc((int)target.playerClientId, willHit); } Vector3 threatPosition = (((Object)(object)target != (Object)null) ? ((Component)target).transform.position : ((Component)this).transform.position); attackTargetPlayer = null; BeginFlee(threatPosition); } [ServerRpc(RequireOwnership = false)] private void LaunchScrapServerRpc(int targetPlayerId, bool willHit) { LaunchScrapClientRpc(targetPlayerId, willHit); } [ClientRpc] private void LaunchScrapClientRpc(int targetPlayerId, bool willHit) { //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) PlayerControllerB val = null; if ((Object)(object)StartOfRound.Instance != (Object)null && targetPlayerId >= 0 && targetPlayerId < StartOfRound.Instance.allPlayerScripts.Length) { val = StartOfRound.Instance.allPlayerScripts[targetPlayerId]; } ThrowCarriedScrapVisual(val); AudioSource val2 = (((Object)(object)launchAudioSource != (Object)null) ? launchAudioSource : base.creatureSFX); if (launchSFX != null && launchSFX.Count > 0 && (Object)(object)val2 != (Object)null) { AudioClip val3 = launchSFX[Random.Range(0, launchSFX.Count)]; val2.PlayOneShot(val3); WalkieTalkie.TransmitOneShotAudio(val2, val3, 1f); } if (willHit && (Object)(object)val != (Object)null && !val.isPlayerDead) { val.DamagePlayer(launchDamage, true, true, launchCauseOfDeath, 0, false, default(Vector3)); } } private void ThrowCarriedScrapVisual(PlayerControllerB target) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) Vector3 val = (((Object)(object)target != (Object)null) ? (((Component)target).transform.position + Vector3.up) : (((Component)this).transform.position + ((Component)this).transform.forward * launchRange)); foreach (GrabbableObject item in carriedScrap) { if (!((Object)(object)item == (Object)null)) { item.parentObject = null; item.EnablePhysics(true); item.hasHitGround = false; if ((Object)(object)item.propBody != (Object)null) { Vector3 val2 = val - ((Component)item).transform.position; Vector3 normalized = ((Vector3)(ref val2)).normalized; item.propBody.velocity = normalized * launchThrowSpeed + Vector3.up * launchArcForce; } item.DiscardItemFromEnemy(); grabbableObjectsInMap.Add(((Component)item).gameObject); } } carriedScrap.Clear(); } public override void HitEnemy(int force = 1, PlayerControllerB playerWhoHit = null, bool playHitSFX = false, int hitID = -1) { ((EnemyAI)this).HitEnemy(force, playerWhoHit, playHitSFX, hitID); if (!base.isEnemyDead) { base.enemyHP -= force; if (base.enemyHP <= 0 && ((NetworkBehaviour)this).IsOwner) { ((EnemyAI)this).KillEnemyOnOwnerClient(false); } } } public override void SetEnemyStunned(bool setToStunned, float setToStunTime = 1f, PlayerControllerB setStunnedByPlayer = null) { ((EnemyAI)this).SetEnemyStunned(setToStunned, setToStunTime, setStunnedByPlayer); if (setToStunned && !base.isEnemyDead && !hasExploded) { hasExploded = true; PlayExplosionEffect(); if (((NetworkBehaviour)this).IsOwner) { ((EnemyAI)this).KillEnemyOnOwnerClient(true); } } } private void PlayExplosionEffect() { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)explosionEffectPrefab != (Object)null) { Object.Destroy((Object)(object)Object.Instantiate(explosionEffectPrefab, ((Component)this).transform.position, Quaternion.identity), explosionEffectLifetime); } AudioSource val = (((Object)(object)explosionAudioSource != (Object)null) ? explosionAudioSource : base.creatureSFX); if ((Object)(object)explosionSFX != (Object)null && (Object)(object)val != (Object)null) { val.PlayOneShot(explosionSFX); WalkieTalkie.TransmitOneShotAudio(val, explosionSFX, 1f); } } public override void KillEnemy(bool destroy = false) { ((EnemyAI)this).KillEnemy(false); base.agent.speed = 0f; DropAllCarriedScrap(); } private void DropAllCarriedScrap() { foreach (GrabbableObject item in carriedScrap) { if (!((Object)(object)item == (Object)null)) { item.parentObject = null; item.EnablePhysics(true); item.hasHitGround = false; item.DiscardItemFromEnemy(); grabbableObjectsInMap.Add(((Component)item).gameObject); } } carriedScrap.Clear(); } }