From 05ea3dc4c3cbcb5825becbdbca238ef9271acf95 Mon Sep 17 00:00:00 2001 From: LadyAliceMargatroid Date: Wed, 26 Jun 2024 08:15:58 -0700 Subject: [PATCH] New enemy, the maid --- ScarletMansion/ScarletMansion/Assets.cs | 19 +- .../GamePatch/Enemies/MaidVariant.cs | 1318 +++++++++++++++++ .../ScarletMansion/GamePatch/InitPatch.cs | 46 +- .../GamePatch/LoadAssetsIntoLevelPatch.cs | 54 +- ScarletMansion/ScarletMansion/Plugin.cs | 7 +- ScarletMansion/ScarletMansion/PluginConfig.cs | 74 +- .../ScarletMansion/ScarletMansion.csproj | 5 + ScarletMansion/ScarletMansion/Utility.cs | 21 + 8 files changed, 1481 insertions(+), 63 deletions(-) create mode 100644 ScarletMansion/ScarletMansion/GamePatch/Enemies/MaidVariant.cs diff --git a/ScarletMansion/ScarletMansion/Assets.cs b/ScarletMansion/ScarletMansion/Assets.cs index 6f9d843..a574ca7 100644 --- a/ScarletMansion/ScarletMansion/Assets.cs +++ b/ScarletMansion/ScarletMansion/Assets.cs @@ -63,6 +63,7 @@ namespace ScarletMansion { } public static Enemy knight; + public static Enemy maid; // item values @@ -167,12 +168,18 @@ namespace ScarletMansion { public static Sprite hoverIcon; - private static string GetAssemblyName() => Assembly.GetExecutingAssembly().FullName.Split(',')[0]; public static void LoadAssetBundle() { if (MainAssetBundle == null) { - using (var assetStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetAssemblyName() + "." + mainAssetBundleName)) { - MainAssetBundle = AssetBundle.LoadFromStream(assetStream); + var assembly = Assembly.GetExecutingAssembly(); + var resourceNames = assembly.GetManifestResourceNames(); + if (resourceNames.Length >= 1) { + var name = resourceNames[0]; + using (var assetStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name)) { + Plugin.logger.LogInfo($"Loading resource {name}"); + MainAssetBundle = AssetBundle.LoadFromStream(assetStream); + } } + } dungeon = Load("SDMLevel"); @@ -185,6 +192,12 @@ namespace ScarletMansion { Load("KnightKeyword") ); + maid = new Enemy( + Load("NET_MaidEnemy"), + Load("MaidNode"), + Load("MaidKeyword") + ); + RegisterNetworkPrefab(networkObjectList.networkDungeon); RegisterNetworkPrefab(networkObjectList.networkDoors); RegisterNetworkPrefab(networkObjectList.networkItems); diff --git a/ScarletMansion/ScarletMansion/GamePatch/Enemies/MaidVariant.cs b/ScarletMansion/ScarletMansion/GamePatch/Enemies/MaidVariant.cs new file mode 100644 index 0000000..f4896e6 --- /dev/null +++ b/ScarletMansion/ScarletMansion/GamePatch/Enemies/MaidVariant.cs @@ -0,0 +1,1318 @@ +//using System; +using System.Collections; +using GameNetcodeStuff; +using Unity.Netcode; +using UnityEngine; +using UnityEngine.Animations.Rigging; + +namespace ScarletMansion.GamePatch.Enemies { + + // Token: 0x0200001C RID: 28 + public class MaidVariant : EnemyAI { + + // Token: 0x040000D0 RID: 208 + private Vector3[] lastSeenPlayerPositions; + + // Token: 0x040000D1 RID: 209 + private bool[] seenPlayers; + + // Token: 0x040000D2 RID: 210 + private float[] timeOfLastSeenPlayers; + + // Token: 0x040000D3 RID: 211 + private float timeSinceSeeingMultiplePlayers; + + // Token: 0x040000D4 RID: 212 + private float timeSinceCheckingForMultiplePlayers; + + // Token: 0x040000D5 RID: 213 + private float timeUntilNextCheck; + + // Token: 0x040000D6 RID: 214 + private int playersInVicinity; + + // Token: 0x040000D7 RID: 215 + private int currentSpecialAnimation; + + // Token: 0x040000D8 RID: 216 + private float timeSinceLastSpecialAnimation; + + // Token: 0x040000D9 RID: 217 + private bool doingKillAnimation; + + // Token: 0x040000DA RID: 218 + private int previousBehaviourState = -1; + + // Token: 0x040000DB RID: 219 + private int playersInView; + + // Token: 0x040000DC RID: 220 + private Vector3 agentLocalVelocity; + + // Token: 0x040000DD RID: 221 + public Transform animationContainer; + + // Token: 0x040000DE RID: 222 + private float velX; + + // Token: 0x040000DF RID: 223 + private float velZ; + + // Token: 0x040000E0 RID: 224 + private Vector3 previousPosition; + + // Token: 0x040000E1 RID: 225 + private PlayerControllerB watchingPlayer; + + // Token: 0x040000E2 RID: 226 + public Transform lookTarget; + + // Token: 0x040000E3 RID: 227 + public MultiAimConstraint headLookRig; + + // Token: 0x040000E4 RID: 228 + public Transform turnCompass; + + // Token: 0x040000E5 RID: 229 + public Transform headLookTarget; + + // Token: 0x040000E6 RID: 230 + private float sweepFloorTimer; + + // Token: 0x040000E7 RID: 231 + private bool isSweeping; + + // Token: 0x040000E8 RID: 232 + public AISearchRoutine roamAndSweepFloor; + + // Token: 0x040000E9 RID: 233 + public AISearchRoutine hoverAroundTargetPlayer; + + // Token: 0x040000EA RID: 234 + public float idleMovementSpeedBase = 3.5f; + + // Token: 0x040000EB RID: 235 + public float timeSinceChangingItem; + + // Token: 0x040000EC RID: 236 + private float timeSinceHittingPlayer; + + // Token: 0x040000F0 RID: 240 + public AudioClip[] footsteps; + + // Token: 0x040000F1 RID: 241 + public AudioClip[] broomSweepSFX; + + // Token: 0x040000F3 RID: 243 + private float pingAttentionTimer; + + // Token: 0x040000F4 RID: 244 + private int focusLevel; + + // Token: 0x040000F5 RID: 245 + private Vector3 pingAttentionPosition; + + // Token: 0x040000F6 RID: 246 + private float timeSincePingingAttention; + + // Token: 0x040000F7 RID: 247 + private Coroutine checkForPlayersCoroutine; + + // Token: 0x040000F8 RID: 248 + private bool hasPlayerInSight; + + // Token: 0x040000F9 RID: 249 + private float timeSinceNoticingFirstPlayer; + + // Token: 0x040000FA RID: 250 + private bool lostPlayerInChase; + + // Token: 0x040000FB RID: 251 + private float loseInChaseTimer; + + // Token: 0x040000FC RID: 252 + private bool startedMurderMusic; + + // Token: 0x040000FD RID: 253 + private PlayerControllerB targetedPlayerAlonePreviously; + + // Token: 0x040000FE RID: 254 + private bool checkedForTargetedPlayerPosition; + + // Token: 0x040000FF RID: 255 + private float timeAtLastTargetPlayerSync; + + // Token: 0x04000100 RID: 256 + private PlayerControllerB syncedTargetPlayer; + + // Token: 0x04000101 RID: 257 + private bool trackingTargetPlayerDownToMurder; + + // Token: 0x04000102 RID: 258 + private float premeditationTimeMultiplier = 1f; + + // Token: 0x04000103 RID: 259 + private float timeSpentWaitingForPlayer; + + // Token: 0x04000109 RID: 265 + private float timeAtLastButlerDamage; + + public Transform stabBloodParticleTransform; + + // Token: 0x0400010A RID: 266 + public ParticleSystem stabBloodParticle; + + // Token: 0x0400010B RID: 267 + private float timeAtLastHeardNoise; + + // Token: 0x0400010C RID: 268 + private bool killedLastTarget; + + // Token: 0x0400010D RID: 269 + private bool startedCrimeSceneTimer; + + // Token: 0x0400010E RID: 270 + private float leaveCrimeSceneTimer; + + // Token: 0x0400010F RID: 271 + private PlayerControllerB lastMurderedTarget; + + // Token: 0x04000110 RID: 272 + public GameObject knifePrefab; + + public AudioSource chaseMusicStart; + + // Token: 0x04000111 RID: 273 + public AudioSource chaseMusic; + + // Token: 0x04000112 RID: 274 + public static AudioSource murderMusicAudio; + + // Token: 0x04000113 RID: 275 + public static bool increaseMurderMusicVolume; + + // Token: 0x04000114 RID: 276 + public static float murderMusicVolume; + + private bool startedButlerDeathAnimation; + + // Token: 0x04000115 RID: 277 + public bool madlySearchingForPlayers; + + // Token: 0x04000116 RID: 278 + private float ambushSpeedMeter; + + // Token: 0x04000117 RID: 279 + private float timeSinceStealthStab; + + // Token: 0x04000118 RID: 280 + private float berserkModeTimer; + + public AnimationCurve footstepCurve; + + public const float WALKING_SPEED_TO_ANIMATION_SPEED = 1.646f; + public const float RUNNING_SPEED_TO_ANIMATION_SPEED = 0.823f; + public const float SPEED_TO_ANIMATION_SPEED = 1.5f; + public const float SPEED_TO_FOOTSTEP_SPEED = 0.7f; + + // Token: 0x060000E4 RID: 228 RVA: 0x0000A8C4 File Offset: 0x00008AC4 + private void LateUpdate() { + if (chaseMusic == MaidVariant.murderMusicAudio) { + if (MaidVariant.increaseMurderMusicVolume) MaidVariant.increaseMurderMusicVolume = false; + else MaidVariant.murderMusicVolume = Mathf.Max(MaidVariant.murderMusicVolume - Time.deltaTime * 0.4f, 0f); + + if (MaidVariant.murderMusicAudio != null) MaidVariant.murderMusicAudio.volume = MaidVariant.murderMusicVolume; + } + } + + // Token: 0x060000E5 RID: 229 RVA: 0x0000A930 File Offset: 0x00008B30 + public override void Start() { + base.Start(); + lastSeenPlayerPositions = new Vector3[ModPatch.ModCompability.GetStartOfRoundScriptLength()]; + seenPlayers = new bool[ModPatch.ModCompability.GetStartOfRoundScriptLength()]; + timeOfLastSeenPlayers = new float[ModPatch.ModCompability.GetStartOfRoundScriptLength()]; + + if (MaidVariant.murderMusicAudio == null) { + transform.SetParent(RoundManager.Instance.spawnedScrapContainer); + MaidVariant.murderMusicAudio = chaseMusic; + } + + if (StartOfRound.Instance.connectedPlayersAmount == 0){ + enemyHP = 2; + idleMovementSpeedBase *= 0.75f; + } + } + + // Token: 0x060000E6 RID: 230 RVA: 0x0000A9C0 File Offset: 0x00008BC0 + public override void KillEnemy(bool destroy = false) { + base.KillEnemy(destroy); + if (currentSearch.inProgress) StopSearch(currentSearch, true); + + chaseMusic.Stop(); + chaseMusic.volume = 0f; + agent.speed = 0f; + agent.acceleration = 1000f; + + if (!startedButlerDeathAnimation) { + startedButlerDeathAnimation = true; + this.creatureAnimator.SetTrigger("Dead"); + } + + } + + private bool previousKillingState = false; + private bool isRunning = false; + private void SwitchKillerState(bool killing){ + if (!IsOwner) { + SetButlerKillerStateLocal(killing); + return; + } + + SetButlerKillStateServerRpc(killing); + } + + + // Token: 0x060000E8 RID: 232 RVA: 0x0000AA70 File Offset: 0x00008C70 + public override void HitEnemy(int force = 1, PlayerControllerB playerWhoHit = null, bool playHitSFX = false, int hitID = -1) { + base.HitEnemy(force, playerWhoHit, playHitSFX, hitID); + enemyHP -= force; + + if (hitID == 5) enemyHP -= 100; + if (playerWhoHit != null) berserkModeTimer = 8f; + + if (enemyHP <= 0 && IsOwner) { + KillEnemyOnOwnerClient(false); + return; + } + + if (playerWhoHit != null && (currentBehaviourStateIndex != 2 || playerWhoHit != targetPlayer)) { + PingAttention(5, 0.6f, playerWhoHit.transform.position, false); + timeAtLastButlerDamage = Time.realtimeSinceStartup; + } + } + + // Token: 0x060000E9 RID: 233 RVA: 0x0000AB1C File Offset: 0x00008D1C + public override void DoAIInterval() + { + base.DoAIInterval(); + if (isEnemyDead || StartOfRound.Instance.allPlayersDead) return; + if (previousBehaviourState != currentBehaviourStateIndex) return; + + switch (currentBehaviourStateIndex) { + case 0: + // run away from crime scene + if (killedLastTarget && !startedCrimeSceneTimer) { + Debug.Log("Starting leave crime scene timer"); + Debug.Log(string.Format("Target player: {0}", targetPlayer.playerClientId)); + startedCrimeSceneTimer = true; + leaveCrimeSceneTimer = 15f; + movingTowardsTargetPlayer = false; + + if (hoverAroundTargetPlayer.inProgress) StopSearch(hoverAroundTargetPlayer, true); + if (roamAndSweepFloor.inProgress) StopSearch(roamAndSweepFloor, true); + SetDestinationToPosition(ChooseFarthestNodeFromPosition(transform.position, false, 0, false).position, false); + } + + // but also look for suspects + LookForChanceToMurder(2f); + return; + + case 1: + // look for suspects + LookForChanceToMurder(8f); + return; + + case 2: + if (GameNetworkManager.Instance.localPlayerController != targetPlayer) return; + if (!IsOwner) return; + + // once target is dead + if (targetPlayer.isPlayerDead && CheckLineOfSightForPosition(targetPlayer.deadBody.bodyParts[5].transform.position, 120f, 60, 2f, null)) { + + // continue berserking if players around + if (playersInVicinity < 2 || berserkModeTimer > 0f) { + PlayerControllerB playerControllerB = CheckLineOfSightForPlayer(100f, 50, 2); + if (playerControllerB != null) { + targetPlayer = playerControllerB; + SwitchOwnershipAndSetToStateServerRpc(2, targetPlayer.actualClientId, berserkModeTimer); + Debug.Log("State 2, changing ownership A"); + return; + } + } + + // otherwise try to run away + Debug.Log("State 2, Switching to state 0, killed target"); + SyncKilledLastTargetServerRpc((int)targetPlayer.playerClientId); + SwitchToBehaviourState(0); + ChangeOwnershipOfEnemy(StartOfRound.Instance.allPlayerScripts[0].actualClientId); + return; + } + + // loading up weapon (i think?) + if (timeSinceChangingItem < 1.7f || (pingAttentionTimer > 0f && berserkModeTimer <= 0f) || stunNormalizedTimer > 0f) { + agent.speed = 0f; + return; + } + + agent.speed = 5.5f; + isRunning = true; + + // lost player in chase + if (lostPlayerInChase) { + // search for last known pos + if (!hoverAroundTargetPlayer.inProgress) { + movingTowardsTargetPlayer = false; + StartSearch(lastSeenPlayerPositions[targetPlayer.playerClientId], hoverAroundTargetPlayer); + } + + PlayerControllerB x = CheckLineOfSightForPlayer(100f, 50, 2); + // completely lost player, time to give up + if (x == null) { + loseInChaseTimer += AIIntervalTime; + if (loseInChaseTimer > 12f) { + targetedPlayerAlonePreviously = this.targetPlayer; + Debug.Log("State 2, Switching to state 0, lost in chase"); + SwitchToBehaviourState(0); + ChangeOwnershipOfEnemy(StartOfRound.Instance.allPlayerScripts[0].actualClientId); + return; + } + } + // found previous suspect + else { + // they still alone, continue killing + if (x == targetPlayer && playersInVicinity < 2) { + lostPlayerInChase = false; + loseInChaseTimer = 0f; + return; + } + + // they no longer alone, whoops! + if (berserkModeTimer <= 0f) { + targetedPlayerAlonePreviously = this.targetPlayer; + Debug.Log("State 2, Switching to state 0, found another player or multiple players 1"); + SwitchToBehaviourState(0); + ChangeOwnershipOfEnemy(StartOfRound.Instance.allPlayerScripts[0].actualClientId); + return; + } + } + } + // chasing player + else { + PlayerControllerB x2 = CheckLineOfSightForPlayer(100f, 50, 2); + // lost player + if (x2 == null) { + loseInChaseTimer += AIIntervalTime; + if (loseInChaseTimer > 3.5f) { + lostPlayerInChase = true; + } + } + // found a player + else { + // who wasn't our og target, whoops! + if ((x2 != targetPlayer || playersInVicinity > 1) && berserkModeTimer <= 0f) { + targetedPlayerAlonePreviously = targetPlayer; + Debug.Log("State 2, Switching to state 0, found another player or multiple players 2"); + SwitchToBehaviourState(0); + ChangeOwnershipOfEnemy(StartOfRound.Instance.allPlayerScripts[0].actualClientId); + return; + } + + // actually they are our og target, continue killing + if (x2 == this.targetPlayer) { + loseInChaseTimer = 0f; + } + } + SetMovingTowardsTargetPlayer(this.targetPlayer); + } + return; + + default: + return; + } + } + + // Token: 0x060000EA RID: 234 RVA: 0x0000AF70 File Offset: 0x00009170 + public void LookForChanceToMurder(float waitForTime = 5f) { + // this function is not called during the Murder state + + if (!IsOwner) return; + + // we looking too sussy with people around + if (currentBehaviourStateIndex == 1 && playersInVicinity > 1) { + SwitchToBehaviourState(0); + } + + // no one is around + // we exit early here + if (playersInVicinity <= 0) { + // since we just killed, we need to bounce + if (killedLastTarget) { + leaveCrimeSceneTimer -= AIIntervalTime; + if (leaveCrimeSceneTimer <= 0f) { + killedLastTarget = false; + startedCrimeSceneTimer = false; + isRunning = false; + + SwitchKillerState(false); + SyncKilledLastTargetFalseClientRpc(); + Debug.Log("Exiting leave crime scene mode, 0"); + return; + } + } + // we haven't killed yet so we ain't scared + else { + if (!roamAndSweepFloor.inProgress) { + StartSearch(transform.position, roamAndSweepFloor); + sweepFloorTimer = 5f; + } + + hasPlayerInSight = false; + checkedForTargetedPlayerPosition = false; + timeSinceSeeingMultiplePlayers = 0f; + timeSinceCheckingForMultiplePlayers = 3f; + timeSpentWaitingForPlayer += AIIntervalTime; + + if (timeSpentWaitingForPlayer > 6f && !madlySearchingForPlayers) { + madlySearchingForPlayers = true; + roamAndSweepFloor.searchPrecision = 16f; + SyncSearchingMadlyServerRpc(true); + } + } + return; + } + + // this function seems to called once after we leave the Murder state + // and when we were just alone + + if (!hasPlayerInSight) { + hasPlayerInSight = true; + timeSpentWaitingForPlayer = 0f; + timeSinceNoticingFirstPlayer = 0f; + ButlerNoticePlayerServerRpc(); + + // we have left the Murder state and killed someone + if (killedLastTarget) { + // and their friend see the dead body, remove evidence + if (playersInVicinity < 2 && !Physics.Linecast(targetPlayer.gameplayCamera.transform.position, lastMurderedTarget.deadBody.bodyParts[6].transform.position + Vector3.up * 0.5f, StartOfRound.Instance.collidersAndRoomMaskAndDefault, QueryTriggerInteraction.Ignore)) { + killedLastTarget = false; + startedCrimeSceneTimer = false; + SwitchToBehaviourStateOnLocalClient(2); + SwitchOwnershipAndSetToStateServerRpc(2, targetPlayer.actualClientId, -1f); + Debug.Log("Found a player; Caught red-handed, entering murder state"); + + for (int i = 0; i < this.seenPlayers.Length; i++) { + Debug.Log(string.Format("Seen player {0}: {1}", i, this.seenPlayers[i])); + } + } + // no one saw nothing, we bounce + else { + killedLastTarget = false; + startedCrimeSceneTimer = false; + leaveCrimeSceneTimer = 0f; + isRunning = false; + + SwitchKillerState(false); + SyncKilledLastTargetFalseClientRpc(); + PingAttention(4, 0.6f, this.lastMurderedTarget.deadBody.bodyParts[0].transform.position, true); + } + } + + // the rest of the function sub seems to be normal following behaviour + + // stop sweeping + if (roamAndSweepFloor.inProgress) { + StopSearch(roamAndSweepFloor, true); + } + + // start following player again + if (!hoverAroundTargetPlayer.inProgress) { + StartSearch(targetPlayer.transform.position, hoverAroundTargetPlayer); + } + + // and it looks like our target is alone, kill + if (currentBehaviourStateIndex == 1 && CheckLineOfSightForPlayer(120f, 50, 2) == targetPlayer && playersInVicinity < 2 && timeSpentWaitingForPlayer > 12f) { + SwitchToBehaviourStateOnLocalClient(2); + SwitchOwnershipAndSetToStateServerRpc(2, targetPlayer.actualClientId, -1f); + return; + } + + // the timer for murder + premeditationTimeMultiplier -= AIIntervalTime * 0.04f; + if (!checkedForTargetedPlayerPosition && Time.realtimeSinceStartup - timeOfLastSeenPlayers[targetPlayer.playerClientId] > 4f) { + checkedForTargetedPlayerPosition = true; + PingAttention(4, 0.8f, lastSeenPlayerPositions[targetPlayer.playerClientId], true); + } + } + + // give up mad search + if (madlySearchingForPlayers) { + madlySearchingForPlayers = false; + SyncSearchingMadlyServerRpc(false); + roamAndSweepFloor.searchPrecision = 10f; + } + + // we got hit, now we angry + if (Time.realtimeSinceStartup - timeAtLastButlerDamage < 3f) { + SwitchToBehaviourStateOnLocalClient(2); + SwitchOwnershipAndSetToStateServerRpc(2, targetPlayer.actualClientId, -1f); + return; + } + + // maids borrow anger from each other + if (berserkModeTimer <= 0f) { + var enemies = Object.FindObjectsByType(FindObjectsSortMode.None); + foreach(var maid in enemies) { + if (maid.berserkModeTimer > 2f && Vector3.SqrMagnitude(maid.transform.position - transform.position) < 15f * 15f && !Physics.Linecast(eye.position, maid.eye.position, StartOfRound.Instance.collidersAndRoomMaskAndDefault, QueryTriggerInteraction.Ignore)){ + berserkModeTimer = maid.berserkModeTimer; + continue; + } + } + } + + // once again we sussy if people around + if (playersInVicinity > 1 && currentBehaviourStateIndex == 1 && berserkModeTimer <= 0f){ + SwitchToBehaviourState(0); + } + // we following our target cause we ready to kill or we angry + else if (trackingTargetPlayerDownToMurder || berserkModeTimer > 0f) { + PlayerControllerB x = CheckLineOfSightForPlayer(100f, 50, 2); + if (x != null) { + // found our target, kill + if (x == targetPlayer) { + SwitchToBehaviourStateOnLocalClient(2); + SwitchOwnershipAndSetToStateServerRpc(2, targetPlayer.actualClientId, berserkModeTimer); + } + // whoops wrong guy + else { + SwitchToBehaviourState(0); + } + } + } + + // search for other players + hoverAroundTargetPlayer.currentSearchStartPosition = targetPlayer.transform.position; + if (timeSinceCheckingForMultiplePlayers > timeUntilNextCheck && timeSinceNoticingFirstPlayer > 1.5f) { + StartCheckForPlayers(); + } else { + timeSinceCheckingForMultiplePlayers += AIIntervalTime; + } + + // search for other players to kill in general + // we get more impatient every time we see a new player or get angry + if (timeSinceSeeingMultiplePlayers > waitForTime * premeditationTimeMultiplier && !trackingTargetPlayerDownToMurder) { + // normal search + if (timeSinceCheckingForMultiplePlayers > 5f) { + StartCheckForPlayers(); + return; + } + + // + if (timeSinceCheckingForMultiplePlayers > 2f && timeSinceCheckingForMultiplePlayers < 4f) { + + if (currentBehaviourStateIndex == 0) { + // we found our og target again, we quick to angry + if (targetPlayer == targetedPlayerAlonePreviously) { + SwitchOwnershipAndSetToStateServerRpc(2, targetPlayer.actualClientId, -1f); + return; + } + + // otherwise we act normal + SwitchToBehaviourState(1); + return; + } else if (currentBehaviourStateIndex == 1) { + // we start tracking + trackingTargetPlayerDownToMurder = true; + PingAttention(4, 0.5f, lastSeenPlayerPositions[targetPlayer.playerClientId], true); + return; + } + } + } else { + timeSinceSeeingMultiplePlayers += AIIntervalTime; + } + } + + // Token: 0x060000EB RID: 235 RVA: 0x0000B5C8 File Offset: 0x000097C8 + private void ForgetSeenPlayers() { + for (int i = 0; i < timeOfLastSeenPlayers.Length; i++) { + if (seenPlayers[i]) { + if (StartOfRound.Instance.allPlayerScripts[i].isPlayerDead) { + seenPlayers[i] = false; + } + + float timeDiff = Time.realtimeSinceStartup - timeOfLastSeenPlayers[i]; + float forgetTimer; + if (i == (int)targetPlayer.playerClientId) forgetTimer = 15f; + else forgetTimer = 9f; + + var seenLastPlayer = timeDiff > forgetTimer; + + if (seenLastPlayer || (Time.realtimeSinceStartup - timeOfLastSeenPlayers[i] > 3f && timeSinceCheckingForMultiplePlayers > 2f && timeSinceCheckingForMultiplePlayers < 4f)) { + seenPlayers[i] = false; + } + } + } + + if (currentBehaviourStateIndex != 2 && targetPlayer != null && Physics.Linecast(transform.position + Vector3.up * 0.7f, targetPlayer.gameplayCamera.transform.position, StartOfRound.Instance.collidersAndRoomMaskAndDefault, QueryTriggerInteraction.Ignore) && isSweeping) { + isSweeping = false; + SetSweepingAnimServerRpc(false); + } + } + + // Token: 0x060000EC RID: 236 RVA: 0x0000B6F0 File Offset: 0x000098F0 + public override void DetectNoise(Vector3 noisePosition, float noiseLoudness, int timesPlayedInOneSpot = 0, int noiseID = 0) { + base.DetectNoise(noisePosition, noiseLoudness, timesPlayedInOneSpot, noiseID); + if (!IsOwner && noiseID != 75) return; + if (isEnemyDead) return; + + if (Time.realtimeSinceStartup - timeAtLastHeardNoise < 3f) return; + if (Vector3.Distance(noisePosition, transform.position + Vector3.up * 0.4f) < 0.75f) return; + + if (targetPlayer != null && Time.realtimeSinceStartup - timeOfLastSeenPlayers[targetPlayer.playerClientId] < 7f && Vector3.Distance(noisePosition + Vector3.up * 0.4f, targetPlayer.transform.position) < 1f) return; + + if (currentBehaviourStateIndex == 2) { + if (Vector3.Angle(transform.forward, noisePosition - transform.position) < 60f) return; + if (!lostPlayerInChase && Vector3.Distance(targetPlayer.transform.position, noisePosition) < 2f) return; + } + + float num = Vector3.Distance(noisePosition, transform.position); + float num2 = noiseLoudness / num; + if (Physics.Linecast(transform.position, noisePosition, StartOfRound.Instance.collidersAndRoomMaskAndDefault, QueryTriggerInteraction.Ignore)) { + num2 *= 0.5f; + } + + if (pingAttentionTimer > 0f) { + if (focusLevel >= 3) { + if (num > 3f || num2 <= 0.12f) return; + } else if (focusLevel == 2) { + if (num > 25f || num2 <= 0.09f) return; + } else if (focusLevel <= 1 && (num > 40f || num2 <= 0.06f)) { + return; + } + } + + if (num2 <= 0.03f) return; + + timeAtLastHeardNoise = Time.realtimeSinceStartup; + PingAttention(3, 0.5f, noisePosition + Vector3.up * 0.6f, true); + } + + // Token: 0x060000ED RID: 237 RVA: 0x0000B8E8 File Offset: 0x00009AE8 + public override void Update() { + base.Update(); + + if (GameNetworkManager.Instance.localPlayerController.isPlayerDead) { + chaseMusic.volume = Mathf.Lerp(chaseMusic.volume, 0f, AIIntervalTime * 9f); + } + + if (isEnemyDead || StartOfRound.Instance.allPlayersDead) { + return; + } + + timeSinceLastSpecialAnimation += Time.deltaTime; + if (berserkModeTimer > 0f) { + timeSinceChangingItem += Time.deltaTime * 6f; + } else { + timeSinceChangingItem += Time.deltaTime; + } + + timeSinceHittingPlayer += Time.deltaTime; + timeSinceNoticingFirstPlayer += Time.deltaTime; + pingAttentionTimer -= Time.deltaTime; + timeSincePingingAttention += Time.deltaTime; + berserkModeTimer -= Time.deltaTime; + + CalculateAnimationDirection(1f); + FootstepLoop(); + + if (IsOwner) { + CheckLOS(); + ForgetSeenPlayers(); + } + + AnimateLooking(); + switch (currentBehaviourStateIndex) { + case 0: + if (previousBehaviourState != currentBehaviourStateIndex) { + if (targetPlayer != null) { + Debug.Log(string.Format("Target player: {0}; is dead?: {1}", targetPlayer.playerClientId, targetPlayer.isPlayerDead)); + } + + creatureSFX.PlayOneShot(enemyType.audioClips[1]); + WalkieTalkie.TransmitOneShotAudio(creatureSFX, enemyType.audioClips[1], 1f); + checkedForTargetedPlayerPosition = false; + timeSinceSeeingMultiplePlayers = 0f; + movingTowardsTargetPlayer = false; + trackingTargetPlayerDownToMurder = false; + addPlayerVelocityToDestination = 0f; + agent.acceleration = 26f; + previousBehaviourState = currentBehaviourStateIndex; + } + + if (killedLastTarget || madlySearchingForPlayers) { + isRunning = true; + } else { + isRunning = false; + } + + SwitchKillerState(false); + + if (MaidVariant.murderMusicAudio.isPlaying && MaidVariant.murderMusicAudio.volume <= 0.01f) { + MaidVariant.murderMusicAudio.Stop(); + } + + startedMurderMusic = false; + if (!IsOwner) return; + + SetButlerWalkSpeed(); + if (timeSinceNoticingFirstPlayer > 1f && !madlySearchingForPlayers) { + if (sweepFloorTimer <= 0f) { + sweepFloorTimer = Random.Range(3.5f, 8f); + isSweeping = !isSweeping; + SetSweepingAnimServerRpc(isSweeping); + return; + } + + sweepFloorTimer -= Time.deltaTime; + return; + } + break; + + case 1: + if (previousBehaviourState != currentBehaviourStateIndex) { + creatureSFX.PlayOneShot(enemyType.audioClips[1]); + WalkieTalkie.TransmitOneShotAudio(creatureSFX, enemyType.audioClips[1], 1f); + + SwitchKillerState(false); + + isSweeping = false; + checkedForTargetedPlayerPosition = false; + movingTowardsTargetPlayer = false; + startedCrimeSceneTimer = false; + killedLastTarget = false; + madlySearchingForPlayers = false; + previousBehaviourState = currentBehaviourStateIndex; + isRunning = false; + } + + if (MaidVariant.murderMusicAudio.isPlaying && MaidVariant.murderMusicAudio.volume <= 0.01f) { + MaidVariant.murderMusicAudio.Stop(); + } + + SetButlerWalkSpeed(); + return; + + case 2: + if (previousBehaviourState != currentBehaviourStateIndex) { + SwitchKillerState(true); + creatureSFX.PlayOneShot(enemyType.audioClips[2]); + WalkieTalkie.TransmitOneShotAudio(creatureSFX, enemyType.audioClips[2], 1f); + + if (roamAndSweepFloor.inProgress) { + StopSearch(roamAndSweepFloor, true); + } + + if (hoverAroundTargetPlayer.inProgress) { + StopSearch(hoverAroundTargetPlayer, true); + } + + ambushSpeedMeter = 1f; + startedCrimeSceneTimer = false; + killedLastTarget = false; + madlySearchingForPlayers = false; + hasPlayerInSight = false; + previousBehaviourState = currentBehaviourStateIndex; + + playedChaseMusicStart = false; + if (targetPlayer.IsLocalPlayer) { + PlayChaseMusicStart(); + } + } + + addPlayerVelocityToDestination = Mathf.Lerp(addPlayerVelocityToDestination, 2f, Time.deltaTime); + if (timeSinceChangingItem > 1.7f) { + ambushSpeedMeter = Mathf.Max(ambushSpeedMeter - Time.deltaTime * 1.2f, 0f); + } + + if (lostPlayerInChase && IsOwner) { + if (startedMurderMusic && MaidVariant.murderMusicAudio.isPlaying && MaidVariant.murderMusicAudio.volume <= 0.01f) { + MaidVariant.murderMusicAudio.Stop(); + return; + } + } else if (!startedMurderMusic) { + if (GameNetworkManager.Instance.localPlayerController.isInsideFactory && GameNetworkManager.Instance.localPlayerController.HasLineOfSightToPosition(transform.position + Vector3.up * 0.7f, 100f, 18, 1f)) { + startedMurderMusic = true; + return; + } + } else { + + if (GameNetworkManager.Instance.localPlayerController.isInsideFactory) { + if (GameNetworkManager.Instance.localPlayerController.HasLineOfSightToPosition(transform.position + Vector3.up * 0.7f, 100f, 18, 1f)) { + PlayChaseMusicStart(); + MaidVariant.murderMusicVolume = Mathf.Max(MaidVariant.murderMusicVolume, Mathf.Lerp(MaidVariant.murderMusicVolume, 0.5f, Time.deltaTime * 3f)); + } else { + MaidVariant.murderMusicVolume = Mathf.Max(MaidVariant.murderMusicVolume, Mathf.Lerp(MaidVariant.murderMusicVolume, 0.2f, Time.deltaTime * 3f)); + } + MaidVariant.increaseMurderMusicVolume = true; + } + + if (!MaidVariant.murderMusicAudio.isPlaying) { + MaidVariant.murderMusicAudio.Play(); + } + } + break; + default: + return; + } + } + + private bool playedChaseMusicStart; + public void PlayChaseMusicStart(){ + if (playedChaseMusicStart) return; + + chaseMusicStart.Play(); + playedChaseMusicStart = true; + } + + // Token: 0x060000EE RID: 238 RVA: 0x0000C1B4 File Offset: 0x0000A3B4 + [ServerRpc(RequireOwnership = false)] + public void SyncSearchingMadlyServerRpc(bool isSearching) { + madlySearchingForPlayers = isSearching; + SyncSearchingMadlyClientRpc(isSearching); + } + + // Token: 0x060000EF RID: 239 RVA: 0x0000C2A8 File Offset: 0x0000A4A8 + [ClientRpc] + public void SyncSearchingMadlyClientRpc(bool isSearching){ + if (IsServer) return; + madlySearchingForPlayers = isSearching; + } + + // Token: 0x060000F0 RID: 240 RVA: 0x0000C39C File Offset: 0x0000A59C + [ServerRpc(RequireOwnership = false)] + public void SyncKilledLastTargetServerRpc(int playerId) { + killedLastTarget = true; + lastMurderedTarget = StartOfRound.Instance.allPlayerScripts[playerId]; + SyncKilledLastTargetClientRpc(); + } + + // Token: 0x060000F1 RID: 241 RVA: 0x0000C490 File Offset: 0x0000A690 + [ClientRpc] + public void SyncKilledLastTargetClientRpc() { + if (IsServer) return; + killedLastTarget = true; + } + + // Token: 0x060000F2 RID: 242 RVA: 0x0000C568 File Offset: 0x0000A768 + [ServerRpc(RequireOwnership = false)] + public void SyncKilledLastTargetFalseServerRpc() { + killedLastTarget = false; + SyncKilledLastTargetClientRpc(); + } + + + // Token: 0x060000F3 RID: 243 RVA: 0x0000C640 File Offset: 0x0000A840 + [ClientRpc] + public void SyncKilledLastTargetFalseClientRpc() { + if (IsServer) return; + Debug.Log("Client received sync killed last target false client rpc"); + killedLastTarget = false; + } + + // Token: 0x060000F4 RID: 244 RVA: 0x0000C724 File Offset: 0x0000A924 + [ServerRpc] + public void SwitchOwnershipAndSetToStateServerRpc(int state, ulong newOwner, float berserkTimer = -1f) { + if (gameObject.GetComponent().OwnerClientId != newOwner) thisNetworkObject.ChangeOwnership(newOwner); + + if (StartOfRound.Instance.ClientPlayerList.TryGetValue(newOwner, out var num)) { + targetPlayer = StartOfRound.Instance.allPlayerScripts[num]; + watchingPlayer = targetPlayer; + + if (berserkTimer != -1f) berserkModeTimer = berserkTimer; + SwitchOwnershipAndSetToStateClientRpc(num, state, berserkModeTimer); + } + } + + // Token: 0x060000F5 RID: 245 RVA: 0x0000C8D8 File Offset: 0x0000AAD8 + [ClientRpc] + public void SwitchOwnershipAndSetToStateClientRpc(int playerVal, int state, float berserkTimer) { + currentOwnershipOnThisClient = playerVal; + SwitchToBehaviourStateOnLocalClient(state); + targetPlayer = StartOfRound.Instance.allPlayerScripts[playerVal]; + watchingPlayer = targetPlayer; + berserkModeTimer = berserkTimer; + } + + // Token: 0x060000F6 RID: 246 RVA: 0x0000CA08 File Offset: 0x0000AC08 + public void SetButlerWalkSpeed() { + if (!IsOwner) return; + + if (timeSinceCheckingForMultiplePlayers < 2f || timeSinceChangingItem < 2f || isSweeping || stunNormalizedTimer > 0f) { + agent.speed = 0f; + return; + } + + if (trackingTargetPlayerDownToMurder || isRunning) { + if (currentBehaviourStateIndex == 2) { + agent.speed = idleMovementSpeedBase + 4f + 4f * ambushSpeedMeter; + agent.acceleration = 38f + 16f * ambushSpeedMeter; + } + agent.speed = idleMovementSpeedBase + 4f; + return; + } + + agent.speed = idleMovementSpeedBase; + } + + // Token: 0x060000F7 RID: 247 RVA: 0x0000CAEB File Offset: 0x0000ACEB + private void StartCheckForPlayers() { + timeSinceCheckingForMultiplePlayers = 0f; + timeUntilNextCheck = Random.Range(8f, 11f); + TurnAndCheckForPlayers(); + Debug.Log("Butler: Checking for players"); + } + + // Token: 0x060000F8 RID: 248 RVA: 0x0000CB20 File Offset: 0x0000AD20 + [ServerRpc] + public void SetButlerKillStateServerRpc(bool isRunning) { + SetButlerKillerStateClientRpc(isRunning); + } + + // Token: 0x060000F9 RID: 249 RVA: 0x0000CC50 File Offset: 0x0000AE50 + [ClientRpc] + public void SetButlerKillerStateClientRpc(bool isKilling) { + SetButlerKillerStateLocal(isKilling); + } + + private void SetButlerKillerStateLocal(bool isKilling) { + if (previousKillingState != isKilling) { + if (isKilling) creatureAnimator.SetTrigger("Killer"); + else creatureAnimator.SetTrigger("Normal"); + + previousKillingState = isKilling; + timeSinceChangingItem = 0f; + } + } + + // Token: 0x060000FA RID: 250 RVA: 0x0000CD44 File Offset: 0x0000AF44 + [ServerRpc] + public void SetSweepingAnimServerRpc(bool sweeping) { + SetSweepingAnimClientRpc(sweeping); + } + + // Token: 0x060000FB RID: 251 RVA: 0x0000CE74 File Offset: 0x0000B074 + [ClientRpc] + public void SetSweepingAnimClientRpc(bool sweeping) { + creatureAnimator.SetBool("Cleaning", sweeping); + timeSinceChangingItem = 0f; + } + + // Token: 0x060000FC RID: 252 RVA: 0x0000CF74 File Offset: 0x0000B174 + private void CalculateAnimationDirection(float maxSpeed = 1f) { + agentLocalVelocity = animationContainer.InverseTransformDirection(Vector3.ClampMagnitude(transform.position - previousPosition, 1f) / (Time.deltaTime * 2f)); + velX = Mathf.Lerp(velX, agentLocalVelocity.x, 10f * Time.deltaTime); + creatureAnimator.SetFloat("Hor", Mathf.Clamp(velX, -maxSpeed, maxSpeed)); + velZ = Mathf.Lerp(velZ, agentLocalVelocity.z, 10f * Time.deltaTime); + creatureAnimator.SetFloat("Ver", Mathf.Clamp(velZ, -maxSpeed, maxSpeed)); + + var v = (velX + velZ) * (isRunning ? RUNNING_SPEED_TO_ANIMATION_SPEED : WALKING_SPEED_TO_ANIMATION_SPEED); + creatureAnimator.SetFloat("SpeedMul", v * SPEED_TO_ANIMATION_SPEED); + + previousPosition = transform.position; + } + + // Token: 0x060000FD RID: 253 RVA: 0x0000D060 File Offset: 0x0000B260 + public void PingAttention(int newFocusLevel, float timeToLook, Vector3 attentionPosition, bool sync = true) { + if (pingAttentionTimer >= 0f && newFocusLevel < focusLevel) return; + + if (currentBehaviourStateIndex == 0 && timeSincePingingAttention < 0.5f) return; + + if (currentBehaviourStateIndex == 1 && timeSincePingingAttention < 0.2f) return; + + if (currentBehaviourStateIndex == 2 && timeSincePingingAttention < 1f) return; + + if (berserkModeTimer > 0f) { + if (timeSincePingingAttention < 4f) return; + timeToLook *= 0.5f; + } + + Debug.Log("Butler: pinged attention to position"); + Debug.DrawLine(eye.position, attentionPosition, Color.yellow, timeToLook); + focusLevel = newFocusLevel; + pingAttentionTimer = timeToLook; + pingAttentionPosition = attentionPosition; + if (sync) PingButlerAttentionServerRpc(timeToLook, attentionPosition); + } + + // Token: 0x060000FE RID: 254 RVA: 0x0000D130 File Offset: 0x0000B330 + [ServerRpc] + public void PingButlerAttentionServerRpc(float timeToLook, Vector3 attentionPosition) { + PingButlerAttentionClientRpc(timeToLook, attentionPosition); + } + + // Token: 0x060000FF RID: 255 RVA: 0x0000D270 File Offset: 0x0000B470 + [ClientRpc] + public void PingButlerAttentionClientRpc(float timeToLook, Vector3 attentionPosition) { + if (IsOwner) return; + pingAttentionTimer = timeToLook; + pingAttentionPosition = attentionPosition; + } + + // Token: 0x06000100 RID: 256 RVA: 0x0000D378 File Offset: 0x0000B578 + [ServerRpc] + public void ButlerNoticePlayerServerRpc() { + ButlerNoticePlayerClientRpc(); + } + + // Token: 0x06000101 RID: 257 RVA: 0x0000D48C File Offset: 0x0000B68C + [ClientRpc] + public void ButlerNoticePlayerClientRpc() { + timeSinceNoticingFirstPlayer = 0f; + hasPlayerInSight = true; + pingAttentionTimer = -1f; + } + + // Token: 0x06000102 RID: 258 RVA: 0x0000D574 File Offset: 0x0000B774 + public void TurnAndCheckForPlayers() { + RoundManager.Instance.tempTransform.position = transform.position; + float num = RoundManager.Instance.YRotationThatFacesTheFarthestFromPosition(transform.position, 40f, 8); + CheckForPlayersServerRpc(Random.Range(0.4f, 0.8f), Random.Range(0.4f, 0.8f), (int)num); + } + + // Token: 0x06000103 RID: 259 RVA: 0x0000D5E0 File Offset: 0x0000B7E0 + [ServerRpc] + public void CheckForPlayersServerRpc(float timeToCheck, float timeToCheckB, int yRot) { + CheckForPlayersClientRpc(timeToCheck, timeToCheckB, yRot); + } + + // Token: 0x06000104 RID: 260 RVA: 0x0000D73C File Offset: 0x0000B93C + [ClientRpc] + public void CheckForPlayersClientRpc(float timeToCheck, float timeToCheckB, int yRot) { + if (checkForPlayersCoroutine != null) StopCoroutine(checkForPlayersCoroutine); + + checkForPlayersCoroutine = StartCoroutine(CheckForPlayersAnim(timeToCheck, timeToCheckB, yRot)); + } + + // Token: 0x06000105 RID: 261 RVA: 0x0000D870 File Offset: 0x0000BA70 + private IEnumerator CheckForPlayersAnim(float timeToCheck, float timeToCheckB, int yRot) + { + RoundManager.Instance.tempTransform.position = transform.position; + RoundManager.Instance.tempTransform.eulerAngles = new Vector3(0f, yRot, 0f); + PingAttention(2, 2f, transform.position + Vector3.up * 0.4f + RoundManager.Instance.tempTransform.forward * 14f, false); + yield return new WaitForSeconds(timeToCheck); + + RoundManager.Instance.tempTransform.position = transform.position; + RoundManager.Instance.tempTransform.eulerAngles = new Vector3(0f, yRot + 144f * timeToCheckB, 0f); + PingAttention(2, 2f, transform.position + Vector3.up * 0.4f + RoundManager.Instance.tempTransform.forward * 14f, false); + yield break; + } + + // Token: 0x06000106 RID: 262 RVA: 0x0000D894 File Offset: 0x0000BA94 + private void AnimateLooking() { + if (stunNormalizedTimer > 0f) { + agent.angularSpeed = 400f; + headLookRig.weight = Mathf.Lerp(headLookRig.weight, 0f, Time.deltaTime * 16f); + return; + } + + bool flag = false; + if (watchingPlayer && currentBehaviourStateIndex != 2 && timeSinceNoticingFirstPlayer > 1f && Time.realtimeSinceStartup - timeSinceStealthStab > 3f) { + flag = watchingPlayer.HasLineOfSightToPosition(transform.position + Vector3.up * 0.7f, 30f, 50, -1f); + } + + if (pingAttentionTimer >= 0f && timeSinceNoticingFirstPlayer > 1f) { + lookTarget.position = Vector3.Lerp(lookTarget.position, pingAttentionPosition, 12f * Time.deltaTime); + flag = false; + } else { + if (!(watchingPlayer != null) || Physics.Linecast(transform.position + Vector3.up * 0.6f, watchingPlayer.gameplayCamera.transform.position, StartOfRound.Instance.collidersAndRoomMaskAndDefault, QueryTriggerInteraction.Ignore)) { + agent.angularSpeed = 400f; + headLookRig.weight = Mathf.Lerp(headLookRig.weight, 0f, Time.deltaTime * 16f); + return; + } + lookTarget.position = Vector3.Lerp(lookTarget.position, watchingPlayer.gameplayCamera.transform.position, 10f * Time.deltaTime); + } + + if (IsOwner) { + if (flag) { + float num = Vector3.Angle(transform.forward, Vector3.Scale(new Vector3(1f, 0f, 1f), lookTarget.position - transform.position)); + if (num < 22f) { + if (velZ >= 0f) { + agent.angularSpeed = 0f; + if (Vector3.Dot(new Vector3(lookTarget.position.x, transform.position.y, lookTarget.position.z) - transform.position, transform.right) > 0f) { + transform.rotation *= Quaternion.Euler(0f, -55f * Time.deltaTime, 0f); + } else { + transform.rotation *= Quaternion.Euler(0f, 55f * Time.deltaTime, 0f); + } + } else { + agent.angularSpeed = 400f; + } + } else if (num > 30f) { + agent.angularSpeed = 400f; + } else { + agent.angularSpeed = 25f; + } + } else if (currentBehaviourStateIndex == 2 || Vector3.Angle(transform.forward, Vector3.Scale(new Vector3(1f, 0f, 1f), lookTarget.position - transform.position)) > 15f) { + agent.angularSpeed = 0f; + turnCompass.LookAt(lookTarget); + turnCompass.eulerAngles = new Vector3(0f, turnCompass.eulerAngles.y, 0f); + float num2 = 3f; + if (berserkModeTimer > 0f && timeSinceChangingItem < 3f) { + num2 = 10f; + } + transform.rotation = Quaternion.Lerp(transform.rotation, turnCompass.rotation, num2 * Time.deltaTime); + transform.localEulerAngles = new Vector3(0f, transform.localEulerAngles.y, 0f); + } + } + if (flag) { + headLookRig.weight = Mathf.Lerp(headLookRig.weight, 0f, 15f * Time.deltaTime); + } else { + float num3 = Vector3.Angle(transform.forward, lookTarget.position - transform.position); + if (num3 > 22f) { + headLookRig.weight = Mathf.Lerp(headLookRig.weight, 1f * (Mathf.Abs(num3 - 180f) / 180f), Time.deltaTime * 11f); + } else { + headLookRig.weight = Mathf.Lerp(headLookRig.weight, 1f, Time.deltaTime * 11f); + } + } + + headLookTarget.position = Vector3.Lerp(headLookTarget.position, lookTarget.position, 8f * Time.deltaTime); + } + + // Token: 0x06000108 RID: 264 RVA: 0x0000DE44 File Offset: 0x0000C044 + public override void OnCollideWithPlayer(Collider other) { + base.OnCollideWithPlayer(other); + if (isEnemyDead) return; + + timeSinceStealthStab = Time.realtimeSinceStartup; + if (currentBehaviourStateIndex != 2 && (Random.Range(0, 100) < 86 || Time.realtimeSinceStartup - timeSinceStealthStab < 10f)) return; + if (timeSinceHittingPlayer < 1f) return; + + PlayerControllerB playerControllerB = MeetsStandardPlayerCollisionConditions(other, false, false); + if (playerControllerB != null) { + timeSinceHittingPlayer = 0f; + if (playerControllerB == GameNetworkManager.Instance.localPlayerController) { + if (currentBehaviourStateIndex != 2) { + berserkModeTimer = 3f; + } + playerControllerB.DamagePlayer(50, true, true, CauseOfDeath.Stabbing, 0, false); + StabPlayerServerRpc((int)playerControllerB.playerClientId, currentBehaviourStateIndex != 2); + } + } + } + + + // Token: 0x06000109 RID: 265 RVA: 0x0000DF18 File Offset: 0x0000C118 + [ServerRpc(RequireOwnership = false)] + public void StabPlayerServerRpc(int playerId, bool setBerserkMode) { + if (setBerserkMode) { + berserkModeTimer = 3f; + targetPlayer = StartOfRound.Instance.allPlayerScripts[playerId]; + watchingPlayer = targetPlayer; + SwitchOwnershipAndSetToStateServerRpc(2, targetPlayer.actualClientId, -1f); + seenPlayers[playerId] = true; + } + StabPlayerClientRpc(playerId, setBerserkMode); + } + + // Token: 0x0600010A RID: 266 RVA: 0x0000E05C File Offset: 0x0000C25C + [ClientRpc] + public void StabPlayerClientRpc(int playerId, bool setBerserkMode) { + timeSinceStealthStab = Time.realtimeSinceStartup; + creatureAnimator.SetTrigger("Attacking"); + stabBloodParticle.Play(true); + creatureSFX.PlayOneShot(enemyType.audioClips[0]); + } + + // Token: 0x0600010D RID: 269 RVA: 0x0000E4FC File Offset: 0x0000C6FC + public void CheckLOS() { + int num = 0; + float num2 = 10000f; + int num3 = -1; + for (int i = 0; i < StartOfRound.Instance.allPlayerScripts.Length; i++) { + if (StartOfRound.Instance.allPlayerScripts[i].isPlayerDead || !StartOfRound.Instance.allPlayerScripts[i].isPlayerControlled) { + seenPlayers[i] = false; + } else { + if (CheckLineOfSightForPosition(StartOfRound.Instance.allPlayerScripts[i].gameplayCamera.transform.position, 110f, 60, 2f, null)) { + num++; + lastSeenPlayerPositions[i] = StartOfRound.Instance.allPlayerScripts[i].gameplayCamera.transform.position; + seenPlayers[i] = true; + timeOfLastSeenPlayers[i] = Time.realtimeSinceStartup; + } else if (seenPlayers[i]) { + num++; + } + + if (seenPlayers[i]) { + float num4 = Vector3.Distance(eye.position, StartOfRound.Instance.allPlayerScripts[i].gameplayCamera.transform.position); + if (num4 < num2) { + num2 = num4; + num3 = i; + } + } + } + } + + if (num > 1) { + timeSinceSeeingMultiplePlayers = 0f; + } + + playersInVicinity = num; + if (currentBehaviourStateIndex == 2) return; + + if (num3 != -1) { + watchingPlayer = StartOfRound.Instance.allPlayerScripts[num3]; + if (currentBehaviourStateIndex != 2) { + targetPlayer = watchingPlayer; + } + } + + if (Time.realtimeSinceStartup - timeAtLastTargetPlayerSync > 0.25f && syncedTargetPlayer != targetPlayer) { + timeAtLastTargetPlayerSync = Time.realtimeSinceStartup; + syncedTargetPlayer = targetPlayer; + SyncTargetServerRpc((int)targetPlayer.playerClientId); + } + } + + // Token: 0x0600010E RID: 270 RVA: 0x0000E6D0 File Offset: 0x0000C8D0 + [ServerRpc] + public void SyncTargetServerRpc(int playerId) { + SyncTargetClientRpc(playerId); + } + + // Token: 0x0600010F RID: 271 RVA: 0x0000E7F4 File Offset: 0x0000C9F4 + [ClientRpc] + public void SyncTargetClientRpc(int playerId) { + watchingPlayer = StartOfRound.Instance.allPlayerScripts[playerId]; + targetPlayer = watchingPlayer; + syncedTargetPlayer = targetPlayer; + } + + private float prevFootstepValue; + private float currentFootstepTime; + public void FootstepLoop() { + var v = (velX + velZ) * SPEED_TO_FOOTSTEP_SPEED; + var t = Time.deltaTime * v; + currentFootstepTime += t; + if (currentFootstepTime > 1) { + currentFootstepTime -= 1; + } + + var f = footstepCurve.Evaluate(currentFootstepTime); + if ((f > 0 && prevFootstepValue < 0) || (f < 0 && prevFootstepValue > 0)) { + int num = Random.Range(0, footsteps.Length); + if (footsteps[num] == null) return; + + creatureSFX.PlayOneShot(footsteps[num]); + WalkieTalkie.TransmitOneShotAudio(creatureSFX, footsteps[num], 1f); + } + prevFootstepValue = f; + } + + // Token: 0x06000111 RID: 273 RVA: 0x0000E98C File Offset: 0x0000CB8C + public override void AnimationEventB() { + base.AnimationEventB(); + int num = Random.Range(0, broomSweepSFX.Length); + if (broomSweepSFX[num] == null) return; + + creatureSFX.PlayOneShot(broomSweepSFX[num]); + WalkieTalkie.TransmitOneShotAudio(creatureSFX, broomSweepSFX[num], 1f); + } + } +} \ No newline at end of file diff --git a/ScarletMansion/ScarletMansion/GamePatch/InitPatch.cs b/ScarletMansion/ScarletMansion/GamePatch/InitPatch.cs index 9ee2fe2..815ebdd 100644 --- a/ScarletMansion/ScarletMansion/GamePatch/InitPatch.cs +++ b/ScarletMansion/ScarletMansion/GamePatch/InitPatch.cs @@ -14,6 +14,8 @@ using LethalLevelLoader; using static UnityEngine.GraphicsBuffer; using ScarletMansion.GamePatch.Components; using System.Security.Cryptography; +using ScarletMansion.GamePatch.Enemies; +using UnityEngine.UI; namespace ScarletMansion.GamePatch { @@ -130,11 +132,12 @@ namespace ScarletMansion.GamePatch { } } + var allEnemies = round.levels + .SelectMany(lev => lev.Enemies); + var knight = Assets.knight; if (knight.enemyType == null){ - var springItem = round.levels - .SelectMany(lev => lev.Enemies) - .FirstOrDefault(e => e.enemyType.name.ToLowerInvariant() == "springman"); + var springItem = allEnemies.FirstOrDefault(e => e.enemyType.name.ToLowerInvariant() == "springman"); if (GameReadNullCheck(springItem, "springman", "Knight enemy will not spawn")) { var type = ScriptableObject.Instantiate(springItem.enemyType); @@ -143,11 +146,44 @@ namespace ScarletMansion.GamePatch { type.enemyName = "Knight"; knight.enemyType = type; - knight.enemy.GetComponentInChildren().enemyType = type; - Enemies.RegisterEnemy(type, 0, Levels.LevelTypes.None, knight.terminalNode, knight.terminalKeyword); + knight.enemy.GetComponentInChildren().enemyType = type; + LethalLib.Modules.Enemies.RegisterEnemy(type, 0, Levels.LevelTypes.None, knight.terminalNode, knight.terminalKeyword); } } + var maid = Assets.maid; + if (maid.enemyType == null){ + var butlerItem = allEnemies.FirstOrDefault(e => e.enemyType.name.ToLowerInvariant() == "butler"); + + if (GameReadNullCheck(butlerItem, "butler", "Maid enemy will not spawn")) { + var type = ScriptableObject.Instantiate(butlerItem.enemyType); + type.name = "Maid"; + type.enemyPrefab = maid.enemy; + type.enemyName = "Maid"; + type.pushPlayerForce *= 0.25f; + + maid.enemyType = type; + var maidScript = maid.enemy.GetComponentInChildren(); + maidScript.enemyType = type; + + var butlerPrefab = butlerItem.enemyType.enemyPrefab; + var butlerScript = butlerPrefab.GetComponent(); + var butlerBloodStabParticle = Utility.FindChildRecurvisely(butlerPrefab.transform, "BloodStabParticle"); + var butlerBloodParticle = Utility.FindChildRecurvisely(butlerPrefab.transform, "BloodParticle"); + + if (GameReadNullCheck(butlerBloodStabParticle, "BloodStabParticle", "Messed up errors will probably happen with blood splats") && GameReadNullCheck(butlerBloodParticle, "BloodParticle", "Messed up errors will probably happen with blood splats")){ + var ps1 = maidScript.stabBloodParticle.GetComponent(); + ps1.material = butlerBloodStabParticle.GetComponent().material; + + var ps2 = ps1.transform.GetChild(0).GetComponent(); + ps2.material = butlerBloodParticle.GetComponent().material; + } + + maidScript.knifePrefab = butlerScript.knifePrefab; + + LethalLib.Modules.Enemies.RegisterEnemy(type, 0, Levels.LevelTypes.None, maid.terminalNode, maid.terminalKeyword); + } + } } catch (Exception e) { diff --git a/ScarletMansion/ScarletMansion/GamePatch/LoadAssetsIntoLevelPatch.cs b/ScarletMansion/ScarletMansion/GamePatch/LoadAssetsIntoLevelPatch.cs index c73cec8..03cf86e 100644 --- a/ScarletMansion/ScarletMansion/GamePatch/LoadAssetsIntoLevelPatch.cs +++ b/ScarletMansion/ScarletMansion/GamePatch/LoadAssetsIntoLevelPatch.cs @@ -101,41 +101,41 @@ namespace ScarletMansion.GamePatch { currentEnemiesRarity = lastEnemiesRarity.ToList(); currentItemsRarity = lastItemsRarity.ToList(); - if (Assets.knight != null) { - var baseWeight = PluginConfig.Instance.knightWeightBaseValue; - var target = currentEnemiesRarity - .Where(c => c.enemyType.name.ToLowerInvariant() == "springman") - .FirstOrDefault(); + void AddEnemy(Assets.Enemy enemy, string sourceEnemyName, string targetEnemyName, int baseWeight, int minBaseWeight, float weightStealPercentage) { + if (enemy != null) { + var target = currentEnemiesRarity + .Where(c => c.enemyType.name.ToLowerInvariant() == targetEnemyName) + .FirstOrDefault(); - if (target == null){ - const int noCoilheaBaseKnightRarity = 10; - Plugin.logger.LogInfo($"No spring enemy in level, using default rarity of {noCoilheaBaseKnightRarity} for knight"); + if (target == null){ + Plugin.logger.LogInfo($"No enemy {targetEnemyName} in level, using default rarity of {minBaseWeight}."); + var entry = enemy.GetItemEntry(baseWeight + minBaseWeight); - var knight = Assets.knight.GetItemEntry(noCoilheaBaseKnightRarity + baseWeight); + Plugin.logger.LogInfo($"Adding enemy {sourceEnemyName} with weight {entry.rarity}"); + currentEnemiesRarity.Add(entry); - Plugin.logger.LogInfo($"Adding enemy Knight with weight {knight.rarity}"); - currentEnemiesRarity.Add(knight); + } else { + currentEnemiesRarity.Remove(target); + var entryRarity = Mathf.RoundToInt(target.rarity * weightStealPercentage); + var prevEntry = new SpawnableEnemyWithRarity(); + prevEntry.enemyType = target.enemyType; + prevEntry.rarity = target.rarity - entryRarity; + + var newEntry = enemy.GetItemEntry(baseWeight + entryRarity); + + Plugin.logger.LogInfo($"Adding enemy {sourceEnemyName} with weight {newEntry.rarity}"); + Plugin.logger.LogInfo($"Setting enemy {targetEnemyName} with weight {prevEntry.rarity}"); + currentEnemiesRarity.Add(newEntry); + currentEnemiesRarity.Add(prevEntry); + } } else { - currentEnemiesRarity.Remove(target); - var percentage = PluginConfig.Instance.knightWeightStealPercentageValue; - var knightRarity = Mathf.RoundToInt(target.rarity * percentage); - - var spring = new SpawnableEnemyWithRarity(); - spring.enemyType = target.enemyType; - spring.rarity = target.rarity - knightRarity; - - var knight = Assets.knight.GetItemEntry(knightRarity + baseWeight); - - Plugin.logger.LogInfo($"Adding enemy Knight with weight {knight.rarity}"); - Plugin.logger.LogInfo($"Setting enemy Coil-head with weight {spring.rarity}"); - currentEnemiesRarity.Add(spring); - currentEnemiesRarity.Add(knight); + Plugin.logger.LogError($"Failed to load custom enemy {sourceEnemyName} as their reference is missing"); } - } else { - Plugin.logger.LogError($"Failed to load custom enemy as their reference is missing"); } + AddEnemy(Assets.knight, "knight", "springman", PluginConfig.Instance.knightWeightBaseValue, 10, PluginConfig.Instance.knightWeightStealPercentageValue); + AddEnemy(Assets.maid, "maid", "butler", PluginConfig.Instance.maidWeightBaseValue, 10, PluginConfig.Instance.maidWeightStealPercentageValue); foreach(var i in Assets.scrapItems){ var entry = i.GetItemRarity(); diff --git a/ScarletMansion/ScarletMansion/Plugin.cs b/ScarletMansion/ScarletMansion/Plugin.cs index 712dce1..b80c657 100644 --- a/ScarletMansion/ScarletMansion/Plugin.cs +++ b/ScarletMansion/ScarletMansion/Plugin.cs @@ -21,10 +21,9 @@ namespace ScarletMansion { [BepInPlugin(modGUID, modName, modVersion)] - [BepInDependency("imabatby.lethallevelloader", "1.2.0.1")] + [BepInDependency("imabatby.lethallevelloader", "1.2.0.3")] [BepInDependency("evaisa.lethallib", "0.13.2")] - [BepInDependency(ModCompability.advancedCompanyGuid, BepInDependency.DependencyFlags.SoftDependency)] [BepInDependency(ModCompability.lethalConfigGuid, BepInDependency.DependencyFlags.SoftDependency)] [BepInDependency(ModCompability.facilityMeldownGuid, BepInDependency.DependencyFlags.SoftDependency)] @@ -33,7 +32,7 @@ namespace ScarletMansion { public class Plugin : BaseUnityPlugin { public const string modGUID = "ImoutoSama.ScarletMansion"; private const string modName = "Scarlet Mansion"; - private const string modVersion = "1.3.15"; + private const string modVersion = "1.3.17"; public readonly Harmony harmony = new Harmony(modGUID); @@ -136,7 +135,7 @@ namespace ScarletMansion { NetworkPrefabs.RegisterNetworkPrefab(i.item.spawnPrefab); } - extendedDungeon.dungeonEvents.onBeforeDungeonGenerate.AddListener(GeneratePathPatch.GeneratePatch); + extendedDungeon.DungeonEvents.onBeforeDungeonGenerate.AddListener(GeneratePathPatch.GeneratePatch); DoorwayManager.onMainEntranceTeleportSpawnedEvent.AddEvent("DoorwayCleanup", DoorwayManager.onMainEntranceTeleportSpawnedFunction); } diff --git a/ScarletMansion/ScarletMansion/PluginConfig.cs b/ScarletMansion/ScarletMansion/PluginConfig.cs index 96a4fc4..04d09a5 100644 --- a/ScarletMansion/ScarletMansion/PluginConfig.cs +++ b/ScarletMansion/ScarletMansion/PluginConfig.cs @@ -26,11 +26,15 @@ namespace ScarletMansion { public const string dungeonWeightPrefix = "Dungeon Weight"; public const string dungeonGenerationPrefix = "Dungeon Generation"; public const string dungeonGenerationBoundingBoxPrefix = "DunGen Bounding Box"; + public const string dungeonGenerationMPathsPrefix = "DunGen Main Path"; public const string dungeonGenerationBPathOnePrefix = "DunGen Branching Path 1"; public const string dungeonGenerationBPathTwoPrefix = "DunGen Branching Path 2"; public const string dungeonGenerationBPathThreePrefix = "DunGen Branching Path 3"; - public const string dungeonLootAndEnemiesPrefix = "Dungeon Loot And Enemies"; + + public const string dungeonLootPrefix = "Dungeon Loot"; + public const string dungeonEnemiesPrefix = "Dungeon Enemies"; + public const string dungeonFeaturesPrefix = "Dungeon Features"; public const string dungeonPaintingEventPrefix = "Dungeon Painting Event"; public const string dungeonLightingPrefix = "Lighting"; @@ -214,7 +218,7 @@ namespace ScarletMansion { // loot public static ConfigEntryBundle lootMultiplier = new ConfigEntryBundle( - dungeonLootAndEnemiesPrefix, + dungeonLootPrefix, "Loot Multiplier", 1.4f, "Multiplies the total amount of loot for the dungeon.", @@ -222,26 +226,8 @@ namespace ScarletMansion { new AcceptableValueRange(0.25f, 4f) ); - public static ConfigEntryBundle mapHazardsMultiplier = new ConfigEntryBundle( - dungeonLootAndEnemiesPrefix, - "Map Hazards Multiplier", - 1.6f, - "Multiplies the total amount of map hazards (landmines, turrets) for the dungeon.", - null, - new AcceptableValueRange(0.25f, 4f) - ); - - public static ConfigEntryBundle minIndoorEnemySpawnCount = new ConfigEntryBundle( - dungeonLootAndEnemiesPrefix, - "Minimum Indoor Enemy Spawn Count", - 1, - "Increases the minimum amount of indoor enemies that spawn with each spawn wave. For reference, Eclipse is +3.", - null, - new AcceptableValueRange(0, 3) - ); - public static ConfigEntryBundle crystalWeight = new ConfigEntryBundle( - dungeonLootAndEnemiesPrefix, + dungeonLootPrefix, "Decorative Crystal Weight", 50, "The decorative crystal's spawn weight. Calculating spawn chance (%) is difficult as the total scrap weight for each moon varies from ~600 to ~850.", @@ -250,7 +236,7 @@ namespace ScarletMansion { ); public static ConfigEntryBundle crystalBrokenWeight = new ConfigEntryBundle( - dungeonLootAndEnemiesPrefix, + dungeonLootPrefix, "Shattered Decorative Crystal Weight", 5, "The shattered decorative crystal's spawn weight. Calculating spawn chance (%) is difficult as the total scrap weight for each moon varies from ~600 to ~850.", @@ -258,8 +244,26 @@ namespace ScarletMansion { new AcceptableValueRange(0, 999) ); + public static ConfigEntryBundle mapHazardsMultiplier = new ConfigEntryBundle( + dungeonEnemiesPrefix, + "Map Hazards Multiplier", + 1.6f, + "Multiplies the total amount of map hazards (landmines, turrets) for the dungeon.", + null, + new AcceptableValueRange(0.25f, 4f) + ); + + public static ConfigEntryBundle minIndoorEnemySpawnCount = new ConfigEntryBundle( + dungeonEnemiesPrefix, + "Minimum Indoor Enemy Spawn Count", + 1, + "Increases the minimum amount of indoor enemies that spawn with each spawn wave. For reference, Eclipse is +3.", + null, + new AcceptableValueRange(0, 3) + ); + public static ConfigEntryBundle knightWeightStealPercentage = new ConfigEntryBundle( - dungeonLootAndEnemiesPrefix, + dungeonEnemiesPrefix, "Knight Weight Steal Percentage", 0.75f, "The percentage of spawn weight that the knight steals from the coil-head for that moon.\nSetting this 0 means that the coil-head's weight is unaffected and the knight's weight is based entirely by Knight Weight Base.\nSetting this 1 means the knight effectively replaces the coil-head.\nIf the moon doesn't spawn the coil-head, the knight's base weight is set to 10.", @@ -268,7 +272,7 @@ namespace ScarletMansion { ); public static ConfigEntryBundle knightWeightBase = new ConfigEntryBundle( - dungeonLootAndEnemiesPrefix, + dungeonEnemiesPrefix, "Knight Weight Base", 0, "The base spawn weight of the knight. This is added onto the spawn weight stolen from the coil-head, or the base weight of 10 if the moon doesn't spawn the coil-head.", @@ -276,15 +280,37 @@ namespace ScarletMansion { new AcceptableValueRange(0, 999) ); + public static ConfigEntryBundle maidWeightStealPercentage = new ConfigEntryBundle( + dungeonEnemiesPrefix, + "Maid Weight Steal Percentage", + 0.75f, + "The percentage of spawn weight that the maid steals from the butler for that moon.\nSetting this 0 means that the butler's weight is unaffected and the maid's weight is based entirely by Maid Weight Base.\nSetting this 1 means the maid effectively replaces the butler.\nIf the moon doesn't spawn the bulter, the maid's base weight is set to 10.", + null, + new AcceptableValueRange(0f, 1f) + ); + + public static ConfigEntryBundle maidWeightBase = new ConfigEntryBundle( + dungeonEnemiesPrefix, + "Maid Weight Base", + 0, + "The base spawn weight of the maid. This is added onto the spawn weight stolen from the butler, or the base weight of 10 if the moon doesn't spawn the butler.", + null, + new AcceptableValueRange(0, 999) + ); + public float lootMultiplierValue; public int crystalWeightValue; public int crystalBrokenWeightValue; public float mapHazardsMultiplierValue; + public int minIndoorEnemySpawnCountValue; public float knightWeightStealPercentageValue; public int knightWeightBaseValue; + public float maidWeightStealPercentageValue; + public int maidWeightBaseValue; + // features public static ConfigEntryBundle shovelDamage = new ConfigEntryBundle( diff --git a/ScarletMansion/ScarletMansion/ScarletMansion.csproj b/ScarletMansion/ScarletMansion/ScarletMansion.csproj index c56f2f0..b35c019 100644 --- a/ScarletMansion/ScarletMansion/ScarletMansion.csproj +++ b/ScarletMansion/ScarletMansion/ScarletMansion.csproj @@ -83,6 +83,10 @@ ..\..\..\Libraries\Unity.AI.Navigation.dll + + False + ..\..\..\Libraries\Unity.Animation.Rigging.dll + ..\..\..\Libraries\Unity.Collections.dll @@ -187,6 +191,7 @@ + diff --git a/ScarletMansion/ScarletMansion/Utility.cs b/ScarletMansion/ScarletMansion/Utility.cs index cd88678..6e6339f 100644 --- a/ScarletMansion/ScarletMansion/Utility.cs +++ b/ScarletMansion/ScarletMansion/Utility.cs @@ -179,6 +179,27 @@ namespace ScarletMansion { return null; } + // https://discussions.unity.com/t/how-to-get-a-component-from-an-object-and-add-it-to-another-copy-components-at-runtime/80939/4 + public static T GetCopyOf(this Component comp, T other) where T : Component { + Type type = comp.GetType(); + if (type != other.GetType()) return null; // type mis-match + BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Default | BindingFlags.DeclaredOnly; + PropertyInfo[] pinfos = type.GetProperties(flags); + foreach (var pinfo in pinfos) { + if (pinfo.CanWrite) { + try { + pinfo.SetValue(comp, pinfo.GetValue(other, null), null); + } + catch { } // In case of NotImplementedException being thrown. For some reason specifying that exception didn't seem to catch it, so I didn't catch anything specific. + } + } + FieldInfo[] finfos = type.GetFields(flags); + foreach (var finfo in finfos) { + finfo.SetValue(comp, finfo.GetValue(other)); + } + return comp as T; + } + public static void PrintToParent(Transform t) { Plugin.logger.LogInfo(t.name); var parent = t.parent;