Creating a Farming RPG (like Harvest Moon) in Unity — Part 29: Tool Anims and Stamina

Ever wanted to create a game like Harvest Moon in Unity? Check out Part 29 of our guide here, where we work on player animations and a stamina system. You can also find Part 28 of our guide here, where we went through Part 3 of our NPC events.

  1. Revamping our UI Assets
  2. Adding Player Tool Animations
  3. Adding the Stamina System
  4. Summary

1. Revamping our UI Assets

Before we start on the main topics in this part, we will replace some of our user interface assets, and give them a fresher look

a. Preparing our new asset pack

After we import our new asset pack for the UI, we need to make some adjustments to the sprites before integrating it into our game.

While selecting all the UI assets in this pack, we need to:

  1. Set the texture type to 2D and UI
  2. Compression to None
  3. Sprite Mode to Single
  4. Filter Mode to Bilinear

After these are set, apply the changes

b. Adjusting the scaling of UI assets

When wanting to adjust the height or width of a UI sprite without maintaining its aspect ratio, it might lead to the sprite having a squashed look. Using the sprite editor, we can easily fix that by setting the border of the sprite.

In the bottom right panel of the sprite editor, adjust the values of your border to indicate what parts of the sprite should maintain its aspect ratio, or use the green dots that are around the sprite to scale them. Remember to apply these changes before closing the tab.

After that just assign the sprite image to the respective UI game object, and we rinse and repeat for all the UI in the game.

For the tool and food icons, it follows the same process, the only difference is that the sprites are added in the ScriptableObject of each respective tool.

2. Adding Player Tool Animations

Now we start adding the animations for the player using farming tools on the plot of land. For this tutorial, we will be adding a Watering and Plowing animation for when the player uses the watering can or hoe

a. Setting up our animation files

After getting our animations from mixamo, we need to adjust some settings of each file to make it work smoothly with our character

  1. Rename each animation file as Watering and Plowing
  2. In the rig panel, set the Animation type as Humanoid and the Avatar definition to Copy from the character model's Avatar
  3. In the animation panel, tick the box to Bake the transform rotation

We will add both animation states into the player's animator controller:

  1. Duplicate the animation clip of each imported animation and rename them Watering and Plowing
  2. Drag and drop them into the animator controller and connect them to the Idle animation state with back and forth transition arrows
  3. For the transition arrows pointing away from idle, set to Has No Exit Time and Transition Duration to 0

Add the respective triggers to each transition as well

b. Integrating the tools into the animations

Now it is time to add the tool models into the character animation. In the hierachy, create a gameobject with the tool model as the child of the player's HandPoint, then adjust it's transform to as close to the player's hand as possible.

Set the tool using animation as the default state (Example: Make the plowing animation the default state when adjusting the hoe) then enter Play Mode, pause play mode at a point in the animation and change the transform of the tool game object to fit in the player's hand.

After adjusting the transform the way you like it, copy the component transform and rotation coordinates by selecting Copy > Component, then exit Play Mode and pasting the component values into the tool's transform. Test out the animation, and it will look like the player's holding the tool. Do the same for the Watering Can.

To integrate the animation triggers into the script:

  1. In the PlayerInteraction script, create a variable for Animator and EquipmentData
  2. Reference the animator component of the player on Start
  3. In the Interact function, reference the equipped tool and make switch cases for each tool, while also adding animation triggers to each case
  4. We will also call to interact with the selected land such that the land state will change once we use a certain tool

PlayerInteraction.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerInteraction : MonoBehaviour
{
PlayerController playerController;

Animator animator;

EquipmentData equipmentTool;

//The land the player is currently selecting
Land selectedLand = null;

//The interactable object the player is currently selecting
InteractableObject selectedInteractable = null; 

// Start is called before the first frame update
void Start()
{
    //Get access to our PlayerController component
    playerController = transform.parent.GetComponent<PlayerController>();

    animator = GameObject.FindGameObjectWithTag("Player").GetComponent<Animator>();
}

// Update is called once per frame
void Update()
{
    RaycastHit hit; 
    if(Physics.Raycast(transform.position, Vector3.down,out hit,  1))
    {
        OnInteractableHit(hit);
    }
}

//Handles what happens when the interaction raycast hits something interactable
void OnInteractableHit(RaycastHit hit)
{
    Collider other = hit.collider;

    //Check if the player is going to interact with land
    if(other.tag == "Land")
    {
        //Get the land component
        Land land = other.GetComponent<Land>();
        SelectLand(land);
        return; 
    }

    //Check if the player is going to interact with an Item
    if(other.tag == "Item")
    {
        //Set the interactable to the currently selected interactable
        selectedInteractable = other.GetComponent<InteractableObject>();
        selectedInteractable.OnHover(); 
        return; 
    }

    //Deselect the interactable if the player is not standing on anything at the moment
    if(selectedInteractable != null)
    {
        selectedInteractable.OnMoveAway(); 
        selectedInteractable = null; 
    }

    //Deselect the land if the player is not standing on any land at the moment
    if(selectedLand != null)
    {
        selectedLand.Select(false);
        selectedLand = null;
    }
}

//Handles the selection process of the land
void SelectLand(Land land)
{
    //Set the previously selected land to false (If any)
    if (selectedLand != null)
    {
        selectedLand.Select(false);
    }

    //Set the new selected land to the land we're selecting now. 
    selectedLand = land; 
    land.Select(true);
}

//Triggered when the player presses the tool button
public void Interact()
{
    //Detecting what is the current tool that the player is holding
    ItemData toolSlot = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);

    EquipmentData equipmentTool = toolSlot as EquipmentData;

    //The player shouldn't be able to use his tool when he has his hands full with an item
    if(InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item))
    {

        return;
    }

    //Check if the player is selecting any land
    if(selectedLand != null)
    {
        EquipmentData.ToolType toolType = equipmentTool.toolType;

        switch (toolType)
        {
            case EquipmentData.ToolType.WateringCan:
                animator.SetTrigger("Watering");
                    selectedLand.Interact();
                return;

            case EquipmentData.ToolType.Hoe:
                animator.SetTrigger("Plowing");
                    selectedLand.Interact();
                return;
        }
        return; 
    }

    Debug.Log("Not on any land!");
}

//Triggered when the player presses the item interact button
public void ItemInteract()
{
    //If the player isn't holding anything, pick up an item

    //Check if there is an interactable selected
    if (selectedInteractable != null)
    {
        //Pick it up
        selectedInteractable.Pickup();
    }

}

public void ItemKeep()
{
    //If the player is holding something, keep it in his inventory
    if (InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item))
    {
        InventoryManager.Instance.HandToInventory(InventorySlot.InventoryType.Item);
        return;
    }
}

}

Save the script then set the Idle animation state as the default.

c. Spawning and Despawning of tools

When we want each tool to only appear when their respective animations are being played, we need to make use of the animation timeline.

