Creating some basic npc entity

Now that we have a player character with some basic FOV implementation and dungeon exploration, it is time to add some npcs to encounter and battle.
For this part we’ll focus on adding a “Goblin” npc and reworking the stats of the actor class to be a bit more broader.

First we’re going to create a new class “ActorStats” this will be the container for our stats like health, atk, def, dodge, etc..

internal sealed class ActorStats
{
	private int _health = 1;
	public int Health
	{
		get => _health;
		set => _health = Math.Max(0, Math.Min(value, MaxHealth));
	}

	public int MaxHealth { get; private set; }
	public int Attack { get; private set; } = 1;
	public int Defense { get; private set; } = 0;
	public int DodgeChance { get; private set; } = 0;
	public int CritChance { get; private set; } = 0;

	public ActorStats(int maxHealth)
	{
		MaxHealth = Math.Max(1, maxHealth);
        Health = MaxHealth;
	}

	public void Set(int? atk = null, int? def = null, int? dodge = null, int? crit = null)
	{
		Attack = Math.Max(1, atk ?? Attack);
		Defense = Math.Max(0, def ?? Defense);
		DodgeChance = Math.Max(0, Math.Min(dodge ?? DodgeChance, 50)); // 50% max dodge chance
		CritChance = Math.Max(0, crit ?? CritChance);
	}
}

Basically we’re providing properties for MaxHealth, Health, Attack, Defense, DodgeChance, CritChance

All these properties are public getter, but private setters. This so we can control
how the values are set. We control these using a method to set each of the attributes we want to be able to be modified.
In this case it will be attack, defense, dodge and crit chance.
All these have a min value of 0 except attack which has a min value of 1.
Dodge chance has a maximum value of 50

MaxHealth will not be changeable, and Health property can directly be modified. But it cannot go under 0.

Lets adjust our actor class by adding this new ActorStats class and initializing it.
Additionally we will remove the MaxHealth and Health properties from Actor class:

public ActorStats Stats { get; }
public bool IsAlive => Stats.Health > 0;

protected Actor(Color foreground, Color background, int glyph, int zIndex, int maxHealth) : base(foreground, background, glyph, zIndex)
{
	Stats = new ActorStats(maxHealth);
}

I’ve created an extra folder “Actors” inside of the “Entities” folder, this is where I will store all the Actor entities. I’ve moved the Player.cs into this folder, and we’ll be making our Goblin actor also into this folder.

Lets start with creating a new class “Goblin” inside the “Actors” folder which will inherit from our Actor class:

internal class Goblin : Actor
{
	public Goblin(Point position) : 
		base(Color.Green, Color.Transparent, 'g', 1, maxHealth: 5)
	{
		Name = "Goblin";
		Position = position;

		// Set base stats
		Stats.Set(atk: 2, dodge: 10);
	}
}

Here we define Goblin actor, and we’ll give it the same Name for logging later.
We also set its position and we give it some default stats. Since goblins are generally weak and agile, we’ll give it an atk value of 2 and some minor dodge chance.

Lets also make sure the player has a name:

public Player(Point position) : base(Color.White, Color.Transparent, '@', zIndex: int.MaxValue, maxHealth: 100)
{
	Name = "Player";

	// Setup FOV map
	var tilemap = ScreenContainer.Instance.World.Tilemap;
	FieldOfView = new RecursiveShadowcastingFOV(new LambdaGridView<bool>(tilemap.Width, tilemap.Height,
		(point) => !BlocksFov(tilemap[point.X, point.Y].Obstruction)));

	IsFocused = true;
	PositionChanged += Player_PositionChanged;
	Position = position;
}

Now to spawn the goblins in the world we’ll need some kind of method, we’ll do that in the WorldScreen were we also create our player. I think a simple way of spawning them, is to spawn between 0 and 2 goblins in each room.

We can do this by looping over each room, taking the positions of the room within the walls (perimeter) and also (excluding the player position) and taking a random position from that list and using that as our spawn position. Later then removing that position from the list to attempt to spawn more per room and not getting the same position again. Here is how that would look:

public void CreateNpcs()
{
	const int maxNpcPerRoom = 2;
	foreach (var room in _dungeonRooms)
	{
		// Define how many npcs will be in this room
		var npcs = ScreenContainer.Instance.Random.Next(0, maxNpcPerRoom + 1);

		// All positions within the room except the perimeter positions and the player position
		var validPositions = room.Positions()
			.Except(room.PerimeterPositions().Append(Player.Position))
			.ToList();

		for (int i=0; i < npcs; i++)
		{
			// Select a random position from the list
			var randomPosition = validPositions[ScreenContainer.Instance.Random.Next(0, validPositions.Count)];

			// Create the goblin npc with the given position and add it to the actor manager
			var goblin = new Goblin(randomPosition);
			ActorManager.Add(goblin);

			// Make sure we don't spawn another at this position
			validPositions.Remove(randomPosition);
		}
	}
}

In our Program.cs in the GameStart method lets call our new CreateNpcs method:

private static void GameStart(object sender, GameHost e)
{
	var world = ScreenContainer.Instance.World;
	world.Generate();
	world.CreatePlayer();
	world.CreateNpcs();
}

Now we will see npcs spawn in the world:

We’ll have to fine tune their visibility next, so they only show when the player sees them in their field of view.

We can do this by checking for each actor if they’re in the player’s field of view or not and setting their IsVisible property which determines if SadConsole will render them or not. We can start by adding a method inside the ActorManager:

public void UpdateVisibility(IFOV fieldOfView = null)
{
	var fov = fieldOfView ?? ScreenContainer.Instance.World.Player.FieldOfView;
	foreach (var actor in _actors)
	{
		actor.Value.IsVisible = fov.BooleanResultView[actor.Key];
	}
}

Lets call this method inside the Player.cs when he moves, right after we calculate the new FOV.

private void Player_PositionChanged(object sender, ValueChangedEventArgs<Point> e)
{
	// Calculate the field of view for the player's position
	FieldOfView.Calculate(e.NewValue, FovRadius);

	// Update the visibility of actors
	ScreenContainer.Instance.World.ActorManager.UpdateVisibility(FieldOfView);

	// Explore the dungeon tiles
	ExploreTilemap();
}

We should also call this method right after we CreateNpcs() in WorldScreen.cs so that the initial visibility is set properly.

public void CreateNpcs()
{
	const int maxNpcPerRoom = 2;
	foreach (var room in _dungeonRooms)
	{
		// Define how many npcs will be in this room
		var npcs = ScreenContainer.Instance.Random.Next(0, maxNpcPerRoom + 1);

		// All positions within the room except the perimeter positions and the player position
		var validPositions = room.Positions()
			.Except(room.PerimeterPositions().Append(Player.Position))
			.ToList();

		for (int i=0; i < npcs; i++)
		{
			// Select a random position from the list
			var randomPosition = validPositions[ScreenContainer.Instance.Random.Next(0, validPositions.Count)];

			// Create the goblin npc with the given position and add it to the actor manager
			var goblin = new Goblin(randomPosition);
			ActorManager.Add(goblin);

			// Make sure we don't spawn another at this position
			validPositions.Remove(randomPosition);
		}
	}

	// Update the visibility of actors
	ScreenContainer.Instance.World.ActorManager.UpdateVisibility();
}

Now when you run the game, you will see the goblins are now correctly visible only if the player can see them.

Checkout the next article to continue with the series!

You can find all the code for this series down in the repository here:
https://github.com/Ven0maus/Code2DTutorials/tree/Tutorials/Roguelike

Leave a comment

Design a site like this with WordPress.com
Get started