Featured Projects

Acolyte of the Altar

Acolyte of the Altar

Summary

In this roguelike deckbuilder, you play the role of an Acolyte traversing the wasteland with the goal of sacrificing yourself at the Altar of your primary patron deity. Use your cards to defeat opposing monsters along the way and build a deck that can outlast the dangers you encounter!

Roles

Content Designer and Game Developer

Development

I was brought on to the Black Kite Games team after they released their debut title in order to flesh out the content after launch. With nearly a hundred cards and 19 relics that could modify the player's strategy, I had a solid base to build upon when designing and adding additional cards to the game.

My work involved designing original card mechanics and implementing them using the existing project architecture, modifying the codebase as needed to accomplish my goals. The intent was to come up with alternate variants of each of the existing cards in the game, ensuring their overall balance in the context of the game's meta while guaranteeing their status as fun to play with.

There are three Patrons to choose from when playing the game, and each of their cards had to align with a particular play style; the Empiricist's strategy is to build up Borrowed Life and outlast their opponents, while the Ravagers take a more aggressive approach, with the intent to demolish their opponents quickly. As for the Sylvans, they tended to take a more moderate approach, building up larger masses and casting more spells to gain the upper hand.

Play

Gameplay

With heavy inspiration from card battlers like Hearthstone and Legends of Runeterra, Acolyte of the Altar puts emphasis on its combat. Its three patrons divide a player's strategic choices evenly between aggro, tempo, and control, with each one containing multiple approaches towards victory.

After looting a new card, one must transmute an existing card in their deck to make room for it, ensuring that their deck size stays the same throughout a run. Many of the cards are accompanied by unique visual and sound effects, making the play experience satisfying and immersive.

After completing one run, higher difficulty options are unlocked, guaranteeing players a high degree of replayability.

Screenshots

acolyte of the altar gameplayacolye of the altar gameplay

Sweetlads' Werewolf

Sweetlads' Werewolf

Summary

A networked multiplayer casual social deduction Werewolf-style game that integrates Steam's inventory service to provide customizable cosmetics and earnable items. Its core design ethos is to provide a chat room in which people can hang out and converse while sussing out the evil players from the villagers.

Roles

Game Developer and Steamworks Integrator

Development

I integrated most of the backend Steamworks functionality to provide players with the ability to obtain items and wear them using a cosmetics system. Using Mirror, we synchronized the visuals of players' outfits and animations so that they could proudly display their chosen cosmetics. Additionally, I integrated a custom wear shader to give each item a completely unique texture.

Gameplay

Use your intuition (or your special role ability) to uncover the Werewolves in the village, or if you are a Werewolf, stay out of the spotlight in order to kill the majority of the Villagers, in order to win the game! Special roles include a Seer who can see someone's role, a Bodyguard who can save someone from being killed, a Martyr who can die in place of someone else, and a Survivor who, well, survives one attack against them.

Play

Screenshots

acolyte of the altar gameplayacolye of the altar gameplay

Scarred Mansion

Scarred Mansion

Summary

Scarred Mansion is a strategy game in which you craft cards in your deck with components you earn and use those cards in the hopes of overcoming encounters with dangerous creatures and hazardous obstacles on your way to escape a haunted house.

Roles

Creative Lead, Game Designer, and Developer

Development

The core concept of this project stemmed from my desire to make a non-combative, systemic deck-building game in which the player has the power to shape the environment of a grid-based terrain using the cards in their deck. What would make this game mechanically unique from the other deck-building roguelikes on the market would be its approach to crafting decks. Rather than adding and removing cards from one deck built by the player, they would maintain a twelve-card deck and change it by adding and removing components from the cards themselves. Through extensive playtesting and iteration with the goal of "finding the fun", I cut most of the systemic mechanics in order to focus on the narrative and character development of a twelve-year-old boy's journey through a run-down mansion.

After deciding on the core gameplay loop based on a deck-building roguelike structure, I designed the encounters, hazards, and enemies around the core pillars of Grid Traversal, Exploration, Deck Manipulation, and Card Manipulation. Grid Traversal includes mechanics such as moving objects in the world from one grid space to another as well as placing and removing objects from grid spaces. Exploration indicates the action of searching grid spaces (or un-searching) and increasing or decreasing the default search radius of the main character. Deck Manipulation features game mechanics such as drawing or discarding cards from the player's hand or deck of cards or modifying the cost or playability of a card. Card Manipulation refers to the action of adding and removing components from cards as well as changing the effect of cards based on the board state of the game.

Play

Gameplay

A deck-building roguelike game in which each encounter takes place on an isometric grid and the goal is to explore a certain percentage of the area in order to find your way out of the mansion. Each level has procedural hazards that you must overcome on your way towards the exit. The main mechanical difference between this game and most other conventional deck-building games is that instead of adding and removing cards from their deck, the player maintains the same number of cards in their deck and instead adds and removes components from cards themselves.

Screenshots

scarred mansion gameplayscarred mansion gameplay

Hibotchi

Hibotchi

Inspiration

