]> git.cameronkatri.com Git - gmtk-gamejam.git/blobdiff - Assets/Scripts/Controllers/GameController.cs
Add shot indicator
[gmtk-gamejam.git] / Assets / Scripts / Controllers / GameController.cs
index de3b00c5479281f2570d782190d821062a657645..c9cb0ce53f9efac920661acc2ea20509dfb27300 100644 (file)
@@ -1,5 +1,9 @@
 using System;
+using System.Collections;
+using JetBrains.Annotations;
 using UnityEngine;
+using UnityEngine.SceneManagement;
+using UnityEngine.UI;
 
 namespace Controllers
 {
@@ -9,19 +13,47 @@ namespace Controllers
   public class GameController : MonoBehaviour
   {
     private State state = State.JumpBall; // A basketball game always starts with a jump ball.
+    internal Animator BallAnimation;
 
     public Player player;
     public Player enemy;
     
+    public bool freezeMotion;
+
+    private float startTime;
+    public static float timeLimit;
+    
     /// <summary>
     /// The single ball for the game.
     /// </summary>
     [SerializeField] public Ball ball;
 
+    [SerializeField] private float dribbleHeight;
+    [SerializeField] private float dribbleSpeed;
+    private Vector3 ballTarget;
+    
+    [SerializeField] public SpriteRenderer[] trenchCoatSegments;
+
+    [Header("Spawn Points")]
+    [SerializeField] private SpawnPoints PlayerSpawnPoints;
+    [SerializeField] private SpawnPoints EnemySpawnPoints;
+
+    [Header("Hoops")]
     [SerializeField] public Hoop PlayerHoop;
     [SerializeField] public Hoop EnemyHoop;
 
+    [Header("SFX")]
     [SerializeField] public AudioSource dribbleSound;
+    [SerializeField] public AudioSource airhornSound;
+
+    [Header("UI")]
+    [SerializeField] private Text playerScoreText;
+    [SerializeField] private Text enemyScoreText;
+    [SerializeField] private Text timerText;
+    
+    [SerializeField] private GameObject resultOverlay;
+    [SerializeField] private Text resultText;
+    [SerializeField] private GameObject actionsUI;
 
     private void Awake()
     {
@@ -30,8 +62,72 @@ namespace Controllers
       ball.controller = this;
       PlayerHoop.controller = this;
       EnemyHoop.controller = this;
+      BallAnimation = ball.GetComponentInChildren<Animator>();
     }
-    
+
+    private void Start()
+    {
+      startTime = Time.time - 2f;
+      freezeMotion = true;
+      StartCoroutine(FadeInCoat());
+    }
+
+    private IEnumerator FadeInCoat()
+    {
+      foreach (var trenchCoat in trenchCoatSegments)
+        trenchCoat.material.color = new Color(1, 1, 1, 0);
+      
+      yield return new WaitForSeconds(1f);
+      
+      for (float t = 0; t < 1f; t += Time.deltaTime / 1)
+      {
+        foreach (var trenchCoat in trenchCoatSegments)
+          trenchCoat.material.color = new Color(1, 1, 1, Mathf.Lerp(0, 1, t));
+        yield return null;
+      }
+
+      yield return new WaitForSeconds(1f);
+
+      freezeMotion = false;
+      startTime = Time.time;
+      ball.Rigidbody.velocity = new Vector2(0, 10f);
+    }
+
+    private void Update()
+    {
+      UpdateUI();
+    }
+
+    private void FixedUpdate()
+    {
+      if (player.HasBall || enemy.HasBall)
+      {
+        ball.transform.position = ballTarget - new Vector3(0, (Mathf.Sin(Time.time * dribbleSpeed) + 1f) * (ballTarget.y - 0.75f) * dribbleHeight, 0);
+      }
+    }
+
+    private bool gameover;
+    private void UpdateUI()
+    {
+      playerScoreText.text = $"{player.score}";
+      enemyScoreText.text = $"{enemy.score}";
+
+      var remainingRaw = timeLimit - (Time.time - startTime);
+      var remaining = TimeSpan.FromSeconds(Mathf.Clamp(remainingRaw, 0, float.MaxValue));
+      timerText.text = $"{remaining.Minutes:00}:{remaining.Seconds:00}";
+
+      if (remainingRaw <= 0 && !gameover)
+      {
+        airhornSound.Play();
+        var outcome = player.score == enemy.score ? "TIE GAME" : player.score < enemy.score ? "AWAY TEAM WINS" : "HOME TEAM WINS";
+        actionsUI.SetActive(true);
+        ShowModal($"{outcome}\n{player.score}-{enemy.score}");
+
+        freezeMotion = true;
+        gameover = true;
+      }
+    }
+
     public struct Player
     {
       /// <summary>
@@ -57,6 +153,9 @@ namespace Controllers
         {
           score += 2;
         }
+        
+        // They made a shot! Now respawn the players and give possession to the opposite player.
+        controller.Respawn(isEnemy ? Possession.Player : Possession.Enemy, $"{controller.player.score}-{controller.enemy.score}");
       }
 
       private State dribble => isEnemy ? State.EnemyDribble : State.PlayerDribble;
@@ -73,7 +172,7 @@ namespace Controllers
       public void Move(Vector2 handPosition)
       {
         if (controller.state == (isEnemy ? State.EnemyDribble : State.PlayerDribble)) // Make sure they're dribbling.
-          controller.ball.transform.position = handPosition; // TODO: Make this perform a dribbling motion, otherwise it looks like they're travelling.
+          controller.ballTarget = handPosition;
       }
 
       /// <summary>
@@ -90,8 +189,12 @@ namespace Controllers
         if (Vector2.Distance(controller.ball.transform.position, handPosition) >= 1f) return false;
         
         controller.state = dribble;
+        controller.BallAnimation.enabled = false;
         controller.dribbleSound.Play();
         Move(handPosition);
+
+        controller.ball.Rigidbody.bodyType = RigidbodyType2D.Kinematic;
+        
         return true;
       }
 
@@ -99,24 +202,94 @@ namespace Controllers
       /// Shoot the ball if possible.
       /// </summary>
       /// <param name="playerTransform"></param>
+      /// <param name="time"></param>
       /// <returns>Whether or not the ball was shot</returns>
-      public bool Shoot(Transform playerTransform)
+      public bool Shoot(Transform playerTransform, float time)
       {
         if (controller.state != dribble) return false; // We must be dribbling the ball to shoot it.
+        controller.BallAnimation.enabled = true;
         controller.dribbleSound.Stop();
         controller.state = shoot;
-        controller.ball.Shoot(hoop.transform.position);
+        controller.ball.Rigidbody.bodyType = RigidbodyType2D.Dynamic;
+        controller.ball.Shoot(hoop, time);
         lastShotPosition = playerTransform.position;
         return true;
       }
+
+      public void Foul(string reason)
+      {
+        // Give the other player the ball on a foul.
+        controller.Respawn(isEnemy ? Possession.Player : Possession.Enemy, reason);
+      }
     }
 
     internal void BallDropped()
     {
+      BallAnimation.enabled = true;
       dribbleSound.Stop();
+      ball.Rigidbody.bodyType = RigidbodyType2D.Dynamic;
       state = State.Idle;
     }
 