Now its time to create functions for spawning and despawning the tools. Make a new script called AnimEventsTools in the Player game object, which will contain the functions to spawn and despawn specific tools.

  1. For each tool, create 2 types of game object variables for each model, one for the original model, and the other for an instantiated clone. Create a transform variable for the handPoint as well.
  2. Then create public functions to spawn and despawn each tool

AnimEventsTools.cs

using UnityEngine;

public class AnimEventsTools : MonoBehaviour
{
    // this script contains the anim events for the spawning of tools when the animation is played

    // this is the original objects of the tools 
 public GameObject wateringCan;
    public GameObject Hoe;

    private GameObject newWateringCan;
    private GameObject newHoe;

    public Transform handPoint;
    public void SpawnWaterCan()
    {
        newWateringCan = Instantiate(wateringCan, handPoint);
    }

    public void DespawnWaterCan()
    {
        Destroy(newWateringCan);
    }

    public void SpawnHoe()
    {
        newHoe = Instantiate(Hoe, handPoint);
    }

    public void DespawnHoe()
    {
        Destroy(newHoe);
    }
}

Save the script and close it. Drag the Hoe and Watering Can game objects into the project files to create prefab variants, then delete the original game objects.

Assign the prefab variants and the Hand Point into the inspector of AnimEventTools

Open the animation timeline of the player's animation controller, in the each animation, we will create 2 animation events, one at the start and the other nearly at the end. Now call the function in AnimEventTools to spawn the tool at the start and despawn the tool at the end. Proceed to save and test the animations.

3. Adding the Stamina System

a. Creating the Stamina HUD

We will start off by adding a visual stamina UI to the HUD, import the stamina UI images (can be found in this drive), and add the stamina UI base as a UI image and a sprite emoticon as a child image.

Now we will start adding the stamina system within the script

In PlayerStats, add stamina as a static integer, and add it into SaveStats. Create 2 new functions UseStamina and RestoreStamina, which we will use later on to manage how much stamina the player will have.

PlayerStats.cs

public class PlayerStats 
{
    public static int Money { get; private set; }
    public static int Stamina;

    public const string CURRENCY = "G"; 

    public static void Spend(int cost)
    {
        //Check if the player has enough to spend
        if(cost > Money)
        {
            Debug.LogError("Player does not have enough money");
            return; 
        }
        Money -= cost;
        UIManager.Instance.RenderPlayerStats(); 
        
    }

    public static void Earn(int income)
    {
        Money += income;
        UIManager.Instance.RenderPlayerStats();
    }

    public static void LoadStats(int money, int stamina)
    {
        Money = money;
        Stamina = stamina;
        UIManager.Instance.RenderPlayerStats();
    }

    public static void UseStamina(int staminaLost)
    {
        Stamina -= staminaLost;
        UIManager.Instance.RenderPlayerStats();
    }

    public static void RestoreStamina()
    {
        Stamina = 150;
        UIManager.Instance.RenderPlayerStats();
    }
}

Now in the UIManager script, create a sprite array for the sprite emoticons, an image variable in the staminaUI emoticon in the game, and lastly a stamina count integer. Call the RestoreStamina() function on start to test out the stamina function.

Create a new function called ChangeStaminaUI and call it in RenderPlayerStats. ChangeStaminaUI will switch the the images of the sprite array when the stamina reaches below a certain value.

UIManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro; 

public class UIManager : MonoBehaviour, ITimeTracker
{
    public static UIManager Instance { get; private set; }

    [Header("Screen Management")]
    public GameObject menuScreen;
    public enum Tab
    {
        Inventory, Relationships, Animals
    }
    //The current selected tab
    public Tab selectedTab;

    [Header("Status Bar")]
    //Tool equip slot on the status bar
    public Image toolEquipSlot;
    //Tool Quantity text on the status bar 
    public TextMeshProUGUI toolQuantityText;
    //Time UI
    public TextMeshProUGUI timeText;
    public TextMeshProUGUI dateText;


    [Header("Inventory System")]
    //The inventory panel
    public GameObject inventoryPanel;

    //The tool equip slot UI on the Inventory panel
    public HandInventorySlot toolHandSlot;

    //The tool slot UIs
    public InventorySlot[] toolSlots;

    //The item equip slot UI on the Inventory panel
    public HandInventorySlot itemHandSlot;

    //The item slot UIs
    public InventorySlot[] itemSlots;

    [Header("Item info box")]
    public GameObject itemInfoBox;
    public TextMeshProUGUI itemNameText;
    public TextMeshProUGUI itemDescriptionText;

    [Header("Screen Transitions")]
    public GameObject fadeIn;
    public GameObject fadeOut;

    [Header("Prompts")]
    public YesNoPrompt yesNoPrompt;
    public NamingPrompt namingPrompt;
    [SerializeField] InteractBubble interactBubble; 

    [Header("Player Stats")]
    public TextMeshProUGUI moneyText;

    [Header("Shop")]
    public ShopListingManager shopListingManager;

    [Header("Relationships")]
    public RelationshipListingManager relationshipListingManager;
    public AnimalListingManager animalRelationshipListingManager;

    [Header("Calendar")]
    public CalendarUIListing calendar;

    [Header("Stamina")]
    public Sprite[] staminaUI;
    public Image StaminaUIImage;
    public int staminaCount;


    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    private void Start()
    {
        PlayerStats.RestoreStamina();
        RenderInventory();
        AssignSlotIndexes();
        RenderPlayerStats();
        DisplayItemInfo(null);

        //Add UIManager to the list of objects TimeManager will notify when the time updates
        TimeManager.Instance.RegisterTracker(this);
    }

    #region Prompts

    public void TriggerNamingPrompt(string message, System.Action<string> onConfirmCallback)
    {
        //Check if another prompt is already in progress
        if (namingPrompt.gameObject.activeSelf)
        {
            //Queue the prompt
            namingPrompt.QueuePromptAction(() => TriggerNamingPrompt(message, onConfirmCallback));
            return; 
        }

        //Open the panel
        namingPrompt.gameObject.SetActive(true);

        namingPrompt.CreatePrompt(message, onConfirmCallback); 
    }

    public void TriggerYesNoPrompt(string message, System.Action onYesCallback)
    {
        //Set active the gameobject of the Yes No Prompt
        yesNoPrompt.gameObject.SetActive(true);

        yesNoPrompt.CreatePrompt(message, onYesCallback); 
    }
    #endregion

    #region Tab Management
    public void ToggleMenuPanel()
    {
        menuScreen.SetActive(!menuScreen.activeSelf);

        OpenWindow(selectedTab);

        TabBehaviour.onTabStateChange?.Invoke(); 
    }

