D20 RPG – Hero Party

“It’s party time, it’s excellent!” – Wayne, Wayne’s World

Overview

In this lesson, I would like to start migrating away from the “Solo Adventure” idea toward a game that supports a full hero party. Toward that end, I will remove the old SoloHeroSystem and then add features to aid in constructing a full hero party. We will make sure that everything works with our encounter, and since the “story” part of the game hasn’t gone away I will probably use a “party leader” as if it were the old “solo” hero for any stat based checks.

Getting Started

Feel free to continue from where we left off, or download and use this project here. It has everything we did in the previous lesson ready to go.

Party Order System

In addition to knowing the party of an entity, it will be helpful to know an “order” of membership. I can imagine this being useful in menus, or to determine who is “active” in a battle (if an encounter uses less than the entire roster). Even in our prototype it will already be useful to make sure the heroes populate on the board in a consistent pattern.

Create a new script at Scripts -> Component named PartyOrderSystem and add the following:

public partial class Data
{
    public CoreDictionary<Entity, int> partyOrder = new CoreDictionary<Entity, int>();
}

public interface IPartyOrderSystem : IDependency<IPartyOrderSystem>, IEntityTableSystem<int>
{
    Entity PartyLeader { get; }
}

public class PartyOrderSystem : EntityTableSystem<int>, IPartyOrderSystem
{
    public override CoreDictionary<Entity, int> Table => IDataSystem.Resolve().Data.partyOrder;

    public Entity PartyLeader
    {
        get
        {
            foreach (var entity in Table.Keys)
            {
                if (entity.PartyOrder == 0)
                    return entity;
            }
            return Entity.None;
        }
    }
}

public partial struct Entity
{
    public int PartyOrder
    {
        get { return IPartyOrderSystem.Resolve().Get(this); }
        set { IPartyOrderSystem.Resolve().Set(this, value); }
    }
}

For the most part this follows the same pattern as any other EntityTableSystem based class we have made in the past. One difference is the addition of a “PartyLeader” property which loops over the entities in the table looking for an Entity whose order is ‘0’. I use ‘0’ as the “first” party index because it matches up with 0-based indexing for arrays etc. Note that this could be optimized by saving the party leader as state, but state has to be maintained whereas the search will always be correct. Since the hero party is likely to be small, I am not too worried about the minor optimization.

To use this class we will of course have to inject it. Modify the ComponentInjector to inject the following:

IPartyOrderSystem.Register(new PartyOrderSystem());

Create Hero Party Flow

Next we will add a new flow that can handle creating the various party members. There are countless ways this could be accomplished including allowing the player great control over picking things like their Ancestry, Background, Class, etc. That customization could be limited to the “main” character or could be extended to the entire hero party. On the other hand, you could also hide all of that and just give the player a pre-specified set of heroes. Regardless of which path you want to take, this flow will be considered the entry point and will handle the order of creation. By the end of the flow, a full hero party will have been created.

Create a new script at Scripts -> Flow named CreateHeroPartyFlow and add the following:

using Cysharp.Threading.Tasks;

public interface ICreateHeroPartyFlow : IDependency<ICreateHeroPartyFlow>
{
    UniTask Play();
}

public class CreateHeroPartyFlow : ICreateHeroPartyFlow
{
    const int heroPartySize = 4;

    public async UniTask Play()
    {
        for (int i = 0; i < heroPartySize; ++i)
        {
            var entity = await IEntityRecipeSystem.Resolve().Create("Hero");
            entity.PartyOrder = i;
        }
        await UniTask.CompletedTask;
    }
}

To use this class it also has to be injected. Add the following to the FlowInjector:

ICreateHeroPartyFlow.Register(new CreateHeroPartyFlow());

Solo Hero System

Now that we have a “Party Leader” we don’t need a “Solo Hero”, so let’s go ahead and delete the SoloHeroSystem and the accompanying SoloHeroSystemTests files. Doing so will cause a host of errors to appear in the console, but that is ok, it actually helps us find all the places that this system was used and therefore what all needs to be fixed. We will need to fix the following files:

  • GameSystem
  • HeroActionFlow
  • EncounterSystem
  • SkillExploreEntryOption
  • SoloAdventureInjector
  • ActionMenu

Game System

