Add initial game systems and input handling for player interactions

This commit is contained in:
2025-12-09 22:20:38 +01:00
commit 5e0db113aa
182 changed files with 70557 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
using System;
using Abstractions;
using UnityEngine;
namespace Presentation
{
public class ElfController : MonoBehaviour
{
[Header("Movement")] [SerializeField] private float moveSpeed = 10f;
[Header("Cane mechanics")] [SerializeField]
private Transform canePivot;
[SerializeField] private float maxTiltAngle = 30f;
[SerializeField] private float tiltSpeed = 15f;
private IInputService _inputService;
public void Configure(IInputService inputService)
{
_inputService = inputService;
}
private void Update()
{
if (_inputService == null) return;
var move = _inputService.GetHorizontalMovement();
transform.Translate(Vector3.right * (move * moveSpeed * Time.deltaTime));
HandleCaneRotation(move);
}
private void HandleCaneRotation(float inputDirection)
{
var targetAngle = 0f;
if (inputDirection > 0.1f)
{
targetAngle = -maxTiltAngle;
}
else if (inputDirection < -0.1f)
{
targetAngle = maxTiltAngle;
}
else
{
targetAngle = 0f;
}
var targetRotation = Quaternion.Euler(0f, 0f, targetAngle);
canePivot.localRotation = Quaternion.Lerp(
canePivot.localRotation,
targetRotation,
Time.deltaTime * tiltSpeed
);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ef7ae4dbcdba422dbfcf1c7ed761a311
timeCreated: 1765307632

View File

@@ -0,0 +1,88 @@
using System;
using Abstractions;
using Core;
using Core.Systems;
using Infrastructure;
using UnityEngine;
namespace Presentation
{
public class GameBootstrap : MonoBehaviour
{
private ScoreSystem _scoreSystem;
private LivesSystem _livesSystem;
private TimeAttackSystem _timeAttackSystem;
private PersistenceSystem _persistenceSystem;
private IDisposable _p1Input;
private IDisposable _p2Input;
[Header("Game Settings")]
[SerializeField] private int startValue= 3;
[SerializeField] private bool twoPlayerMode = false;
[SerializeField] private GameMode mode = GameMode.Arcade;
[Header("References")]
[SerializeField] private ElfController elfPrefab;
[SerializeField] private Transform spawnP1;
[SerializeField] private Transform spawnP2;
[SerializeField] private PresentSpawner spawner;
private void Start()
{
_scoreSystem = new ScoreSystem();
_livesSystem = new LivesSystem(startValue);
var saveKey = mode == GameMode.Arcade ? "HS_Arcade" : "HS_TimeAttack";
_persistenceSystem = new PersistenceSystem(new PlayerPrefsPersistence(), saveKey);
switch (mode)
{
case GameMode.Arcade:
_livesSystem = new LivesSystem(startValue);
break;
case GameMode.TimeAttack:
_timeAttackSystem = new TimeAttackSystem(startValue);
break;
}
_p1Input = new PlayerOneInput();
_p2Input = new PlayerTwoInput();
SpawnElf(_p1Input as IInputService, spawnP1.position, Color.white);
if (twoPlayerMode)
{
SpawnElf(_p2Input as IInputService, spawnP2.position, Color.green);
}
spawner.StartSpawning();
}
private void Update()
{
if (mode == GameMode.TimeAttack && _timeAttackSystem != null)
{
_timeAttackSystem.Tick(Time.deltaTime);
}
}
private void SpawnElf(IInputService input, Vector3 position, Color tint)
{
var elf = Instantiate(elfPrefab, position, Quaternion.identity);
elf.Configure(input);
elf.GetComponentInChildren<SpriteRenderer>().color = tint;
}
private void OnDestroy()
{
_scoreSystem?.Dispose();
_livesSystem?.Dispose();
_timeAttackSystem?.Dispose();
_persistenceSystem?.Dispose();
_p1Input?.Dispose();
_p2Input?.Dispose();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e824a9a954a6401395ad66ebb282accc
timeCreated: 1765307449

View File

@@ -0,0 +1,68 @@
using Core;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Presentation
{
public class GameHud : MonoBehaviour
{
[Header("Text References")]
[SerializeField] private TextMeshProUGUI scoreText;
[SerializeField] private TextMeshProUGUI livesOrTimeText;
[SerializeField] private TextMeshProUGUI highScoreText;
[Header("Panels")]
[SerializeField] private GameObject gameOverPanel;
private void Awake()
{
if (gameOverPanel) gameOverPanel.SetActive(false);
}
private void OnEnable()
{
GameEvents.ScoreUpdated += UpdateScore;
GameEvents.LivesUpdated += UpdateLives;
GameEvents.TimeUpdated += UpdateTime;
GameEvents.GameOver += ShowGameOver;
}
private void OnDisable()
{
GameEvents.ScoreUpdated -= UpdateScore;
GameEvents.LivesUpdated -= UpdateLives;
GameEvents.TimeUpdated -= UpdateTime;
GameEvents.GameOver -= ShowGameOver;
}
private void UpdateScore(int score)
{
if(scoreText) scoreText.text = $"Score: {score}";
}
private void UpdateLives(int lives)
{
if(livesOrTimeText) livesOrTimeText.text = $"Lives: {lives}";
}
private void UpdateTime(float time)
{
if(livesOrTimeText) livesOrTimeText.text = $"Time: {time:F0}";
if (time <= 10f) livesOrTimeText.color = Color.red;
else livesOrTimeText.color = Color.white;
}
private void ShowGameOver()
{
if(gameOverPanel) gameOverPanel.SetActive(true);
}
public void RestartGame()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 420df631eed549fe9ab3dcddacb210c0
timeCreated: 1765314012

View File

@@ -0,0 +1,33 @@
using System;
using Core;
using UnityEngine;
namespace Presentation
{
public class Present : MonoBehaviour
{
[SerializeField] private int points = 1;
private PresentSpawner _spawner;
public void Configure(PresentSpawner spawner)
{
_spawner = spawner;
}
private void OnCollisionEnter2D(Collision2D other)
{
if (other.gameObject.CompareTag("Ground"))
{
GameEvents.ReportPresentDropped();
_spawner.ReturnToPool(this);
}
if (other.gameObject.CompareTag("Sleigh"))
{
GameEvents.ReportPresentCaught(points);
_spawner.ReturnToPool(this);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a93e3dd5e12b468089cd95e7566ece1d
timeCreated: 1765307547

View File

@@ -0,0 +1,112 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Core;
using UnityEngine;
using Random = UnityEngine.Random;
namespace Presentation
{
public class PresentSpawner : MonoBehaviour
{
[Header("Pooling")]
[SerializeField] private Present presentPrefab;
[SerializeField] private int poolSize = 20;
[Header("Spawning")]
[SerializeField] private Transform[] spawnPoints;
[SerializeField] private float spawnInterval = 1.5f;
[SerializeField] private float burstChance = 0.3f;
[SerializeField] private Vector2 initialDirection = Vector2.right;
private Queue<Present> _pool = new();
private List<Present> _activePresents = new();
private bool _isSpawning;
private void Awake()
{
for (var i = 0; i < poolSize; i++)
{
CreateNewPresent();
}
}
private Present CreateNewPresent()
{
var p = Instantiate(presentPrefab, transform);
p.gameObject.SetActive(false);
p.Configure(this);
_pool.Enqueue(p);
return p;
}
private Present GetPresent()
{
if (_pool.Count == 0) CreateNewPresent();
var p = _pool.Dequeue();
p.gameObject.SetActive(true);
_activePresents.Add(p);
return p;
}
public void ReturnToPool(Present p)
{
if (!p.gameObject.activeSelf) return;
p.gameObject.SetActive(false);
_activePresents.Remove(p);
_pool.Enqueue(p);
}
public void StartSpawning()
{
_isSpawning = true;
GameEvents.GameOver += StopSpawning;
StartCoroutine(SpawnRoutine());
}
public void StopSpawning()
{
_isSpawning = false;
GameEvents.GameOver -= StopSpawning;
StopAllCoroutines();
foreach (var p in _activePresents.ToArray()) ReturnToPool(p);
_activePresents.Clear();
}
private IEnumerator SpawnRoutine()
{
while (_isSpawning)
{
SpawnOne();
if (Random.value < burstChance)
{
yield return new WaitForSeconds(0.2f);
SpawnOne();
}
//TODO: Difficulty scaling
yield return new WaitForSeconds(spawnInterval);
}
}
private void SpawnOne()
{
var p = GetPresent();
var rb = p.GetComponent<Rigidbody2D>();
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
var index = Random.Range(0, spawnPoints.Length);
p.transform.position = spawnPoints[index].position;
p.transform.rotation = Quaternion.identity;
rb.AddForce(initialDirection + new Vector2(Random.Range(-1f,1f), Random.Range(-1f,1f)), ForceMode2D.Impulse);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 540dd15a8852481ab85229c9e3f928c8
timeCreated: 1765312250