    //Manage the opening of windows associated with the tab
    public void OpenWindow(Tab windowToOpen)
    {
        //Disable all windows
        relationshipListingManager.gameObject.SetActive(false);
        inventoryPanel.SetActive(false);
        animalRelationshipListingManager.gameObject.SetActive(false);

        //Open the corresponding window and render it
        switch (windowToOpen)
        {
            case Tab.Inventory:
                inventoryPanel.SetActive(true);
                RenderInventory(); 
                break;
            case Tab.Relationships:
                relationshipListingManager.gameObject.SetActive(true);
                relationshipListingManager.Render(RelationshipStats.relationships); 
                break;
            case Tab.Animals:
                animalRelationshipListingManager.gameObject.SetActive(true);
                animalRelationshipListingManager.Render(AnimalStats.animalRelationships);
                break;

        }

        //Set the selected tab
        selectedTab = windowToOpen; 


    }
    #endregion

    #region Fadein Fadeout Transitions

    public void FadeOutScreen()
    {
        fadeOut.SetActive(true);
    }

    public void FadeInScreen()
    {
        fadeIn.SetActive(true); 
    }

    public void OnFadeInComplete()
    {
        //Disable Fade in Screen when animation is completed
        fadeIn.SetActive(false); 
    }

    //Reset the fadein fadeout screens to their default positions
    public void ResetFadeDefaults()
    {
        fadeOut.SetActive(false);
        fadeIn.SetActive(true);
    }



    #endregion

    #region Inventory
    //Iterate through the slot UI elements and assign it its reference slot index
    public void AssignSlotIndexes()
    {
        for (int i =0; i<toolSlots.Length; i++)
        {
            toolSlots[i].AssignIndex(i);
            itemSlots[i].AssignIndex(i);
        }
    }

    //Render the inventory screen to reflect the Player's Inventory. 
    public void RenderInventory()
    {
        //Get the respective slots to process
        ItemSlotData[] inventoryToolSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Tool);
        ItemSlotData[] inventoryItemSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Item);

        //Render the Tool section
        RenderInventoryPanel(inventoryToolSlots, toolSlots);

        //Render the Item section
        RenderInventoryPanel(inventoryItemSlots, itemSlots);

        //Render the equipped slots
        toolHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));
        itemHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Item));

        //Get Tool Equip from InventoryManager
        ItemData equippedTool = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);

        //Text should be empty by default
        toolQuantityText.text = "";
        //Check if there is an item to display
        if (equippedTool != null)
        {
            //Switch the thumbnail over
            toolEquipSlot.sprite = equippedTool.thumbnail;

            toolEquipSlot.gameObject.SetActive(true);

            //Get quantity 
            int quantity = InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool).quantity;
            if (quantity > 1)
            {
                toolQuantityText.text = quantity.ToString();
            }
            return;
        }

        toolEquipSlot.gameObject.SetActive(false);
    }

    //Iterate through a slot in a section and display them in the UI
    void RenderInventoryPanel(ItemSlotData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            //Display them accordingly
            uiSlots[i].Display(slots[i]);
        }
    }

    public void ToggleInventoryPanel()
    {
        //If the panel is hidden, show it and vice versa
        inventoryPanel.SetActive(!inventoryPanel.activeSelf);

        RenderInventory();
    }

    //Display Item info on the Item infobox
    public void DisplayItemInfo(ItemData data)
    {
        //If data is null, reset
        if(data == null)
        {
            itemNameText.text = "";
            itemDescriptionText.text = "";
            itemInfoBox.SetActive(false); 
            return;
        }
        itemInfoBox.SetActive(true); 
        itemNameText.text = data.name;
        itemDescriptionText.text = data.description; 
    }
    #endregion

    #region Time
    //Callback to handle the UI for time
    public void ClockUpdate(GameTimestamp timestamp)
    {
        //Handle the time
        //Get the hours and minutes
        int hours = timestamp.hour;
        int minutes = timestamp.minute; 

        //AM or PM
        string prefix = "AM ";
        
        //Convert hours to 12 hour clock
        if (hours >= 12)
        {
            //Time becomes PM 
            prefix = "PM ";
            //12 PM and later
            hours = hours - 12; 
            Debug.Log(hours);
        }
        //Special case for 12am/pm to display it as 12 instead of 0
        hours = hours == 0 ? 12 : hours;
        
        //Format it for the time text display
        timeText.text = prefix + hours + ":" + minutes.ToString("00");

        //Handle the Date
        int day = timestamp.day;
        string season = timestamp.season.ToString();
        string dayOfTheWeek = timestamp.GetDayOfTheWeek().ToString();

        //Format it for the date text display
        dateText.text = season + " " + day + " (" + dayOfTheWeek +")";

    }
    #endregion

    //Render the UI of the player stats in the HUD
    public void RenderPlayerStats()
    {
        moneyText.text = PlayerStats.Money + PlayerStats.CURRENCY;
        staminaCount = PlayerStats.Stamina;
        ChangeStaminaUI();
        
    }

    //Open the shop window with the shop items listed
    public void OpenShop(List<ItemData> shopItems)
    {
        //Set active the shop window
        shopListingManager.gameObject.SetActive(true);
        shopListingManager.Render(shopItems); 
    }

    public void ToggleRelationshipPanel()
    {
        GameObject panel = relationshipListingManager.gameObject;
        panel.SetActive(!panel.activeSelf);

        //If open, render the screen
        if (panel.activeSelf)
        {
            relationshipListingManager.Render(RelationshipStats.relationships);
        }        
    }

    public void InteractPrompt(Transform item, string message, float offset)
    {
        interactBubble.gameObject.SetActive(true); 
        interactBubble.transform.position = item.transform.position + new Vector3(0, offset, 0);
        interactBubble.Display(message);
    }

    public void DeactivateInteractPrompt()
    {
        interactBubble.gameObject.SetActive(false);
    }

    public void ChangeStaminaUI()
    {
        if (staminaCount <= 45) StaminaUIImage.sprite = staminaUI[3]; // exhausted
        else if (staminaCount <= 80) StaminaUIImage.sprite = staminaUI[2]; // tired
        else if (staminaCount <= 115) StaminaUIImage.sprite = staminaUI[1]; // active
        else if (staminaCount <= 150) StaminaUIImage.sprite = staminaUI[0]; // energised

    }
}

b. Adding the stamina usage into the script

Returning back to the PlayerInteraction script, add the UseStamina function into the switch cases.

PlayerInteraction.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerInteraction : MonoBehaviour
{
    PlayerController playerController;

    Animator animator;

    EquipmentData equipmentTool;

    //The land the player is currently selecting
    Land selectedLand = null;

    //The interactable object the player is currently selecting
    InteractableObject selectedInteractable = null; 

    // Start is called before the first frame update
    void Start()
    {
        //Get access to our PlayerController component
        playerController = transform.parent.GetComponent<PlayerController>();

        animator = GameObject.FindGameObjectWithTag("Player").GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        RaycastHit hit; 
        if(Physics.Raycast(transform.position, Vector3.down,out hit,  1))
        {
            OnInteractableHit(hit);
        }
    }

    //Handles what happens when the interaction raycast hits something interactable
    void OnInteractableHit(RaycastHit hit)
    {
        Collider other = hit.collider;
        
        //Check if the player is going to interact with land
        if(other.tag == "Land")
        {
            //Get the land component
            Land land = other.GetComponent<Land>();
            SelectLand(land);
            return; 
        }

        //Check if the player is going to interact with an Item
        if(other.tag == "Item")
        {
            //Set the interactable to the currently selected interactable
            selectedInteractable = other.GetComponent<InteractableObject>();
            selectedInteractable.OnHover(); 
            return; 
        }

        //Deselect the interactable if the player is not standing on anything at the moment
        if(selectedInteractable != null)
        {
            selectedInteractable.OnMoveAway(); 
            selectedInteractable = null; 
        }

        //Deselect the land if the player is not standing on any land at the moment
        if(selectedLand != null)
        {
            selectedLand.Select(false);
            selectedLand = null;
        }
    }

    //Handles the selection process of the land
    void SelectLand(Land land)
    {
        //Set the previously selected land to false (If any)
        if (selectedLand != null)
        {
            selectedLand.Select(false);
        }
        
        //Set the new selected land to the land we're selecting now. 
        selectedLand = land; 
        land.Select(true);
    }

    //Triggered when the player presses the tool button
    public void Interact()
    {
        //Detecting what is the current tool that the player is holding
        ItemData toolSlot = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);

        EquipmentData equipmentTool = toolSlot as EquipmentData;

        //The player shouldn't be able to use his tool when he has his hands full with an item
        if(InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item))
        {

            return;
        }

        //Check if the player is selecting any land
        if(selectedLand != null)
        {
            EquipmentData.ToolType toolType = equipmentTool.toolType;

            switch (toolType)
            {
                case EquipmentData.ToolType.WateringCan:
                    PlayerStats.UseStamina(2);
                    animator.SetTrigger("Watering");
                    selectedLand.Interact();
                    return;

                case EquipmentData.ToolType.Hoe:
                    PlayerStats.UseStamina(2);
                    animator.SetTrigger("Plowing");
                    selectedLand.Interact();
                    return;
            }
            return; 
        }

        Debug.Log("Not on any land!");
    }

    //Triggered when the player presses the item interact button
    public void ItemInteract()
    {
        //If the player isn't holding anything, pick up an item

        //Check if there is an interactable selected
        if (selectedInteractable != null)
        {
            //Pick it up
            selectedInteractable.Pickup();
        }

    }

    
    public void ItemKeep()
    {
        //If the player is holding something, keep it in his inventory
        if (InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item))
        {
            InventoryManager.Instance.HandToInventory(InventorySlot.InventoryType.Item);
            return;
        }
    }
}

Save all these scripts and assign the Stamina UI emoticons to the UIManager in the inspector in the order to energised to exhausted. Then test the game, if the stamina UI changes after using the tools for a while, our stamina system is working.

c. Saving our stamina

We need to add our stamina variables into our save system, to do that we can start adding it into the PlayerSaveState script

PlayerSaveState.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class PlayerSaveState
{
    public int money;
    public int stamina;

    public PlayerSaveState(int money, int stamina)
    {
        this.money = money;
        this.stamina = stamina;
    }

    public void LoadData()
    {
        PlayerStats.LoadStats(money, stamina);
    }

    public static PlayerSaveState Export()
    {
        return new PlayerSaveState(PlayerStats.Money, PlayerStats.Stamina); 
    }
}

Lastly for our current stamina system, we will make it such that when the player sleeps , they will replenish stamina completely.

In our GameStateManager script, we will call RestoreStamina in the Sleep function

GameStateManager.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices.WindowsRuntime;
using UnityEngine;
using UnityEngine.Events; 

public class GameStateManager : MonoBehaviour, ITimeTracker
{
    public static GameStateManager Instance { get; private set; }

    //Check if the screen has finished fading out
    bool screenFadedOut;

    //To track interval updates
    private int minutesElapsed = 0;

    //Event triggered every 15 minutes
    public UnityEvent onIntervalUpdate;

    [SerializeField]
    GameBlackboard blackboard = new GameBlackboard();
    //This blackboard will not be saved 
    GameBlackboard sceneItemsBoard = new GameBlackboard();

    const string TIMESTAMP = "Timestamp";
    public GameBlackboard GetBlackboard()
    {
        return blackboard;
    }

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        //Add this to TimeManager's Listener list
        TimeManager.Instance.RegisterTracker(this);
        SceneTransitionManager.Instance.onLocationLoad.AddListener(RenderPersistentObjects); 
    }

    public void ClockUpdate(GameTimestamp timestamp)
    {
        UpdateShippingState(timestamp); 
        UpdateFarmState(timestamp);
        IncubationManager.UpdateEggs();
        blackboard.SetValue(TIMESTAMP, timestamp);

        if (timestamp.hour == 0 && timestamp.minute == 0)
        {
            
            OnDayReset(); 
        }

        if(minutesElapsed >= 15)
        {
            minutesElapsed = 0;
            onIntervalUpdate?.Invoke();
            

        } else
        {
            minutesElapsed++; 
        }
    }

    //Called when the day has been reset
    void OnDayReset()
    {
        Debug.Log("Day has been reset"); 
        foreach(NPCRelationshipState npc in RelationshipStats.relationships)
        {
            npc.hasTalkedToday = false;
            npc.giftGivenToday = false; 
        }
        AnimalFeedManager.ResetFeedboxes();
        AnimalStats.OnDayReset();
    }

    void UpdateShippingState(GameTimestamp timestamp)
    {
        //Check if the hour is here (Exactly 1800 hours)
        if(timestamp.hour == ShippingBin.hourToShip && timestamp.minute == 0)
        {
            ShippingBin.ShipItems();
        }
    }

    void UpdateFarmState(GameTimestamp timestamp)
    {
        //Update the Land and Crop Save states as long as the player is outside of the Farm scene
        if (SceneTransitionManager.Instance.currentLocation != SceneTransitionManager.Location.Farm)
        {
            //If there is nothing to update to begin with, stop 
            if (LandManager.farmData == null) return;

            //Retrieve the Land and Farm data from the static variable
            List<LandSaveState> landData = LandManager.farmData.Item1;
            List<CropSaveState> cropData = LandManager.farmData.Item2;

            //If there are no crops planted, we don't need to worry about updating anything
            if (cropData.Count == 0) return;

            for (int i = 0; i < cropData.Count; i++)
            {
                //Get the crop and corresponding land data
                CropSaveState crop = cropData[i];
                LandSaveState land = landData[crop.landID];

                //Check if the crop is already wilted
                if (crop.cropState == CropBehaviour.CropState.Wilted) continue;

                //Update the Land's state
                land.ClockUpdate(timestamp);
                //Update the crop's state based on the land state
                if (land.landStatus == Land.LandStatus.Watered)
                {
                    crop.Grow();
                }
                else if (crop.cropState != CropBehaviour.CropState.Seed)
                {
                    crop.Wither();
                }

                //Update the element in the array
                cropData[i] = crop;
                landData[crop.landID] = land;

            }
        }
    }

    //Instantiate the GameObject such that it stays in the scene even after the player leaves 
    public GameObject PersistentInstantiate(GameObject gameObject, Vector3 position, Quaternion rotation)
    {
        GameObject item = Instantiate(gameObject, position, rotation);
        //Save it to the blackboard
        (GameObject, Vector3) itemInformation = (gameObject, position);
        List<(GameObject, Vector3)> items = sceneItemsBoard.GetOrInitList<(GameObject, Vector3)>(SceneTransitionManager.Instance.currentLocation.ToString());
        items.Add(itemInformation);
        sceneItemsBoard.Debug();
        return item;
    }

    public void PersistentDestroy(GameObject item)
    {
        if (sceneItemsBoard.TryGetValue(SceneTransitionManager.Instance.currentLocation.ToString(), out List<(GameObject, Vector3)> items))
        {
            int index = items.FindIndex(i => i.Item2 == item.transform.position);
            items.RemoveAt(index); 
        }
        Destroy(item); 
    }

    void RenderPersistentObjects()
    {
        Debug.Log("Rendering Persistent Objects");
        if (sceneItemsBoard.TryGetValue(SceneTransitionManager.Instance.currentLocation.ToString(), out List<(GameObject, Vector3)> items)) { 
            foreach((GameObject, Vector3) item in items)
            {
                Instantiate(item.Item1, item.Item2, Quaternion.identity); 
            }
        }
    }

    public void Sleep()
    {
        //Call a fadeout
        UIManager.Instance.FadeOutScreen();
        screenFadedOut = false;
        StartCoroutine(TransitionTime());
        //Restore Stamina fully when sleeping
        PlayerStats.RestoreStamina();
    }

    IEnumerator TransitionTime()
    {
        //Calculate how many ticks we need to advance the time to 6am

        //Get the time stamp of 6am the next day
        GameTimestamp timestampOfNextDay = TimeManager.Instance.GetGameTimestamp();
        timestampOfNextDay.day += 1;
        timestampOfNextDay.hour = 6;
        timestampOfNextDay.minute = 0;
        Debug.Log(timestampOfNextDay.day + " " + timestampOfNextDay.hour + ":" + timestampOfNextDay.minute);
        
        
        //Wait for the scene to finish fading out before loading the next scene
        while (!screenFadedOut)
        {
            yield return new WaitForSeconds(1f);
        }
        TimeManager.Instance.SkipTime(timestampOfNextDay);
        //Save
        SaveManager.Save(ExportSaveState());
        //Reset the boolean
        screenFadedOut = false;
        UIManager.Instance.ResetFadeDefaults();

    }

    //Called when the screen has faded out
    public void OnFadeOutComplete()
    {
        screenFadedOut = true;

    }

    public GameSaveState ExportSaveState()
    {
        //Retrieve Farm Data 
        FarmSaveState farmSaveState = FarmSaveState.Export();

        //Retrieve inventory data 
        InventorySaveState inventorySaveState = InventorySaveState.Export();

        PlayerSaveState playerSaveState = PlayerSaveState.Export();

        //Time
        GameTimestamp timestamp = TimeManager.Instance.GetGameTimestamp();

        return new GameSaveState(blackboard, farmSaveState, inventorySaveState, timestamp, playerSaveState);
    }

    public void LoadSave()
    {
        //Retrieve the loaded save
        GameSaveState save = SaveManager.Load();
        //Load up the parts

        blackboard = save.blackboard;
        blackboard.Debug();

        //Time
        TimeManager.Instance.LoadTime(save.timestamp);

        //Inventory
        save.inventorySaveState.LoadData();

        //Farming data 
        save.farmSaveState.LoadData();

        //Player Stats
        save.playerSaveState.LoadData();

        //Relationship stats
        AnimalStats.LoadStats();
        RelationshipStats.LoadStats(); 

    }
}

Summary

In this article, we added tool animations to our player and a stamina system to our game.

If you are a Patreon supporter, you can download the project files for what we have done so far. To use the files, you will have to unzip the file (7-Zip can help you do that).

Here is the final code for all the scripts we have worked on today:

PlayerInteraction.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerInteraction : MonoBehaviour
{
    PlayerController playerController;

    Animator animator;

    EquipmentData equipmentTool;

    //The land the player is currently selecting
    Land selectedLand = null;

    //The interactable object the player is currently selecting
    InteractableObject selectedInteractable = null; 

    // Start is called before the first frame update
    void Start()
    {
        //Get access to our PlayerController component
        playerController = transform.parent.GetComponent<PlayerController>();

        animator = GameObject.FindGameObjectWithTag("Player").GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        RaycastHit hit; 
        if(Physics.Raycast(transform.position, Vector3.down,out hit,  1))
        {
            OnInteractableHit(hit);
        }
    }

    //Handles what happens when the interaction raycast hits something interactable
    void OnInteractableHit(RaycastHit hit)
    {
        Collider other = hit.collider;
        
        //Check if the player is going to interact with land
        if(other.tag == "Land")
        {
            //Get the land component
            Land land = other.GetComponent<Land>();
            SelectLand(land);
            return; 
        }

        //Check if the player is going to interact with an Item
        if(other.tag == "Item")
        {
            //Set the interactable to the currently selected interactable
            selectedInteractable = other.GetComponent<InteractableObject>();
            selectedInteractable.OnHover(); 
            return; 
        }

        //Deselect the interactable if the player is not standing on anything at the moment
        if(selectedInteractable != null)
        {
            selectedInteractable.OnMoveAway(); 
            selectedInteractable = null; 
        }

        //Deselect the land if the player is not standing on any land at the moment
        if(selectedLand != null)
        {
            selectedLand.Select(false);
            selectedLand = null;
        }
    }

    //Handles the selection process of the land
    void SelectLand(Land land)
    {
        //Set the previously selected land to false (If any)
        if (selectedLand != null)
        {
            selectedLand.Select(false);
        }
        
        //Set the new selected land to the land we're selecting now. 
        selectedLand = land; 
        land.Select(true);
    }

    //Triggered when the player presses the tool button
    public void Interact()
    {
        //Detecting what is the current tool that the player is holding
        ItemData toolSlot = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);

        EquipmentData equipmentTool = toolSlot as EquipmentData;

        //The player shouldn't be able to use his tool when he has his hands full with an item
        if(InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item))
        {

            return;
        }

        //Check if the player is selecting any land
        if(selectedLand != null)
        {
            EquipmentData.ToolType toolType = equipmentTool.toolType;

            switch (toolType)
            {
                case EquipmentData.ToolType.WateringCan:
                    PlayerStats.UseStamina(2);
                    animator.SetTrigger("Watering");
                    selectedLand.Interact();
                    return;

                case EquipmentData.ToolType.Hoe:
                    PlayerStats.UseStamina(2);
                    animator.SetTrigger("Plowing");
                    selectedLand.Interact();
                    return;
            }
            return; 
        }

        Debug.Log("Not on any land!");
    }

    //Triggered when the player presses the item interact button
    public void ItemInteract()
    {
        //If the player isn't holding anything, pick up an item

        //Check if there is an interactable selected
        if (selectedInteractable != null)
        {
            //Pick it up
            selectedInteractable.Pickup();
        }

    }

    
    public void ItemKeep()
    {
        //If the player is holding something, keep it in his inventory
        if (InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item))
        {
            InventoryManager.Instance.HandToInventory(InventorySlot.InventoryType.Item);
            return;
        }
    }
}

UIManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro; 

public class UIManager : MonoBehaviour, ITimeTracker
{
    public static UIManager Instance { get; private set; }

    [Header("Screen Management")]
    public GameObject menuScreen;
    public enum Tab
    {
        Inventory, Relationships, Animals
    }
    //The current selected tab
    public Tab selectedTab;

    [Header("Status Bar")]
    //Tool equip slot on the status bar
    public Image toolEquipSlot;
    //Tool Quantity text on the status bar 
    public TextMeshProUGUI toolQuantityText;
    //Time UI
    public TextMeshProUGUI timeText;
    public TextMeshProUGUI dateText;


    [Header("Inventory System")]
    //The inventory panel
    public GameObject inventoryPanel;

    //The tool equip slot UI on the Inventory panel
    public HandInventorySlot toolHandSlot;

    //The tool slot UIs
    public InventorySlot[] toolSlots;

    //The item equip slot UI on the Inventory panel
    public HandInventorySlot itemHandSlot;

    //The item slot UIs
    public InventorySlot[] itemSlots;

    [Header("Item info box")]
    public GameObject itemInfoBox;
    public TextMeshProUGUI itemNameText;
    public TextMeshProUGUI itemDescriptionText;

    [Header("Screen Transitions")]
    public GameObject fadeIn;
    public GameObject fadeOut;

    [Header("Prompts")]
    public YesNoPrompt yesNoPrompt;
    public NamingPrompt namingPrompt;
    [SerializeField] InteractBubble interactBubble; 

    [Header("Player Stats")]
    public TextMeshProUGUI moneyText;

    [Header("Shop")]
    public ShopListingManager shopListingManager;

    [Header("Relationships")]
    public RelationshipListingManager relationshipListingManager;
    public AnimalListingManager animalRelationshipListingManager;

    [Header("Calendar")]
    public CalendarUIListing calendar;

    [Header("Stamina")]
    public Sprite[] staminaUI;
    public Image StaminaUIImage;
    public int staminaCount;


    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    private void Start()
    {
        PlayerStats.RestoreStamina();
        RenderInventory();
        AssignSlotIndexes();
        RenderPlayerStats();
        DisplayItemInfo(null);

        //Add UIManager to the list of objects TimeManager will notify when the time updates
        TimeManager.Instance.RegisterTracker(this);
    }

    #region Prompts

    public void TriggerNamingPrompt(string message, System.Action<string> onConfirmCallback)
    {
        //Check if another prompt is already in progress
        if (namingPrompt.gameObject.activeSelf)
        {
            //Queue the prompt
            namingPrompt.QueuePromptAction(() => TriggerNamingPrompt(message, onConfirmCallback));
            return; 
        }

        //Open the panel
        namingPrompt.gameObject.SetActive(true);

        namingPrompt.CreatePrompt(message, onConfirmCallback); 
    }

    public void TriggerYesNoPrompt(string message, System.Action onYesCallback)
    {
        //Set active the gameobject of the Yes No Prompt
        yesNoPrompt.gameObject.SetActive(true);

        yesNoPrompt.CreatePrompt(message, onYesCallback); 
    }
    #endregion

    #region Tab Management
    public void ToggleMenuPanel()
    {
        menuScreen.SetActive(!menuScreen.activeSelf);

        OpenWindow(selectedTab);

        TabBehaviour.onTabStateChange?.Invoke(); 
    }

    //Manage the opening of windows associated with the tab
    public void OpenWindow(Tab windowToOpen)
    {
        //Disable all windows
        relationshipListingManager.gameObject.SetActive(false);
        inventoryPanel.SetActive(false);
        animalRelationshipListingManager.gameObject.SetActive(false);

        //Open the corresponding window and render it
        switch (windowToOpen)
        {
            case Tab.Inventory:
                inventoryPanel.SetActive(true);
                RenderInventory(); 
                break;
            case Tab.Relationships:
                relationshipListingManager.gameObject.SetActive(true);
                relationshipListingManager.Render(RelationshipStats.relationships); 
                break;
            case Tab.Animals:
                animalRelationshipListingManager.gameObject.SetActive(true);
                animalRelationshipListingManager.Render(AnimalStats.animalRelationships);
                break;

        }

        //Set the selected tab
        selectedTab = windowToOpen; 


    }
    #endregion

    #region Fadein Fadeout Transitions

    public void FadeOutScreen()
    {
        fadeOut.SetActive(true);
    }

    public void FadeInScreen()
    {
        fadeIn.SetActive(true); 
    }

    public void OnFadeInComplete()
    {
        //Disable Fade in Screen when animation is completed
        fadeIn.SetActive(false); 
    }

    //Reset the fadein fadeout screens to their default positions
    public void ResetFadeDefaults()
    {
        fadeOut.SetActive(false);
        fadeIn.SetActive(true);
    }



    #endregion

    #region Inventory
    //Iterate through the slot UI elements and assign it its reference slot index
    public void AssignSlotIndexes()
    {
        for (int i =0; i<toolSlots.Length; i++)
        {
            toolSlots[i].AssignIndex(i);
            itemSlots[i].AssignIndex(i);
        }
    }

    //Render the inventory screen to reflect the Player's Inventory. 
    public void RenderInventory()
    {
        //Get the respective slots to process
        ItemSlotData[] inventoryToolSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Tool);
        ItemSlotData[] inventoryItemSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Item);

        //Render the Tool section
        RenderInventoryPanel(inventoryToolSlots, toolSlots);

        //Render the Item section
        RenderInventoryPanel(inventoryItemSlots, itemSlots);

        //Render the equipped slots
        toolHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));
        itemHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Item));

        //Get Tool Equip from InventoryManager
        ItemData equippedTool = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);

        //Text should be empty by default
        toolQuantityText.text = "";
        //Check if there is an item to display
        if (equippedTool != null)
        {
            //Switch the thumbnail over
            toolEquipSlot.sprite = equippedTool.thumbnail;

            toolEquipSlot.gameObject.SetActive(true);

            //Get quantity 
            int quantity = InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool).quantity;
            if (quantity > 1)
            {
                toolQuantityText.text = quantity.ToString();
            }
            return;
        }

        toolEquipSlot.gameObject.SetActive(false);
    }

    //Iterate through a slot in a section and display them in the UI
    void RenderInventoryPanel(ItemSlotData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            //Display them accordingly
            uiSlots[i].Display(slots[i]);
        }
    }

    public void ToggleInventoryPanel()
    {
        //If the panel is hidden, show it and vice versa
        inventoryPanel.SetActive(!inventoryPanel.activeSelf);

        RenderInventory();
    }

    //Display Item info on the Item infobox
    public void DisplayItemInfo(ItemData data)
    {
        //If data is null, reset
        if(data == null)
        {
            itemNameText.text = "";
            itemDescriptionText.text = "";
            itemInfoBox.SetActive(false); 
            return;
        }
        itemInfoBox.SetActive(true); 
        itemNameText.text = data.name;
        itemDescriptionText.text = data.description; 
    }
    #endregion

    #region Time
    //Callback to handle the UI for time
    public void ClockUpdate(GameTimestamp timestamp)
    {
        //Handle the time
        //Get the hours and minutes
        int hours = timestamp.hour;
        int minutes = timestamp.minute; 

        //AM or PM
        string prefix = "AM ";
        
        //Convert hours to 12 hour clock
        if (hours >= 12)
        {
            //Time becomes PM 
            prefix = "PM ";
            //12 PM and later
            hours = hours - 12; 
            Debug.Log(hours);
        }
        //Special case for 12am/pm to display it as 12 instead of 0
        hours = hours == 0 ? 12 : hours;
        
        //Format it for the time text display
        timeText.text = prefix + hours + ":" + minutes.ToString("00");

        //Handle the Date
        int day = timestamp.day;
        string season = timestamp.season.ToString();
        string dayOfTheWeek = timestamp.GetDayOfTheWeek().ToString();

        //Format it for the date text display
        dateText.text = season + " " + day + " (" + dayOfTheWeek +")";

    }
    #endregion

    //Render the UI of the player stats in the HUD
    public void RenderPlayerStats()
    {
        moneyText.text = PlayerStats.Money + PlayerStats.CURRENCY;
        staminaCount = PlayerStats.Stamina;
        ChangeStaminaUI();
        
    }

    //Open the shop window with the shop items listed
    public void OpenShop(List<ItemData> shopItems)
    {
        //Set active the shop window
        shopListingManager.gameObject.SetActive(true);
        shopListingManager.Render(shopItems); 
    }

    public void ToggleRelationshipPanel()
    {
        GameObject panel = relationshipListingManager.gameObject;
        panel.SetActive(!panel.activeSelf);

        //If open, render the screen
        if (panel.activeSelf)
        {
            relationshipListingManager.Render(RelationshipStats.relationships);
        }        
    }

    public void InteractPrompt(Transform item, string message, float offset)
    {
        interactBubble.gameObject.SetActive(true); 
        interactBubble.transform.position = item.transform.position + new Vector3(0, offset, 0);
        interactBubble.Display(message);
    }

    public void DeactivateInteractPrompt()
    {
        interactBubble.gameObject.SetActive(false);
    }

    public void ChangeStaminaUI()
    {
        if (staminaCount <= 45) StaminaUIImage.sprite = staminaUI[3]; // exhausted
        else if (staminaCount <= 80) StaminaUIImage.sprite = staminaUI[2]; // tired
        else if (staminaCount <= 115) StaminaUIImage.sprite = staminaUI[1]; // active
        else if (staminaCount <= 150) StaminaUIImage.sprite = staminaUI[0]; // energised

    }
}

PlayerSaveState.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class PlayerSaveState
{
    //In future we will collect information like achievements, shipping history, building assets/upgrades and the like here. 
    public int money;
    public int stamina;

    public PlayerSaveState(int money, int stamina)
    {
        this.money = money;
        this.stamina = stamina;
        this.stamina = stamina;
    }

    public void LoadData()
    {
        PlayerStats.LoadStats(money, stamina);
    }

    public static PlayerSaveState Export()
    {
        return new PlayerSaveState(PlayerStats.Money, PlayerStats.Stamina); 
    }
}

GameStateManager.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices.WindowsRuntime;
using UnityEngine;
using UnityEngine.Events; 

public class GameStateManager : MonoBehaviour, ITimeTracker
{
    public static GameStateManager Instance { get; private set; }

    //Check if the screen has finished fading out
    bool screenFadedOut;

    //To track interval updates
    private int minutesElapsed = 0;

    //Event triggered every 15 minutes
    public UnityEvent onIntervalUpdate;

    [SerializeField]
    GameBlackboard blackboard = new GameBlackboard();
    //This blackboard will not be saved 
    GameBlackboard sceneItemsBoard = new GameBlackboard();

    const string TIMESTAMP = "Timestamp";
    public GameBlackboard GetBlackboard()
    {
        return blackboard;
    }

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        //Add this to TimeManager's Listener list
        TimeManager.Instance.RegisterTracker(this);
        SceneTransitionManager.Instance.onLocationLoad.AddListener(RenderPersistentObjects); 
    }

    public void ClockUpdate(GameTimestamp timestamp)
    {
        UpdateShippingState(timestamp); 
        UpdateFarmState(timestamp);
        IncubationManager.UpdateEggs();
        blackboard.SetValue(TIMESTAMP, timestamp);

        if (timestamp.hour == 0 && timestamp.minute == 0)
        {
            
            OnDayReset(); 
        }

        if(minutesElapsed >= 15)
        {
            minutesElapsed = 0;
            onIntervalUpdate?.Invoke();
            

        } else
        {
            minutesElapsed++; 
        }
    }

    //Called when the day has been reset
    void OnDayReset()
    {
        Debug.Log("Day has been reset"); 
        foreach(NPCRelationshipState npc in RelationshipStats.relationships)
        {
            npc.hasTalkedToday = false;
            npc.giftGivenToday = false; 
        }
        AnimalFeedManager.ResetFeedboxes();
        AnimalStats.OnDayReset();
    }

    void UpdateShippingState(GameTimestamp timestamp)
    {
        //Check if the hour is here (Exactly 1800 hours)
        if(timestamp.hour == ShippingBin.hourToShip && timestamp.minute == 0)
        {
            ShippingBin.ShipItems();
        }
    }

    void UpdateFarmState(GameTimestamp timestamp)
    {
        //Update the Land and Crop Save states as long as the player is outside of the Farm scene
        if (SceneTransitionManager.Instance.currentLocation != SceneTransitionManager.Location.Farm)
        {
            //If there is nothing to update to begin with, stop 
            if (LandManager.farmData == null) return;

            //Retrieve the Land and Farm data from the static variable
            List<LandSaveState> landData = LandManager.farmData.Item1;
            List<CropSaveState> cropData = LandManager.farmData.Item2;

            //If there are no crops planted, we don't need to worry about updating anything
            if (cropData.Count == 0) return;

            for (int i = 0; i < cropData.Count; i++)
            {
                //Get the crop and corresponding land data
                CropSaveState crop = cropData[i];
                LandSaveState land = landData[crop.landID];

                //Check if the crop is already wilted
                if (crop.cropState == CropBehaviour.CropState.Wilted) continue;

                //Update the Land's state
                land.ClockUpdate(timestamp);
                //Update the crop's state based on the land state
                if (land.landStatus == Land.LandStatus.Watered)
                {
                    crop.Grow();
                }
                else if (crop.cropState != CropBehaviour.CropState.Seed)
                {
                    crop.Wither();
                }

                //Update the element in the array
                cropData[i] = crop;
                landData[crop.landID] = land;

            }

            /*LandManager.farmData.Item2.ForEach((CropSaveState crop) => {
                Debug.Log(crop.seedToGrow + "\n Health: " + crop.health + "\n Growth: " + crop.growth + "\n State: " + crop.cropState.ToString());
            });*/
        }
    }

    //Instantiate the GameObject such that it stays in the scene even after the player leaves 
    public GameObject PersistentInstantiate(GameObject gameObject, Vector3 position, Quaternion rotation)
    {
        GameObject item = Instantiate(gameObject, position, rotation);
        //Save it to the blackboard
        (GameObject, Vector3) itemInformation = (gameObject, position);
        List<(GameObject, Vector3)> items = sceneItemsBoard.GetOrInitList<(GameObject, Vector3)>(SceneTransitionManager.Instance.currentLocation.ToString());
        items.Add(itemInformation);
        sceneItemsBoard.Debug();
        return item;
    }

    public void PersistentDestroy(GameObject item)
    {
        if (sceneItemsBoard.TryGetValue(SceneTransitionManager.Instance.currentLocation.ToString(), out List<(GameObject, Vector3)> items))
        {
            int index = items.FindIndex(i => i.Item2 == item.transform.position);
            items.RemoveAt(index); 
        }
        Destroy(item); 
    }

    void RenderPersistentObjects()
    {
        Debug.Log("Rendering Persistent Objects");
        if (sceneItemsBoard.TryGetValue(SceneTransitionManager.Instance.currentLocation.ToString(), out List<(GameObject, Vector3)> items)) { 
            foreach((GameObject, Vector3) item in items)
            {
                Instantiate(item.Item1, item.Item2, Quaternion.identity); 
            }
        }
    }

    public void Sleep()
    {
        //Call a fadeout
        UIManager.Instance.FadeOutScreen();
        screenFadedOut = false;
        StartCoroutine(TransitionTime());
        //Restore Stamina fully when sleeping
        PlayerStats.RestoreStamina();
    }

    IEnumerator TransitionTime()
    {
        //Calculate how many ticks we need to advance the time to 6am

        //Get the time stamp of 6am the next day
        GameTimestamp timestampOfNextDay = TimeManager.Instance.GetGameTimestamp();
        timestampOfNextDay.day += 1;
        timestampOfNextDay.hour = 6;
        timestampOfNextDay.minute = 0;
        Debug.Log(timestampOfNextDay.day + " " + timestampOfNextDay.hour + ":" + timestampOfNextDay.minute);
        
        
        //Wait for the scene to finish fading out before loading the next scene
        while (!screenFadedOut)
        {
            yield return new WaitForSeconds(1f);
        }
        TimeManager.Instance.SkipTime(timestampOfNextDay);
        //Save
        SaveManager.Save(ExportSaveState());
        //Reset the boolean
        screenFadedOut = false;
        UIManager.Instance.ResetFadeDefaults();

    }

    //Called when the screen has faded out
    public void OnFadeOutComplete()
    {
        screenFadedOut = true;

    }

    public GameSaveState ExportSaveState()
    {
        //Retrieve Farm Data 
        FarmSaveState farmSaveState = FarmSaveState.Export();

        //Retrieve inventory data 
        InventorySaveState inventorySaveState = InventorySaveState.Export();

        PlayerSaveState playerSaveState = PlayerSaveState.Export();

        //Time
        GameTimestamp timestamp = TimeManager.Instance.GetGameTimestamp();
        //Relationships
        //RelationshipSaveState relationshipSaveState = RelationshipSaveState.Export();

        return new GameSaveState(blackboard, farmSaveState, inventorySaveState, timestamp, playerSaveState);
    }

    public void LoadSave()
    {
        //Retrieve the loaded save
        GameSaveState save = SaveManager.Load();
        //Load up the parts

        blackboard = save.blackboard;
        blackboard.Debug();

        //Time
        TimeManager.Instance.LoadTime(save.timestamp);

        //Inventory
        save.inventorySaveState.LoadData();

        //Farming data 
        save.farmSaveState.LoadData();

        //Player Stats
        save.playerSaveState.LoadData();

        //Relationship stats
        AnimalStats.LoadStats();
        RelationshipStats.LoadStats(); 
        //save.relationshipSaveState.LoadData();

    }
}

PlayerStats.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerStats 
{
    public static int Money { get; private set; }
    public static int Stamina;

    public const string CURRENCY = "G"; 

    public static void Spend(int cost)
    {
        //Check if the player has enough to spend
        if(cost > Money)
        {
            Debug.LogError("Player does not have enough money");
            return; 
        }
        Money -= cost;
        UIManager.Instance.RenderPlayerStats(); 
        
    }

    public static void Earn(int income)
    {
        Money += income;
        UIManager.Instance.RenderPlayerStats();
    }

    public static void LoadStats(int money, int stamina)
    {
        Money = money;
        Stamina = stamina;
        UIManager.Instance.RenderPlayerStats();
    }

    public static void UseStamina(int staminaLost)
    {
        Stamina -= staminaLost;
        UIManager.Instance.RenderPlayerStats();
    }

    public static void RestoreStamina()
    {
        Stamina = 150;
        UIManager.Instance.RenderPlayerStats();
    }
}

AnimEventsTools.cs

using UnityEngine;

public class AnimEventsTools : MonoBehaviour
{
    // this script contains the anim events for the spawing of tools when the animation is played

    // this is the original objects of the tools 
    public GameObject wateringCan;
    public GameObject Hoe;

    private GameObject newWateringCan;
    private GameObject newHoe;

    public Transform handPoint;
    public void SpawnWaterCan()
    {
        newWateringCan = Instantiate(wateringCan, handPoint);
    }

    public void DespawnWaterCan()
    {
        Destroy(newWateringCan);
    }

    public void SpawnHoe()
    {
        newHoe = Instantiate(Hoe, handPoint);
    }

    public void DespawnHoe()
    {
        Destroy(newHoe);
    }
}

To view this content, you must be a member of Terresqualls Patreon at $5 or more
Already a qualifying Patreon member? Refresh to access this content.