This game was born of a desire to make Tetris into a dungeon-crawling deck-building game with roguelike elements like Slay the Spire.

Gameplay

After prototyping my first version of the game, where I restricted tetromino movement to match held cards, I realized that completing lines with this system in place became much harder. To adapt, I combined this idea with a cooking game I had in mind. I transformed the falling tetrominos into food and changed the goal to fulfilling customer orders by merging nearby food blocks.

Roles

Lead Game Designer, Core Programmer, Art Director, Pixel Artist, Sound Designer

Design & Development

The main tasks involved were developing a robust ScriptableObject and GameEvent-based project architecture in Unity, creating unique gameplay mechanics for each chef class, optimizing the code for small file size and fast loading times, and designing a player progression curve to encourage daily engagement. To improve player retention, I introduced unlockable cards that rewarded users for completing games. Additionally, I implemented optional video reward ads to expedite card unlocks, with a 24-hour waiting period between each unlock using this method.

During development, I noticed that there were usually very few customers in the mid-to-late game, resulting in an accumulation of unused food at the bottom of the player's grid. To mitigate this issue, I implemented a solution where playing a card would deduct one second from the customer arrival timer. This straightforward adjustment ensured that more customers arrived when the player's hand was full and their restaurant was empty.

Based on playtesting and the principle of "following the fun," I made the significant design decision to alter the timing of card drawing. Instead of drawing a card when a food landed at the bottom of the grid, I changed it to when a food was instantiated at the top. I implemented this change to address situations where a food failed to reach the bottom or got destroyed, resulting in the player not receiving a card to guide the next spawned food. This could be frustrating, especially when the player had an empty hand and a new food appeared, leaving them with no actions to perform. The new approach ensured that players could take at least one action on each falling food, even if their hand was empty when it spawned.

Read the full Game Design Document here.

Tools Used

Unity Game Engine, Visual Studio Code, Adobe Photoshop, GIMP, Shotcut, Adobe Premiere, Adobe Audition

Screenshots

Hibotchi Screenshot



Narrative

You play as a hibachi chef desiring to feed as many customers as possible in order to gain more experience to become a better chef.

Code Sample

The most common type of ScriptableObject I used in this project was the "Card".
										
	public class Card : ScriptableObject
	{
		// Each asset has a unique resource ID so it can be saved and loaded correctly.
		[SerializeField] private string uniqueResourceID;
		public string UniqueResourceID { get => uniqueResourceID; }
	
		// Effects are ScriptableObjects and are assigned here to determine what the card will do when played (and when played with a doubling effect, and when a new effect is added)
		private Effect[] effects;
		public Effect[] Effects { get => effects; }
	
		private Effect[] doubleEffects;
		public Effect[] DoubleEffects { get => doubleEffects; }
	
		private Effect[] effectsToAdd;
		public Effect[] EffectsToAdd { get => effectsToAdd; }
	
		private string cardName;
		public string CardName { get => cardName; }
	
		private string cardDescription;
		public string CardDescription { get => cardDescription; }
	
		private string effectDescription;
		public string EffectDescription { get => effectDescription; }
	
		// Drag and drop these from the Inspector in order to assign them correctly
		[Header("Scene References")]
		public TextMeshProUGUI cardText;
		public Canvas cardCanvas;
		public Color imageColor;
	
		public enum ChefClass
		{
			Balanced,
			Aggressive,
			Methodical,
			Quirky
		}
	
		public enum Rarity
		{
			Common,
			Uncommon,
			Rare
		}
		
		// Each card is assigned one or more Chef Classes to ensure it only can be used by a player of that class
			public List<ChefClass> chefClasses;
		public Rarity rarity;
	
		// Set these to tell it which animation and sound to play when it is played
		[SerializeField] private string animationTrigger;
		public string AnimationTrigger { get => animationTrigger; }
	
		[SerializeField] private string soundName;
		public string SoundName { get => soundName; }
	
		// Index is to determine the sprite index of its art from the main effect sprite sheet
		public int spriteIndex;
	
		public bool isShuffle, isPersistent, hasTrail, performEffectsOnce, isMovement, isUnlocked;
				
		public GameObject card;
		CardScript cs;
	
		public GameObject CardObject()
		{
			GameObject cardWithValues = Instantiate(Resources.Load("Prefabs/Card Prefab", typeof(GameObject))) as GameObject;
			
			// Load card art from public library of resource.loaded sprites based on index on sprite sheet
			Sprite image;
			
			if (PublicLibrary.Instance && spriteIndex < PublicLibrary.Instance.effectSprites.Length)
			{
				image = PublicLibrary.Instance.effectSprites[index];
			}
			else
			{
				image = Resources.Load("Sprites/Placeholder Sprite", typeof(Sprite)) as Sprite;
			}
	
			// Set values for CardScript
			cs = cardWithValues.GetComponent<CardScript>();
			cs.cardName = cardName;
			cs.card = this;
			cs.cardNameText.text = cardDescription;
			cs.effects = effects.Clone() as Effect[];
			cs.doubleEffects = doubleEffects.Clone() as Effect[];
			cs.cardImageRenderer.sprite = image;
			cs.cardImageRenderer.color = imageColor;
			cs.animTrigger = animTrigger;
			cs.hasTrail = hasTrail;
			cs.performEffectOnce = performEffectsOnce;
			cs.glow.SetActive(false);
	
			if (string.IsNullOrEmpty(SoundName))
			{
				cs.soundName = "Whoosh";
			}
			else
			{
				cs.soundName = soundName;
			}
	
			LerpAlpha lerpAlpha = cardWithValues.GetComponentInChildren<LerpAlpha>();
			
			if (isPersistent)
			{
				cs.canvasColor = new Color32(217, 206, 186, 255);
				lerpAlpha.enabled = false;
			}
			else
			{
				cs.canvasColor = new Color32(117, 117, 117, 255);
				lerpAlpha.enabled = true;
			}
	
			return cardWithValues;
		}
	}
										
									
Caldera

Caldera

Inspiration

Caldera is a puzzle game in which the goal is to defeat enemies by moving tiles around. One crucial pillar of game design was to ensure that each level could be solved with multiple solutions. Into The Breach was the primary inspiration.

Gameplay

Tap adjacent tiles to move on a grid, moving tiles in your row or column the chosen direction, similar to 2048. The catch was that there were enemies, and to defeat them, the player had to push them off of the edge of the grid.

Roles

Lead Level Designer, Sound Designer, Programmer

Development

The first part of the process was designing enemies. We had one enemy that moved one space toward the player each turn (using the A* pathfinding algorithm) and attacked one space in front of it, and we immediately knew that we could take that one step further by creating an enemy that shot a beam to each tile in front of it rather than just one.

The next step involved creating the terrain, starting with movable sandy land and then progressing to non-movable mountainous land. We also introduced chasms, which acted like the ocean at the grid edges but were located within the land. The levels are intended to showcase each enemy and land type individually, gradually integrating new mechanics with previously introduced ones. We thoroughly tested each level to ensure validity and multiple potential solutions.

Tools Used

Unity Game Engine, Visual Studio Code, Aseprite, Adobe Creative Suite, XCode, Github

Play

Media

Caldera Screenshot

Code Sample

This script dynamically spawns a button onto a Canvas for each Level in the build, then restricts access to only unlocked levels.
											
	public class LevelSelectionManagerScript : MonoBehaviour
	{
		public GameObject levelButton;
		public GameObject portraitContent;
		public GameObject landscapeContent;
		public MainMenuScript mms;
	
		public void LoadLevels()
		{
			if (DataSaver.instance != null)
			{
				DataSaver.instance.LoadData();
	
				for (int i = 0; i < DataSaver.instance.completedLevels.Length; i++)
				{
					GameObject b = Instantiate(levelButton) as GameObject;
	
					if (Camera.main.aspect > 1)
					{
						b.transform.SetParent(landscapeContent.transform);
					}
					else
					{
						b.transform.SetParent(portraitContent.transform);
					}
	
					int levelToLoad = i + 1;
					
					string path = SceneUtility.GetScenePathByBuildIndex(levelToLoad);
					string sceneName = path.Substring(0, path.Length - 6).Substring(path.LastIndexOf('/') + 1);
					string loadLevel = sceneName;
					
					b.transform.localScale = Vector3.one;
	
					Button bButton = b.GetComponent<Button>();
					TextMeshProUGUI bTMP = b.GetComponentInChildren<TextMeshProUGUI>();
					bButton.onClick.AddListener(() => mms.LoadSelectedScene(levelToLoad));
					bButton.interactable = DataSaver.instance.completedLevels[i];
	
					bTMP.text = loadLevel;
					bTMP.fontSize = 14;
				}
			}
			else if (PlayerPrefsSave.instance != null)
			{
	
				PlayerPrefsSave.instance.Load();
	
				for (int i = 0; i < PlayerPrefsSave.instance.totalLevels; i++)
				{
					GameObject b = Instantiate(levelButton) as GameObject;
	
					if (Camera.main.aspect > 1)
					{
						b.transform.SetParent(landscapeContent.transform);
					}
					else
					{
						b.transform.SetParent(portraitContent.transform);
					}
	
					int levelToLoad = i + 1;
					string path = SceneUtility.GetScenePathByBuildIndex(levelToLoad);
					string sceneName = path.Substring(0, path.Length - 6).Substring(path.LastIndexOf('/') + 1);
					string loadLevel = sceneName;
	
					b.transform.localScale = new Vector3(1, 1, 1);
	
					Button bButton = b.GetComponent<Button>();
					TextMeshProUGUI bTMP = b.GetComponentInChildren<TextMeshProUGUI>();
					bButton.onClick.AddListener(() => mms.LoadSelectedScene(levelToLoad));
					if (i <= PlayerPrefsSave.instance.levelsCompleted)
					{
						bButton.interactable = true;
					}
					else
					{
						bButton.interactable = false;
					}
	
					bTMP.text = loadLevel;
					bTMP.fontSize = 14;
				}
			}
		}
	}