]> git.cameronkatri.com Git - gmtk-gamejam.git/blob - Assets/Scripts/Controllers/GameController.cs
Merge branch 'master' of git.cameronkatri.com:gmtk-gamejam
[gmtk-gamejam.git] / Assets / Scripts / Controllers / GameController.cs
1 using System;
2 using System.Collections;
3 using JetBrains.Annotations;
4 using UnityEngine;
5 using UnityEngine.UI;
6
7 namespace Controllers
8 {
9 /// <summary>
10 /// The controller for the basketball-game logic.
11 /// </summary>
12 public class GameController : MonoBehaviour
13 {
14 private State state = State.JumpBall; // A basketball game always starts with a jump ball.
15 internal Animator BallAnimation;
16
17 public Player player;
18 public Player enemy;
19
20 public bool freezeMotion;
21
22 private float startTime;
23 [SerializeField] private float timeLimit;
24
25 /// <summary>
26 /// The single ball for the game.
27 /// </summary>
28 [SerializeField] public Ball ball;
29
30 [Header("Spawn Points")]
31 [SerializeField] private SpawnPoints PlayerSpawnPoints;
32 [SerializeField] private SpawnPoints EnemySpawnPoints;
33
34 [Header("Hoops")]
35 [SerializeField] public Hoop PlayerHoop;
36 [SerializeField] public Hoop EnemyHoop;
37
38 [Header("SFX")]
39 [SerializeField] public AudioSource dribbleSound;
40
41 [Header("UI")]
42 [SerializeField] private Text playerScoreText;
43 [SerializeField] private Text enemyScoreText;
44 [SerializeField] private Text timerText;
45
46 [SerializeField] private GameObject resultOverlay;
47 [SerializeField] private Text resultText;
48
49 private void Awake()
50 {
51 player = new Player { isEnemy = false, controller = this };
52 enemy = new Player { isEnemy = true, controller = this };
53 ball.controller = this;
54 PlayerHoop.controller = this;
55 EnemyHoop.controller = this;
56 BallAnimation = ball.GetComponentInChildren<Animator>();
57 }
58
59 private void Start()
60 {
61 startTime = Time.time;
62 }
63
64 private void Update()
65 {
66 UpdateUI();
67 }
68
69 private void UpdateUI()
70 {
71 playerScoreText.text = $"{player.score}";
72 enemyScoreText.text = $"{enemy.score}";
73
74 var remainingRaw = timeLimit - (Time.time - startTime);
75 var remaining = TimeSpan.FromSeconds(Mathf.Clamp(remainingRaw, 0, float.MaxValue));
76 timerText.text = $"{remaining.Minutes:00}:{remaining.Seconds:00}";
77
78 if (remainingRaw <= 0)
79 {
80 resultOverlay.SetActive(true);
81 var outcome = player.score == enemy.score ? "TIE GAME" : player.score < enemy.score ? "AWAY TEAM WINS" : "HOME TEAM WINS";
82 resultText.text = $"{outcome}\n{player.score}-{enemy.score}";
83 }
84 }
85
86 public struct Player
87 {
88 /// <summary>
89 /// Whether this player is the AI-enemy.
90 /// </summary>
91 internal bool isEnemy;
92
93 /// <summary>
94 /// A back-reference to the containing GameController.
95 /// </summary>
96 internal GameController controller;
97
98 internal int score;
99
100 private Vector2 lastShotPosition;
101 public void Score(Vector2 Rim)
102 {
103 if (Vector2.Distance(lastShotPosition, Rim) >= 10)
104 {
105 score += 3;
106 }
107 else
108 {
109 score += 2;
110 }
111
112 // They made a shot! Now respawn the players and give possession to the opposite player.
113 controller.Respawn(isEnemy ? Possession.Player : Possession.Enemy);
114 }
115
116 private State dribble => isEnemy ? State.EnemyDribble : State.PlayerDribble;
117 private State shoot => isEnemy ? State.EnemyShoot : State.PlayerShoot;
118
119 private Hoop hoop => isEnemy ? controller.EnemyHoop : controller.PlayerHoop;
120
121 public bool HasBall => controller.state == dribble;
122
123 /// <summary>
124 /// When dribbling, move the ball with the player.
125 /// </summary>
126 /// <param name="handPosition">The position of the hand dribbling the ball.</param>
127 public void Move(Vector2 handPosition)
128 {
129 if (controller.state == (isEnemy ? State.EnemyDribble : State.PlayerDribble)) // Make sure they're dribbling.
130 controller.ball.transform.position = handPosition; // TODO: Make this perform a dribbling motion, otherwise it looks like they're travelling.
131 }
132
133 /// <summary>
134 /// Grab the ball if possible given the current game state.
135 /// </summary>
136 /// <param name="handPosition">The position of the hand to attempt grabbing from.</param>
137 /// <returns>Whether or not the ball was able to be picked up.</returns>
138 public bool GrabBall(Vector2 handPosition)
139 {
140 // Don't allow the ball to be picked up if someone shot it. Also don't try picking it up if we're already holding it.
141 if (controller.state == shoot || controller.state == dribble) return false;
142
143 // Make sure its within their grab area.
144 if (Vector2.Distance(controller.ball.transform.position, handPosition) >= 1f) return false;
145
146 controller.state = dribble;
147 controller.BallAnimation.enabled = false;
148 controller.dribbleSound.Play();
149 Move(handPosition);
150
151 controller.ball.Rigidbody.bodyType = RigidbodyType2D.Kinematic;
152
153 return true;
154 }
155
156 /// <summary>
157 /// Shoot the ball if possible.
158 /// </summary>
159 /// <param name="playerTransform"></param>
160 /// <param name="time"></param>
161 /// <returns>Whether or not the ball was shot</returns>
162 public bool Shoot(Transform playerTransform, float time)
163 {
164 if (controller.state != dribble) return false; // We must be dribbling the ball to shoot it.
165 controller.BallAnimation.enabled = true;
166 controller.dribbleSound.Stop();
167 controller.state = shoot;
168 controller.ball.Rigidbody.bodyType = RigidbodyType2D.Dynamic;
169 controller.ball.Shoot(hoop, time);
170 lastShotPosition = playerTransform.position;
171 return true;
172 }
173 }
174
175 internal void BallDropped()
176 {
177 BallAnimation.enabled = true;
178 dribbleSound.Stop();
179 ball.Rigidbody.bodyType = RigidbodyType2D.Dynamic;
180 state = State.Idle;
181 }
182
183 private void Respawn(Possession possession)
184 {
185 PlayerSpawnPoints.body.transform.position = new Vector3(PlayerSpawnPoints.character.position.x, PlayerSpawnPoints.character.position.y, PlayerSpawnPoints.body.transform.position.y);
186 PlayerSpawnPoints.body.GetComponent<Rigidbody2D>().velocity = Vector2.zero;
187
188 if (PlayerSpawnPoints.secondBody is { })
189 {
190 if (PlayerSpawnPoints.secondCharacter is { })
191 PlayerSpawnPoints.secondBody.transform.position = new Vector3(PlayerSpawnPoints.secondCharacter.position.x, PlayerSpawnPoints.secondCharacter.position.y, PlayerSpawnPoints.body.transform.position.y);
192 PlayerSpawnPoints.secondBody.GetComponent<Rigidbody2D>().velocity = Vector2.zero;
193 }
194
195 PlayerSpawnPoints.body.transform.localRotation = Quaternion.identity;
196 EnemySpawnPoints.body.transform.position = new Vector3(EnemySpawnPoints.character.position.x, EnemySpawnPoints.character.position.y, EnemySpawnPoints.body.transform.position.y);
197 ball.transform.position = possession switch
198 {
199 Possession.Player => new Vector3(PlayerSpawnPoints.ball.position.x, PlayerSpawnPoints.ball.position.y, ball.transform.position.y),
200 Possession.Enemy => new Vector3(EnemySpawnPoints.ball.position.x, EnemySpawnPoints.ball.position.y, ball.transform.position.y),
201 _ => ball.transform.position
202 };
203
204 // Set a cooldown so they can stop trying to wrangle the player while it respawns.
205 StartCoroutine(RespawnCooldown(possession));
206 }
207
208 private IEnumerator RespawnCooldown(Possession possession)
209 {
210 // Show the new score.
211 var possessionText = possession == Possession.Player ? "HOME" : "AWAY";
212 ShowModal($"{player.score}-{enemy.score}\n{possessionText}'S POSSESSION");
213
214 freezeMotion = true;
215
216 yield return new WaitForSeconds(1f);
217
218 HideModal();
219
220 freezeMotion = false;
221 }
222
223 private void ShowModal(string text)
224 {
225 resultOverlay.SetActive(true);
226 resultText.text = text;
227 }
228
229 private void HideModal() => resultOverlay.SetActive(false);
230
231 internal enum State
232 {
233 Idle,
234 JumpBall,
235 PlayerDribble,
236 PlayerShoot,
237 EnemyDribble,
238 EnemyShoot,
239 }
240
241 [Serializable]
242 private struct SpawnPoints
243 {
244 [SerializeField] internal Transform body;
245 [SerializeField] [CanBeNull] internal Transform secondBody;
246 [SerializeField] internal Transform ball;
247 [SerializeField] internal Transform character;
248 [SerializeField] [CanBeNull] internal Transform secondCharacter;
249 }
250
251 private enum Possession
252 {
253 Player,
254 Enemy
255 }
256 }
257
258 internal static class GameControllerStateExtensions
259 {
260 internal static bool IsShot(this GameController.State state)
261 {
262 return state == GameController.State.EnemyShoot || state == GameController.State.PlayerShoot;
263 }
264
265 internal static bool IsDribble(this GameController.State state)
266 {
267 return state == GameController.State.EnemyDribble || state == GameController.State.PlayerDribble;
268 }
269 }
270 }