+    private void Respawn(Possession possession, string message)
+    {
+      BallDropped();
+      
+      PlayerSpawnPoints.body.transform.position = new Vector3(PlayerSpawnPoints.character.position.x, PlayerSpawnPoints.character.position.y, PlayerSpawnPoints.body.transform.position.y);
+      PlayerSpawnPoints.body.GetComponent<Rigidbody2D>().velocity = Vector2.zero;
+
+      if (PlayerSpawnPoints.secondBody is { })
+      {
+        if (PlayerSpawnPoints.secondCharacter is { })
+          PlayerSpawnPoints.secondBody.transform.position = new Vector3(PlayerSpawnPoints.secondCharacter.position.x, PlayerSpawnPoints.secondCharacter.position.y, PlayerSpawnPoints.body.transform.position.y);
+        PlayerSpawnPoints.secondBody.GetComponent<Rigidbody2D>().velocity = Vector2.zero;
+      }
+
+      PlayerSpawnPoints.body.transform.localRotation = Quaternion.identity;
+      EnemySpawnPoints.body.transform.position = new Vector3(EnemySpawnPoints.character.position.x, EnemySpawnPoints.character.position.y, EnemySpawnPoints.body.transform.position.y);
+      ball.transform.position = possession switch
+      {
+        Possession.Player => new Vector3(PlayerSpawnPoints.ball.position.x, PlayerSpawnPoints.ball.position.y, ball.transform.position.y),
+        Possession.Enemy => new Vector3(EnemySpawnPoints.ball.position.x, EnemySpawnPoints.ball.position.y, ball.transform.position.y),
+        _ => ball.transform.position
+      };
+      
+      // Set a cooldown so they can stop trying to wrangle the player while it respawns.
+      StartCoroutine(RespawnCooldown(possession, message));
+    }
+
+    private IEnumerator RespawnCooldown(Possession possession, string message)
+    {
+      // Show the new score.
+      var possessionText = possession == Possession.Player ? "HOME" : "AWAY";
+      ShowModal($"{message}\n{possessionText}'S POSSESSION");
+
+      freezeMotion = true;
+      
+      yield return new WaitForSeconds(1f);
+      
+      HideModal();
+      
+      freezeMotion = false;
+    }
+
+    private void ShowModal(string text)
+    {
+      if (gameover) return;
+      resultOverlay.SetActive(true);
+      resultText.text = text;
+    }
+
+    private void HideModal()
+    {
+      if (gameover) return;
+      resultOverlay.SetActive(false);
+    }
+
+    public void Restart() => SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
+
+    public void MainMenu() => SceneManager.LoadScene("Menu");
+
     internal enum State
     {
       Idle,
@@ -126,6 +299,22 @@ namespace Controllers
       EnemyDribble,
       EnemyShoot,
     }
+
+    [Serializable]
+    private struct SpawnPoints
+    {
+      [SerializeField] internal Transform body;
+      [SerializeField] [CanBeNull] internal Transform secondBody;
+      [SerializeField] internal Transform ball;
+      [SerializeField] internal Transform character;
+      [SerializeField] [CanBeNull] internal Transform secondCharacter;
+    }
+
+    private enum Possession
+    {
+      Player,
+      Enemy
+    }
   }
 
   internal static class GameControllerStateExtensions