2026-09-03 16:35:05 +01:00
|
|
|
using System;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
using Random = UnityEngine.Random;
|
|
|
|
|
|
|
|
|
|
namespace MedievalShopkeeper
|
|
|
|
|
{
|
|
|
|
|
public enum GameEvent
|
|
|
|
|
{
|
|
|
|
|
None,
|
|
|
|
|
GoblinRobber,
|
|
|
|
|
ShopRaid,
|
|
|
|
|
Plague,
|
|
|
|
|
MarketBoom,
|
|
|
|
|
DeliveryArrived
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class GameManager : MonoBehaviour
|
|
|
|
|
{
|
|
|
|
|
public static GameManager Instance { get; private set; }
|
|
|
|
|
|
|
|
|
|
[Header("Time Settings")]
|
|
|
|
|
[Tooltip("How many in-game minutes pass per real-life second? (Default: 1 real second = 1 game minute)")]
|
|
|
|
|
[SerializeField] private float inGameMinutesPerRealSecond = 1f;
|
|
|
|
|
[SerializeField] private int startHour = 7;
|
|
|
|
|
[SerializeField] private int startMinute = 0;
|
|
|
|
|
|
|
|
|
|
[Header("Shop Hours")]
|
|
|
|
|
[SerializeField] private bool autoManageShopHours = true;
|
|
|
|
|
[SerializeField] private int shopOpenHour = 8;
|
|
|
|
|
[SerializeField] private int shopCloseHour = 18;
|
|
|
|
|
|
|
|
|
|
[Header("Economy")]
|
|
|
|
|
[SerializeField] private int startingMoney = 100;
|
|
|
|
|
|
|
|
|
|
[Header("Random Events")]
|
|
|
|
|
[Tooltip("Percentage chance to trigger a random event every in-game hour.")]
|
|
|
|
|
[Range(0f, 100f)]
|
|
|
|
|
[SerializeField] private float hourlyEventChance = 5f;
|
|
|
|
|
|
|
|
|
|
// State properties
|
|
|
|
|
public int CurrentHour { get; private set; }
|
|
|
|
|
public int CurrentMinute { get; private set; }
|
|
|
|
|
public int CurrentMoney { get; private set; }
|
|
|
|
|
public int CurrentCustomersInShop { get; private set; }
|
|
|
|
|
public bool IsShopOpen { get; private set; }
|
|
|
|
|
|
|
|
|
|
// Events
|
|
|
|
|
public event Action<int, int> OnTimeChanged; // Hour, Minute
|
|
|
|
|
public event Action<int> OnMoneyChanged; // New Balance
|
|
|
|
|
public event Action<bool> OnShopStateChanged; // IsOpen
|
|
|
|
|
public event Action<int> OnCustomerCountChanged; // Current Customers
|
|
|
|
|
public event Action<GameEvent> OnRandomEventTriggered; // The Event
|
|
|
|
|
|
|
|
|
|
private float timeAccumulator = 0f;
|
|
|
|
|
private int lastEvaluatedHour = -1;
|
|
|
|
|
|
|
|
|
|
private void Awake()
|
|
|
|
|
{
|
|
|
|
|
if (Instance != null && Instance != this)
|
|
|
|
|
{
|
|
|
|
|
Destroy(gameObject);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
Instance = this;
|
|
|
|
|
|
|
|
|
|
// Initialize state
|
|
|
|
|
CurrentHour = startHour;
|
|
|
|
|
CurrentMinute = startMinute;
|
|
|
|
|
CurrentMoney = startingMoney;
|
|
|
|
|
CurrentCustomersInShop = 0;
|
2026-09-04 16:33:41 +01:00
|
|
|
|
|
|
|
|
if (autoManageShopHours && CurrentHour >= shopOpenHour && CurrentHour < shopCloseHour)
|
|
|
|
|
{
|
|
|
|
|
IsShopOpen = true;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
IsShopOpen = false;
|
|
|
|
|
}
|
2026-09-03 16:35:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Start()
|
|
|
|
|
{
|
|
|
|
|
// Initial broadcasts
|
|
|
|
|
OnTimeChanged?.Invoke(CurrentHour, CurrentMinute);
|
|
|
|
|
OnMoneyChanged?.Invoke(CurrentMoney);
|
|
|
|
|
OnShopStateChanged?.Invoke(IsShopOpen);
|
|
|
|
|
OnCustomerCountChanged?.Invoke(CurrentCustomersInShop);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Update()
|
|
|
|
|
{
|
|
|
|
|
UpdateTime();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#region Time Management
|
|
|
|
|
|
|
|
|
|
public bool IsNightTime => CurrentHour >= shopCloseHour || CurrentHour < shopOpenHour;
|
|
|
|
|
|
|
|
|
|
private void UpdateTime()
|
|
|
|
|
{
|
|
|
|
|
// Night time is 10x faster so the player doesn't have to wait a full hour real-time with no customers
|
|
|
|
|
float timeMultiplier = (IsNightTime && !IsShopOpen) ? 10f : 1f;
|
|
|
|
|
|
|
|
|
|
// Calculate time progression
|
|
|
|
|
timeAccumulator += Time.deltaTime * (inGameMinutesPerRealSecond * timeMultiplier);
|
|
|
|
|
|
|
|
|
|
if (timeAccumulator >= 1f)
|
|
|
|
|
{
|
|
|
|
|
int minutesPassed = Mathf.FloorToInt(timeAccumulator);
|
|
|
|
|
timeAccumulator -= minutesPassed;
|
|
|
|
|
|
|
|
|
|
AdvanceTime(minutesPassed);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void AdvanceTime(int minutes)
|
|
|
|
|
{
|
|
|
|
|
CurrentMinute += minutes;
|
|
|
|
|
|
|
|
|
|
while (CurrentMinute >= 60)
|
|
|
|
|
{
|
|
|
|
|
CurrentMinute -= 60;
|
|
|
|
|
CurrentHour++;
|
|
|
|
|
|
|
|
|
|
if (CurrentHour >= 24)
|
|
|
|
|
{
|
|
|
|
|
CurrentHour = 0;
|
|
|
|
|
OnDayPassed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OnHourPassed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OnTimeChanged?.Invoke(CurrentHour, CurrentMinute);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnHourPassed()
|
|
|
|
|
{
|
|
|
|
|
// Handle automatic opening and closing
|
|
|
|
|
if (autoManageShopHours)
|
|
|
|
|
{
|
|
|
|
|
if (!IsShopOpen && CurrentHour == shopOpenHour)
|
|
|
|
|
{
|
|
|
|
|
OpenShop();
|
|
|
|
|
}
|
|
|
|
|
else if (IsShopOpen && CurrentHour == shopCloseHour)
|
|
|
|
|
{
|
|
|
|
|
CloseShop();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Roll for a random event each hour
|
|
|
|
|
if (CurrentHour != lastEvaluatedHour)
|
|
|
|
|
{
|
|
|
|
|
lastEvaluatedHour = CurrentHour;
|
|
|
|
|
RollForRandomEvent();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnDayPassed()
|
|
|
|
|
{
|
|
|
|
|
Debug.Log("[GameManager] A new day has started!");
|
|
|
|
|
// Future logic: Reset daily stats, pay taxes, etc.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Shop Management
|
|
|
|
|
|
|
|
|
|
public void OpenShop()
|
|
|
|
|
{
|
|
|
|
|
if (IsShopOpen) return;
|
|
|
|
|
IsShopOpen = true;
|
|
|
|
|
|
|
|
|
|
if (IsNightTime)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning("[GameManager] Shop opened outside of regular hours! High risk of robberies.");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Debug.Log("[GameManager] The shop is now OPEN.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OnShopStateChanged?.Invoke(IsShopOpen);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Header("Testing & Debug")]
|
|
|
|
|
[Tooltip("Prefab of the CustomerNPC to spawn for testing.")]
|
|
|
|
|
[SerializeField] private GameObject testNPCPrefab;
|
|
|
|
|
[Tooltip("Where to spawn the test NPC. If left empty, will try to spawn at the ShopDoor.")]
|
|
|
|
|
[SerializeField] private Transform testNPCSpawnPoint;
|
|
|
|
|
|
|
|
|
|
public void SpawnTestNPC()
|
|
|
|
|
{
|
|
|
|
|
if (testNPCPrefab == null)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning("[GameManager] Test NPC Prefab is not assigned!");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Vector3 spawnPos = Vector3.zero;
|
|
|
|
|
|
|
|
|
|
if (testNPCSpawnPoint != null)
|
|
|
|
|
{
|
|
|
|
|
spawnPos = testNPCSpawnPoint.position;
|
|
|
|
|
}
|
|
|
|
|
else if (ShopDoor.Instance != null)
|
|
|
|
|
{
|
|
|
|
|
spawnPos = ShopDoor.Instance.transform.position;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning("[GameManager] No spawn point or ShopDoor found. Spawning at 0,0,0.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GameObject newNPC = Instantiate(testNPCPrefab, spawnPos, Quaternion.identity);
|
2026-09-04 16:33:41 +01:00
|
|
|
|
|
|
|
|
CustomerNPC npc = newNPC.GetComponent<CustomerNPC>();
|
|
|
|
|
if (npc != null)
|
|
|
|
|
{
|
|
|
|
|
npc.Initialize(NPCIntention.Shopper, new System.Collections.Generic.List<ItemData>());
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-03 16:35:05 +01:00
|
|
|
Debug.Log($"[GameManager] Spawned test NPC: {newNPC.name}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void CloseShop()
|
|
|
|
|
{
|
|
|
|
|
if (!IsShopOpen) return;
|
|
|
|
|
IsShopOpen = false;
|
|
|
|
|
Debug.Log("[GameManager] The shop is now CLOSED.");
|
|
|
|
|
|
|
|
|
|
// Force all NPCs to leave and put items back
|
|
|
|
|
CustomerNPC[] allNPCs = FindObjectsByType<CustomerNPC>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
|
|
|
|
foreach (var npc in allNPCs)
|
|
|
|
|
{
|
|
|
|
|
npc.KickOut();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OnShopStateChanged?.Invoke(IsShopOpen);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void CustomerEntered()
|
|
|
|
|
{
|
|
|
|
|
CurrentCustomersInShop++;
|
|
|
|
|
OnCustomerCountChanged?.Invoke(CurrentCustomersInShop);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void CustomerLeft()
|
|
|
|
|
{
|
|
|
|
|
CurrentCustomersInShop = Mathf.Max(0, CurrentCustomersInShop - 1);
|
|
|
|
|
OnCustomerCountChanged?.Invoke(CurrentCustomersInShop);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Economy Management
|
|
|
|
|
|
|
|
|
|
public void AddMoney(int amount)
|
|
|
|
|
{
|
|
|
|
|
if (amount <= 0) return;
|
|
|
|
|
CurrentMoney += amount;
|
|
|
|
|
Debug.Log($"[GameManager] Earned {amount} coin(s). Balance: {CurrentMoney}");
|
|
|
|
|
OnMoneyChanged?.Invoke(CurrentMoney);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool TrySpendMoney(int amount)
|
|
|
|
|
{
|
|
|
|
|
if (amount <= 0) return true;
|
|
|
|
|
|
|
|
|
|
if (CurrentMoney >= amount)
|
|
|
|
|
{
|
|
|
|
|
CurrentMoney -= amount;
|
|
|
|
|
Debug.Log($"[GameManager] Spent {amount} coin(s). Balance: {CurrentMoney}");
|
|
|
|
|
OnMoneyChanged?.Invoke(CurrentMoney);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Debug.LogWarning($"[GameManager] Not enough money! Needed: {amount}, Balance: {CurrentMoney}");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Random Events
|
|
|
|
|
|
|
|
|
|
private void RollForRandomEvent()
|
|
|
|
|
{
|
|
|
|
|
float currentEventChance = hourlyEventChance;
|
|
|
|
|
|
|
|
|
|
// If shop is open at night, triple the chance of random events (robberies)
|
|
|
|
|
if (IsShopOpen && IsNightTime)
|
|
|
|
|
{
|
|
|
|
|
currentEventChance *= 3f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Random.Range(0f, 100f) <= currentEventChance)
|
|
|
|
|
{
|
|
|
|
|
TriggerRandomEvent();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[ContextMenu("Force Trigger Random Event")]
|
|
|
|
|
public void TriggerRandomEvent()
|
|
|
|
|
{
|
|
|
|
|
// Pick a random event that isn't 'None'
|
|
|
|
|
int minEvent = 1; // skip None
|
|
|
|
|
int maxEvent = Enum.GetValues(typeof(GameEvent)).Length;
|
|
|
|
|
|
|
|
|
|
GameEvent triggeredEvent = (GameEvent)Random.Range(minEvent, maxEvent);
|
|
|
|
|
|
|
|
|
|
Debug.Log($"<color=#FF5555><b>[GameManager] RANDOM EVENT TRIGGERED: {triggeredEvent}</b></color>");
|
|
|
|
|
OnRandomEventTriggered?.Invoke(triggeredEvent);
|
|
|
|
|
|
|
|
|
|
// Basic immediate effects
|
|
|
|
|
switch (triggeredEvent)
|
|
|
|
|
{
|
|
|
|
|
case GameEvent.Plague:
|
|
|
|
|
Debug.Log(">> Plague strikes! Customers might avoid the shop today.");
|
|
|
|
|
break;
|
|
|
|
|
case GameEvent.GoblinRobber:
|
|
|
|
|
Debug.Log(">> A Goblin Robber is trying to steal stock!");
|
|
|
|
|
break;
|
|
|
|
|
case GameEvent.ShopRaid:
|
|
|
|
|
Debug.Log(">> A Shop Raid! Lock the doors!");
|
|
|
|
|
break;
|
|
|
|
|
case GameEvent.MarketBoom:
|
|
|
|
|
Debug.Log(">> Market Boom! Selling prices are temporarily higher.");
|
|
|
|
|
break;
|
|
|
|
|
case GameEvent.DeliveryArrived:
|
|
|
|
|
Debug.Log(">> An unexpected extra delivery arrived.");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|