using NUnit.Framework; using System.Collections; using System.Collections.Generic; using System.Linq; using TMPro; using UnityEngine; using UnityEngine.AI; using UnityEngine.Diagnostics; using UnityEngine.SceneManagement; public class TinkyWinky : MonoBehaviour { // singleton public static TinkyWinky Instance; // const private const float offscreenDotRange = 0.7f; private const float minDistanceForChase = 20f; private const float minGazeTimeForChase = 2f; private const float minIgnoredNearbyTimeForChase = 15f; private const float minGazeTimeForDeath = 6f; private const float minDistanceForStalk = 100f; private const float maxDistanceForStalk = 200f; private const float keptDistanceForStalk = 30f; private const float loseChaseDuration = 45f; private const float loseChaseDistance = 50f; private const float pathRefreshInterval = 0.25f; // state public enum States { None, Idle, Wander, Stalk, Chase } public States ActiveState { get; private set; } = States.None; public States NextState { get; private set; } = States.None; [Header("Setup")] public NavMeshAgent Agent; public LayerMask LayerMask; public Vector3 RaycastOffset; [Header("Stats")] public float WalkSpeed = 3; public float RunSpeed = 7; public float KillRange = 1f; public Vector3 KillOffset; [Header("the SCARY stuff")] public int SprintOnlyCustardThreshold = 2; [Header("Visuals")] public Transform Model; public MeshRenderer MeshRenderer; public Texture2D ChaseFace; public Texture2D NormalFace; [Header("Audio")] public AudioSource FootstepSource; public AudioSource EhOhSource; public AudioSource ScreamSource; public float MaxFootstepVolume = 0.3f; // behaviour stuff int custardCount => (CollectGamemode.Instance) ? CollectGamemode.Instance.CustardAmount - CollectGamemode.Instance.CollectedCustard : 99; List custards => CollectGamemode.Instance.Custards ?? null; GameObject[] fallbackSpots; float speed; Vector3? targetPosition; [HideInInspector] public Vector3? recentlyCollectedCustard; float nextStalkRefresh; float nextChaseRefresh; float nextTargetInvalidation; float nextTeleport; float chaseDuration; float gazeDurationInStalk; float ignoredNearbyDurationInStalk; float gazeDurationInChase; float timeSinceTooDistant; bool chargeTowardPlayerInWander; bool angeredInStalk; bool saidEhOh; bool nightmare; bool isOffscreen; private void Awake() { Instance = this; } private void Start() { if (DynamicTime.Instance) nightmare = DynamicTime.Instance.ActiveTOD == DynamicTime.TimeOfDay.Nightmare; if (nightmare) { WalkSpeed = RunSpeed / 1.2f; MaxFootstepVolume /= 2; } fallbackSpots = GameObject.FindGameObjectsWithTag("Fallback Spot"); StartCoroutine(StateUpdate()); } private void LateUpdate() { isOffscreen = CheckIfOffScreen(); } private void Update() { if (Player.Instance) { Model.LookAt(Player.Instance.Camera.transform.position); Model.transform.eulerAngles = new Vector3(-90, Model.transform.eulerAngles.y, Model.transform.eulerAngles.z); if (Vector3.Distance(this.transform.position + KillOffset, Player.Instance.transform.position) < KillRange) Player.Instance.Kill(); } if (Agent.desiredVelocity.sqrMagnitude > 0.1f) FootstepSource.volume = Mathf.Lerp(FootstepSource.volume, 0.3f, Time.deltaTime * 20f); else FootstepSource.volume = Mathf.Lerp(FootstepSource.volume, 0f, Time.deltaTime * 20f); Agent.speed = speed; } IEnumerator StateUpdate() { while (true) { switch (ActiveState) { case States.None: case States.Idle: RequestState(States.Wander); break; case States.Wander: yield return WanderUpdate(); break; case States.Stalk: yield return StalkUpdate(); break; case States.Chase: yield return ChaseUpdate(); break; } if (NextState == ActiveState) NextState = States.None; if (NextState != States.None) { targetPosition = null; Agent.ResetPath(); chaseDuration = 0; gazeDurationInStalk = 0; gazeDurationInChase = 0; ignoredNearbyDurationInStalk = 0; angeredInStalk = false; chargeTowardPlayerInWander = false; timeSinceTooDistant = 0; ActiveState = NextState; NextState = States.None; if (ActiveState == States.Chase) { MeshRenderer.materials[0].SetTexture("_BaseMap", ChaseFace); ScreamSource.Play(); this.transform.position = GetChaseTeleportPosition(); } else { MeshRenderer.materials[0].SetTexture("_BaseMap", NormalFace); } } yield return new WaitForEndOfFrame(); } } IEnumerator ChaseUpdate() { if (!Player.Instance) { RequestState(States.Idle); yield break; } float tpChance = 0; // used to be rng too if (nightmare) tpChance = (isOffscreen) ? 80 : 90; else tpChance = (isOffscreen) ? 50 : 80; speed = (isOffscreen) ? RunSpeed/1.2f : RunSpeed*3; if (!isOffscreen) NearbyVignette.Show(); if (Time.time > nextChaseRefresh) { if (gazeDurationInChase < 3 && Random.Range(0, 100) < tpChance && Vector3.Distance(this.transform.position + KillOffset, Player.Instance.transform.position) > KillRange * 2 && Time.time > nextTeleport) { Agent.ResetPath(); this.transform.position = GetChaseTeleportPosition(); nextTeleport = Time.time + Random.Range(1f,2f); } Agent.SetDestination(GetNavPosition(Player.Instance.transform.position)); nextChaseRefresh = Time.time + pathRefreshInterval; } if (!isOffscreen) gazeDurationInChase = Mathf.Clamp(gazeDurationInChase + Time.deltaTime, 0, 10f); else gazeDurationInChase = Mathf.Clamp(gazeDurationInChase - Time.deltaTime * 2f, 0, 10f); chaseDuration += (isOffscreen) ? Time.deltaTime * 2 : Time.deltaTime; if (gazeDurationInChase > minGazeTimeForDeath) Player.Instance.Kill(); if (chaseDuration >= loseChaseDuration || Vector3.Distance(this.transform.position, Player.Instance.transform.position) > loseChaseDistance) { this.transform.position = fallbackSpots[Random.Range(0, fallbackSpots.Length)].transform.position; RequestState(States.Idle); } } IEnumerator StalkUpdate() { if (!Player.Instance) yield break; if (angeredInStalk || nightmare) { speed = (!nightmare) ? RunSpeed * 2 : RunSpeed * 3; if (Time.time > nextChaseRefresh) { Agent.SetDestination(GetNavPosition(Player.Instance.transform.position)); nextChaseRefresh = Time.time + pathRefreshInterval; } if (PlayerDistance() <= minDistanceForChase / 1.5f) // replace back to 1.75 if range too big RequestState(States.Chase); yield break; } if (PlayerDistance() > minDistanceForStalk/2) { speed = RunSpeed * 4; if (Time.time > nextChaseRefresh) { Agent.SetDestination(GetNavPosition(Player.Instance.transform.position - Player.Instance.transform.forward * keptDistanceForStalk)); nextChaseRefresh = Time.time + pathRefreshInterval; } yield break; } if (PlayerDistance() >= maxDistanceForStalk) { RequestState(States.Wander); yield break; } speed = WalkSpeed * 1.5f; if ((!targetPosition.HasValue || ReachedDestination()) && Time.time > nextStalkRefresh) { targetPosition = GetStalkPosition(); Agent.SetDestination(targetPosition.Value); nextStalkRefresh = Time.time + Random.Range(5,10); } if (!isOffscreen) { float multi = 1f; // pure evil if (PlayerDistance() <= keptDistanceForStalk) multi += 1.5f; if (PlayerDistance() <= keptDistanceForStalk/2) multi += 1.5f; gazeDurationInStalk = Mathf.Clamp(gazeDurationInStalk + Time.deltaTime * multi, 0, minGazeTimeForChase); ignoredNearbyDurationInStalk = Mathf.Clamp(ignoredNearbyDurationInStalk - Time.deltaTime * 2, 0, ignoredNearbyDurationInStalk); if (gazeDurationInStalk > 0.5f && !saidEhOh) { saidEhOh = true; EhOhSource.Play(); } Agent.isStopped = true; } else { gazeDurationInStalk = Mathf.Clamp(gazeDurationInStalk - Time.deltaTime / 2f, 0, minGazeTimeForChase); if (PlayerDistance() <= keptDistanceForStalk * 1.2f) ignoredNearbyDurationInStalk = Mathf.Clamp(ignoredNearbyDurationInStalk + Time.deltaTime, 0, ignoredNearbyDurationInStalk); Agent.isStopped = false; } if ((gazeDurationInStalk >= minGazeTimeForChase || ignoredNearbyDurationInStalk >= minIgnoredNearbyTimeForChase) && PlayerDistance() <= minDistanceForChase * 2) angeredInStalk = true; if (PlayerDistance() <= minDistanceForChase / 1.75f) RequestState(States.Chase); yield break; } IEnumerator WanderUpdate() { if (nightmare && Player.Instance || chargeTowardPlayerInWander) { speed = RunSpeed * 4; if (Time.time > nextChaseRefresh) { Agent.SetDestination(GetNavPosition(Player.Instance.transform.position)); nextChaseRefresh = Time.time + pathRefreshInterval; } if (NextState < States.Stalk) { if (isOffscreen && PlayerDistance() <= minDistanceForStalk && !chargeTowardPlayerInWander) RequestState(States.Stalk); if (!isOffscreen && PlayerDistance() <= minDistanceForChase) RequestState(States.Chase); if (PlayerDistance() <= minDistanceForChase / 1.25f) RequestState(States.Chase); } yield break; } if (Player.Instance) { // "behind u" logic stuff if (PlayerDistance() <= minDistanceForStalk) timeSinceTooDistant = 0; if (PlayerDistance() > minDistanceForStalk) timeSinceTooDistant += Time.deltaTime; if (PlayerDistance() > minDistanceForStalk * 2) timeSinceTooDistant += Time.deltaTime; if (PlayerDistance() > keptDistanceForStalk * 1.1f && custardCount <= 4) timeSinceTooDistant += Time.deltaTime; if (timeSinceTooDistant > 15) { Agent.ResetPath(); timeSinceTooDistant = 0; Vector3 behind = GetNavPosition(Player.Instance.Camera.transform.position - Player.Instance.Camera.transform.forward * minDistanceForStalk / 1.25f); Vector3 front = GetNavPosition(Player.Instance.Camera.transform.position + Player.Instance.Camera.transform.forward * minDistanceForStalk / 1.25f); if (Random.Range(0, 100) < 50 && custardCount <= 2) { // tinky casually rushing towards you from the fog in front of you if (Random.Range(0, 100) < 60) chargeTowardPlayerInWander = true; this.transform.position = front; } else { this.transform.position = behind; } yield break; } } if (custards != null) speed = (custards.Count <= SprintOnlyCustardThreshold) ? RunSpeed : WalkSpeed; else speed = WalkSpeed; if (Random.Range(0, 1000) < 5 && Time.time > nextTargetInvalidation) { nextTargetInvalidation = Time.time + Random.Range(10, 40); targetPosition = null; } if (recentlyCollectedCustard == null) { if (!targetPosition.HasValue || ReachedDestination()) { if (Random.Range(0, 100) < 10 && custards != null) { var custard = custards[Random.Range(0, custards.Count)]; targetPosition = GetNavPosition(custard.transform.position); } else { targetPosition = GetNavPosition(this.transform.position + (Random.insideUnitSphere * 40)); } if (Random.Range(0, 100) < 20 || Random.Range(0, 100) < 70 && custardCount < SprintOnlyCustardThreshold) targetPosition = GetNavPosition(Player.Instance.transform.position + (Random.insideUnitSphere * 15)); Agent.SetDestination(targetPosition.Value); } } else { targetPosition = recentlyCollectedCustard; Agent.SetDestination(targetPosition.Value); recentlyCollectedCustard = null; } if (NextState < States.Stalk) { if (isOffscreen && PlayerDistance() <= minDistanceForStalk) RequestState(States.Stalk); if (!isOffscreen && PlayerDistance() <= minDistanceForChase) RequestState(States.Chase); if (PlayerDistance() <= minDistanceForChase / 1.25f) RequestState(States.Chase); } yield break; } public void RequestState(States state) { NextState = state; } public float PlayerDistance() { if (!Player.Instance) return 0; return Vector3.Distance(this.transform.position, Player.Instance.transform.position); } public bool CheckIfOffScreen(bool blockedCheck = false) // use isOffscreen instead of this { if (!Player.Instance) return true; Vector3 normalized = Player.Instance.Camera.transform.forward.normalized; Vector3 normalized2 = (Player.Instance.Camera.transform.position - (this.transform.position + RaycastOffset)).normalized; float num = Vector3.Dot(normalized, normalized2); bool blocked = true; RaycastHit hit; if (Physics.Raycast(this.transform.position + RaycastOffset, normalized2, out hit, 1000f, LayerMask)) if (hit.transform.CompareTag("Player")) blocked = false; if (num < offscreenDotRange && (!blocked || !blockedCheck)) { return true; } else { return false; } } private void OnDrawGizmos() { Gizmos.color = Color.green; if (targetPosition != null) Gizmos.DrawLine(this.transform.position, targetPosition.Value); Gizmos.color = Color.pink; if (recentlyCollectedCustard != null) Gizmos.DrawLine(this.transform.position, recentlyCollectedCustard.Value); Gizmos.color = new Color(1f, 0.5f, 0f, 0.5f); Gizmos.DrawWireSphere(this.transform.position, minDistanceForStalk); Gizmos.color = new Color(1f, 0.25f, 0f, 0.25f); Gizmos.DrawWireSphere(this.transform.position, keptDistanceForStalk); Gizmos.color = new Color(1f, 0f, 0f, 0.1f); Gizmos.DrawWireSphere(this.transform.position, minDistanceForChase); Gizmos.color = new Color(1f, 0f, 0f, 0.25f); Gizmos.DrawWireSphere(this.transform.position, minDistanceForChase / 1.25f); Gizmos.color = new Color(1f, 0f, 0f, 1f); Gizmos.DrawWireSphere(this.transform.position, minDistanceForChase / 1.75f); Gizmos.color = Color.red; Gizmos.DrawLine(this.transform.position + RaycastOffset, this.transform.position + RaycastOffset + this.transform.forward * 10f); Gizmos.color = Color.lightPink; Gizmos.DrawWireSphere(this.transform.position + KillOffset, KillRange); Gizmos.color = Color.white; } private bool ReachedDestination() { if (Agent.pathPending) return false; if (Agent.remainingDistance > Agent.stoppingDistance + 0.25f) return false; return !Agent.hasPath || Agent.velocity.sqrMagnitude < 0.01f; } private Vector3 GetNavPosition(Vector3 pos) { NavMeshHit hit; if (NavMesh.SamplePosition(pos, out hit, 5f, NavMesh.AllAreas)) return hit.position; return Agent.transform.position; } private Vector3 GetStalkPosition() { float back = Random.Range(keptDistanceForStalk / 2f, keptDistanceForStalk); float side = Random.Range(-keptDistanceForStalk / 4f, keptDistanceForStalk / 4f); Vector3 desired = Player.Instance.transform.position - (Player.Instance.transform.forward * back) + (Player.Instance.transform.right * side); NavMeshHit hit; if (NavMesh.SamplePosition(desired, out hit, 5f, NavMesh.AllAreas)) return hit.position; return Agent.transform.position; } private Vector3 GetChaseTeleportPosition() { float side = Random.Range(-keptDistanceForStalk / 5f, keptDistanceForStalk / 5f); float dist = Vector3.Distance(this.transform.position, Player.Instance.transform.position); Vector3 desired = Player.Instance.transform.position + (Player.Instance.transform.forward * dist) + (Player.Instance.transform.right * side); NavMeshHit hit; if (NavMesh.SamplePosition(desired, out hit, 5f, NavMesh.AllAreas)) return hit.position; return Agent.transform.position; } }