Hallowfall is a project created as the groundwork for a 2D roguelike game. Built upon systems adapted from my Hollow Omen project, its primary focus is to refine and scale both the player and enemy controllers into more modular and reusable components. The initial goal was to improve existing mechanics, increase code maintainability, and establish a solid base upon which roguelike gameplay systems could be implemented and expanded ranging from procedural combat interactions to diverse abilities and enemy AI.
public interface IPlayerState
{
void InitState(PlayerConfig config);
void OnEnterState();
void OnExitState();
void HandleState();
void SetOnInitializeVariables(PlayerController statesManagerRef);
}
public class PlayerBaseState : MonoBehaviour, IPlayerState
{
protected PlayerStateEnum stateEnum;
protected PlayerController playerController;
public PlayerBaseState()
{
}
public virtual void InitState(PlayerConfig config)
{
}
public virtual void Start()
{
InitState(playerController.PlayerConfig);
}
public void SetOnInitializeVariables(PlayerController statesManagerRef)
{
this.playerController = statesManagerRef;
}
public PlayerStateEnum GetStateEnum()
{
return stateEnum;
}
public virtual void OnEnterState()
{
}
public virtual void OnExitState()
{
}
public virtual void HandleState()
{
}
}
public class PlayerRunState : PlayerBaseState
{
private float runSpeed = 0;
private AudioSource audioSource;
private AudioClip[] groundRunSFX;
private AudioClip[] grassRunSFX;
private AudioClip[] stoneRunSFX;
public float RunSpeed { get => runSpeed; set => runSpeed = value; }
public PlayerRunState()
{
this.stateEnum = PlayerStateEnum.Run;
}
public override void InitState(PlayerConfig config)
{
runSpeed = config.runSpeed;
groundRunSFX = config.groundSFX;
grassRunSFX = config.grassSFX;
stoneRunSFX = config.stoneSFX;
}
private void Awake()
{
audioSource = GetComponent();
}
public override void OnEnterState()
{
StartRunning();
}
public override void OnExitState()
{
StopRunning();
}
public override void HandleState()
{
}
public void PlayStepSound()
{
switch (playerController.CurrentFloorType)
{
case FloorTypeEnum.Ground:
AudioManager.Instance.PlayRandomSFX(audioSource, groundRunSFX, 0.1f);
break;
case FloorTypeEnum.Grass:
AudioManager.Instance.PlayRandomSFX(audioSource, grassRunSFX, 0.1f);
break;
case FloorTypeEnum.Stone:
AudioManager.Instance.PlayRandomSFX(audioSource, stoneRunSFX, 0.1f);
break;
}
}
private void StartRunning()
{
playerController.PlayerMovementManager.MoveSpeed = RunSpeed;
playerController.AnimationController.SetBoolForAnimations("isRunning", true);
}
private void StopRunning()
{
playerController.AnimationController.SetBoolForAnimations("isRunning", false);
}
public void PauseRunningSFX()
{
if (audioSource.isPlaying)
{
audioSource.Pause();
}
}
public void ResumeRunningSFX()
{
if (!audioSource.isPlaying)
{
audioSource.UnPause();
}
}
}
public class BaseAbility : ScriptableObject
{
public string abilityName;
public string description;
public Sprite icon;
public bool canLevel;
public virtual void CallAbility()
{
UIManager.Instance.AddAbilitySlot(this);
}
}
public class ActiveAbility : BaseAbility
{
public GameObject handlerPrefab;
public PassiveAbility[] supportAbilities;
public override void CallAbility()
{
GameObject handlerGO = Instantiate(handlerPrefab, GameManager.Instance.Player.transform.Find("AbilityHolder"));
ActiveAbilityHandler handler = handlerGO.GetComponent();
foreach (PassiveAbility ability in supportAbilities)
{
LevelupManager.Instance.abilities.Add(ability);
}
LevelupManager.Instance.abilities.Remove(this);
base.CallAbility();
}
}
public class PassiveAbility : BaseAbility
{
public float modifier;
public delegate void PassiveAbilityEvent();
public event PassiveAbilityEvent passiveAbilityEvent;
public override void CallAbility()
{
passiveAbilityEvent?.Invoke();
base.CallAbility();
}
}
public class SkillSO : ScriptableObject
{
public string skillName;
public string skillDescription;
public Sprite icon;
public int skillCost;
public int id;
public virtual void ApplySkill(PlayerController playerController)
{
//Call Event From SkillEvents
}
}
public static class SkillEvents
{
//
public static event Action OnDoubleDashSkillUnlocked;
public static void UnlockDoubleDash() => OnDoubleDashSkillUnlocked?.Invoke();
//
//
public static event Action OnCounterSkillUnlocked;
public static void UnlockCounter() => OnCounterSkillUnlocked?.Invoke();
//
//
public static event Action OnPerfectTimingSkillUnlocked;
public static void UnlockPerfectTiming() => OnPerfectTimingSkillUnlocked?.Invoke();
//
//
public static event Action OnEchoingSteelSkillUnlocked;
public static void UnlockEchoingSteel() => OnEchoingSteelSkillUnlocked?.Invoke();
//
//
public static event Action OnMomentumShiftSkillUnlocked;
public static void UnlockMomentumShift() => OnMomentumShiftSkillUnlocked?.Invoke();
//
//
public static event Action OnCounterSurgeSkillUnlocked;
public static void UnlockCounterSurge() => OnCounterSurgeSkillUnlocked?.Invoke();
//
//
public static event Action OnBladeReflectionSkillUnlocked;
public static void UnlockBladeReflection() => OnBladeReflectionSkillUnlocked?.Invoke();
//
}
public class SkillNode : MonoBehaviour
{
[Header("Node Dependencies")]
[SerializeField] private SkillNode[] previousNodes;
[SerializeField] private SkillNode[] nextNodes;
[SerializeField] private SkillSO skillSO;
private SkillTreeManager skillManager;
[Header("Visuals")]
[SerializeField] private Image outlineImage;
[SerializeField] private Transform linkHolder;
[SerializeField] private Image skillImage;
private List links = new List();
private bool isUnlocked = false;
private bool canBeUnlocked = false;
public bool IsUnlocked { get => isUnlocked; private set => isUnlocked = value; }
private void Awake()
{
skillManager = GetComponentInParent();
foreach (Transform child in linkHolder)
{
Image image = child.GetComponent();
if (image != null)
{
links.Add(image);
}
}
}
private void Start()
{
CheckPreviousNodes();
skillImage.sprite = skillSO.icon;
}
public void CheckPreviousNodes()
{
bool allUnlocked = true;
foreach (var node in previousNodes)
{
if (!node.IsUnlocked)
{
allUnlocked = false;
break;
}
}
canBeUnlocked = allUnlocked && !isUnlocked;
UpdateNodeVisuals();
}
public void Unlock()
{
IsUnlocked = true;
canBeUnlocked = false;
foreach (var node in nextNodes)
{
node.CheckPreviousNodes();
}
UpdateLinkColors(true);
skillManager.UpdateSkullsText();
UpdateNodeVisuals();
}
private void UpdateLinkColors(bool shouldHighlight)
{
Color targetColor = shouldHighlight ? new Color(1f, 0.5f, 0f) : Color.white;
foreach (var link in links)
{
link.color = targetColor;
}
}
private void UpdateNodeVisuals()
{
if (isUnlocked)
{
skillImage.color = Color.white;
outlineImage.color = Color.green;
}
else if (canBeUnlocked)
{
skillImage.color = new Color(1, 1, 1, 0.7f);
outlineImage.color = Color.yellow;
}
else
{
skillImage.color = new Color(1, 1, 1, 0.1f);
outlineImage.color = Color.red;
}
}
public void OnNodeClicked()
{
if (!canBeUnlocked || isUnlocked) return;
int newSkullCount = skillManager.LoadSkullCount() - skillSO.skillCost;
if (newSkullCount < 0) return;
Unlock();
SaveSystem.UpdatePlayerSkulls(newSkullCount);
skillManager.UpdateSkullsText();
SaveSystem.UpdateSkillTree(skillSO.id, true);
}
public void OnSkillHover()
{
skillManager.ShowDescriptionFrame(transform.position, skillSO.skillName, skillSO.skillDescription, skillSO.skillCost);
}
public void OnSkillHoverClear()
{
skillManager.HideDescriptionFrame();
}
public void ResetSkill()
{
isUnlocked = false;
canBeUnlocked = false;
UpdateLinkColors(false);
CheckPreviousNodes();
SaveSystem.UpdateSkillTree(skillSO.id, false);
}
public void UnlockBasedOfSkillTreeData(int[] skillTreeNodesData)
{
if (skillSO.id >= 0 && skillSO.id < skillTreeNodesData.Length && skillTreeNodesData[skillSO.id] == 1)
{
Unlock();
}
}
}
public class SkillTreeManager : MonoBehaviour
{
[Header("UI Dependencies")]
[SerializeField] GameObject skillDescriptionFrame;
[SerializeField] TextMeshProUGUI descriptionText;
[SerializeField] TextMeshProUGUI nameText;
[SerializeField] TextMeshProUGUI costText;
[SerializeField] TextMeshProUGUI resetCostText;
[SerializeField] TextMeshProUGUI skullText;
private SkillNode[] skillNodes;
[SerializeField] private int resetCost = 100;
public SkillNode[] SkillNodes { get => skillNodes; }
private void Awake()
{
skillNodes = GetComponentsInChildren();
}
private void Start()
{
resetCostText.text = resetCost.ToString();
}
public void ShowDescriptionFrame(Vector3 pos, string name, string description, int cost)
{
skillDescriptionFrame.transform.position = pos + new Vector3(130, 70, 0);
nameText.text = name;
descriptionText.text = description;
costText.text = cost.ToString();
skillDescriptionFrame.SetActive(true);
}
public void HideDescriptionFrame()
{
skillDescriptionFrame.SetActive(false);
}
public void ResetAllSkills()
{
int temp = LoadSkullCount() - resetCost;
if (temp >= 0)
{
foreach (SkillNode skillNode in SkillNodes)
{
skillNode.ResetSkill();
}
SaveSystem.UpdatePlayerSkulls(temp);
UpdateSkullsText();
}
}
public int LoadSkullCount()
{
GameData gameData = SaveSystem.LoadGameData();
if (gameData != null)
{
return gameData.skullCount;
}
else return 0;
}
public void UpdateSkullsText()
{
skullText.text = LoadSkullCount().ToString();
}
public void ApplySavedSkillTree(int[] savedData)
{
foreach (var node in SkillNodes)
{
node.UnlockBasedOfSkillTreeData(savedData);
}
}
}