Open the GameSystem. On line 16 we will want to make the following change:

// Replace this:
await ISoloHeroSystem.Resolve().CreateHero();

// With this:
await ICreateHeroPartyFlow.Resolve().Play();

This will swap from creating the solo hero to instead create our whole hero party.

Hero Action Flow

Open the HeroActionFlow. On line 12 we will want to make the following change:

// Replace this:
var hero = ISoloHeroSystem.Resolve().Hero;

// With this:
var hero = ITurnSystem.Resolve().Current;

Before, we had only a single hero, and so we always knew exactly which Entity should act. Now, we will need to get the currently active Entity from the TurnSystem so we know which of the heroes to manage.

Encounter System

Open the EncounterSystem. Line 12 has the issue, but we will actually swap out a bit more:

// Replace all of this:
var hero = ISoloHeroSystem.Resolve().Hero;
hero.Position = encounter.HeroPositions[0];
await CreateView(hero, heroPath);

// With this:
foreach (var entity in IPartyOrderSystem.Resolve().Table.Keys)
{
    var hero = entity;
    if (hero.PartyOrder >= encounter.HeroPositions.Count)
        continue;
    hero.Position = encounter.HeroPositions[hero.PartyOrder];
    await CreateView(hero, heroPath);
}

Now I loop over the keys in the “ParyOrderSystem”. Each key is one of our Hero Entity’s. Next I check that the “PartyOrder” is low enough to be included in the encounter’s battle team, and if not will “continue” to the next Hero. If the Entity is included, I will update its position based on the Encounter’s list of positions. We use the PartyOrder as an index into that list. Finally, I use the “CreateView” method to instantiate a prefab representing the new hero.

There is one additional bug I would like to fix at this time. When a combatant moves on the board, their sorting order is updated to appear closer or further depending on their vertical position. However, when placing them initially, I do not assign the correct layer order, so things can look wrong right from the beginning. To fix that, add the following to the end of the CreateView method:

var combatant = view.GetComponent<CombatantView>();
ICombatantViewSystem.Resolve().SetLayerOrder(combatant, entity.Position.y);

Skill Explore Entry Option

Open the SkillExploreEntryOption. On line 20 we will want to make the following change:

// Replace this:
var hero = ISoloHeroSystem.Resolve().Hero;

// With this:
var hero = IPartyOrderSystem.Resolve().PartyLeader;

This change is allowing us to use the “Party Leader” Entity as a replacement for story based skill checks that the “Solo Hero” used to perform.

Solo Adventure Injector

Open the SoloAdventureInjector. This one is easy, we just need to delete line 13:

// Remove this:
ISoloHeroSystem.Register(new SoloHeroSystem());

Action Menu

Open the ActionMenu. On line 30 we will want to make the following change:

// Replace this:
entity = ISoloHeroSystem.Resolve().Hero; // TODO: Get the "current" entity from a "turn" system

// With this:
entity = ITurnSystem.Resolve().Current;

Encounter_01

Now that we’ve finished fixing all of the errors, we will need to update the asset at Asset/Objects/Encounters/Encounter_01.prefab. We will want to modify it to have four “Hero Positions”:

  1. 0, 0
  2. 0, 1
  3. 0, 2
  4. 0, 3

Demo

You should now be able to play the game with a full hero party. Give it a try! Each of the heroes will take a turn, then the monsters, and then rounds will continue in that order.

It’s also worth pointing out that the “Encounter_01” asset already handles some potential issues. If you add more “Hero Positions” than there are Heroes, nothing bad will happen. On the other hand if there are less “Hero Positions” than there are Heroes, then you just fight with a subset of your total hero party. The other members still exist, they just aren’t being used. Feel free to try that as well.

Summary

In this lesson we finally moved away from the idea of a solo adventure. We now have a full hero party, which will be helpful in the future as we want to add other features like the ability to heal an ally, or to test things like initiative order. We added a new Party “Order” so that we can control which Hero is considered the Leader, and also to make sure that we can control who participates in an Encounter and which of the hero positions they will occupy. We also put in a placeholder system for generating heroes, though at the moment it just clones our solo hero four times. In the next lesson we will start exploring ways to make each hero unique.

If you got stuck along the way, feel free to download the finished project for this lesson here.

If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *