Skip to content

Demon Builder

pickled-dev edited this page May 17, 2024 · 4 revisions

This is far and away the most complex part of the site. Everything feel a little weird if you're new to big web platforms like Angular, but Demon Builder is wholly my own creation and thus much more jumbled and less thought out. Couple that with how difficult recursive call stacks can be to wrap your mind around already, and you get something quite impenetrable. I know that every time I come back to the project the DemonBuilder is something I have to sit down and read through again to understand what's going on.

I'm going to try to explain the general flow here in English to hopefully make the code itself easier to understand.

What is the Demon Builder?

If you have never played an SMT game before, or haven't played one with inheritance, I imagine the Demon Builder is going to be pretty difficult to understand.

In SMT you recruit demons to fight for you. Once you have recruited at least two demons, you can 'fuse' them together to create an entirely different demon. In a lot of the games this resultant demon will inherit properties from it's sources. Now when you recruit yet another demon, you can fuse that with your resultant demon, and get a new demon that now can have properties of it's parent and it's grandparents. This can be done several times, and in some games it can be done ad infinitum.

In some games these fusions are completely deterministic, and can thus be calculated given a few parameters. The goal of the Demon Builder is to perform these calculations and give the users lists of fusions that can result in a desired demon with desired properties.

Nomenclature

See app/shared/smt-tools.types.ts for for more information and type definitions.

BuildRecipe

This is the largest unit of data in the DemonBuilder. It contains all the information necessary to create a resultant demon with the desired properties.

Fusion

A single step of a fusion chain. Contains demons that will be fused (the sources) and the resultant demon (the result) as well as what properties are inherited in the fusion.

Fusions can be thought of as a multiplication problem where the two demons that fuse together are the factors and the resulting demon is the product. Demon A x Demon B = Demon C

Fission

Because of the deterministic nature of fusions, it is possible to calculate every single fusion that will result in a given demon. These fusions that result in a specific demon are called it's fissions. For example: Say we have 5 demons. They are called Demon A through Demon E. Demon A + B = Demon E and Demon C + Demon D = Demon E. Demon E is the resultant demon and the pairs Demon A and B and Demon C and D are it's fissions.

You can think of fissions as finding all the factors that make up a product.

For our purposes, fissions can be thought of as a subset of fusions, because they can be represented with the Fusion data type.

Advanced Fusion Concepts

There's a lot of hidden stuff in the SMT games that seasoned players might not even be aware. These concepts are not documented in the games, at least the one's I've played, and don't really have a strong bearing on gameplay unless you're really trying to break things.

Elemental Demons

This one is probably quite obvious to anyone who's played more than Persona 5. There are demons that cannot be fused together, only recruited. These demons are usually rare and have a lot of skills that can be very useful. In Persona 5 these are called Treasure Demons.

Inheritance Types

There is strikingly little information about this on the web, the best I found was a GameFAQs thread from a while back.

Skill inheritance types return in P5. Each persona has a hidden inheritance type that determines what skills it can receive during fusion based on the skills' elements. These restrictions apply to both the guillotine and gallows, but can be bypassed through use of skill cards. Furthermore, unique skills cannot be passed on irregardless of inheritance type. There are a total of twelve inheritance types with the following restrictions:

Algorithm

The meat of the project is in the DemonBuilder class. This feature will allow a user to supply a variety of properties of a demon they want to build, and the builder will tell you the steps to make that demon. This is only applicable in some game, but could be a very powerful feature to users who enjoy tinkering with the mechanics of the game.

The algorithm I developed is far from perfect, and will capture most cases that a user could desire.

In some games, the list of possible combinations to attain a given build is infinite, so we have to draw the line somewhere. Since my algorithm is recursive it was pretty easy to draw that line; I usually don't go deeper than 1 recursive call, but I do go to 2 in some edge cases.

Weaknesses

There are some possible fusions that I am aware my algorithm won't detect. Primarily, my algorithm will not check elemental demons for any skills. This is mostly because you cannot fuse to create an elemental demon, so when you try to get the fissions of an elemental demon my function will return an empty list and go on without checking the elemental demon.

Code Structure

My algorithm is built with RxJS and web workers so it doesn't tie up the main thread and make the page look like it's hung. Specifically I use a package called observable-webworker. When the user clicks the 'Calculate' button the browser spins up a new thread for the webworker and creates an instance of the DemonBuilder object which implements an interface from observable-webworker called DoWorkUnit. This will require a method called workUnit which will return an RxJS Observable. The calculation will update emit different data depending on how the algorithm is proceeding: any successful calculations, any errors, and how many fusions it has attempted.

First Steps

There are two primary cases that emerge when trying to build a demon: if you want a specific species of demon with certain skills, or if you want any demon with certain skills. The two end up being mostly the same with one key difference. If the user has not specified a demon name I build a list of every demon with at least one of the desired skills, then iterate over that list running the same algorithm I use for specific species of demons.

Here's a very broad outline of the algorithm:

if (userInput is not valid)
  throw Error

if (user did not enter demonName) {
  potential-demons: list = getAllDemonsWithDesirableSkills
  for (demon in potential-demons) {
    getFusionChains(demon)
  }
} else {
  getFusionChains(demon)
}

Methods

There are 4 main methods in the DemonBuilder: validateInput, getFusionChains, getFusionChain, and isPossible. And each method is then split in two, for if the user provided a demonName or not. Each method is prefixed with either demon_ or noDemon_. There is also a wrapper method for each with no prefix. You should only ever call the wrapper method.

Abstraction

At the time of writing, I have only implemented the DemonBuilder for Persona 5, and I have only played Persona 5 and Persona 5 Royal. Persona 5 Royal introduces new mechanics that will require a P5RDemonBuilder that won't even be able to extend the P5DemonBuilder. And from what I know of Persona 3 it would also require a major rewrite to work for that game.

So I want to keep as many methods as possible implemented in the abstract class of DemonBuilder to minimize the work to implement each new game, but I'm assuming all 8 methods will need to be re-implemented for almost every game. Perhaps in the case of P3P it will be able to extend the P3DemonBuilder, or--if I'm lucky--P3P will be able to simply use the P3DemonBuilder with a different dataset.

validateInput()

So far I have identified 7 fail conditions that can be determined easily by examining user input with little calculation:

  1. The user has entered the name of a demon that is a higher level than the max level the user entered
  2. The user has entered a skill that is only learned by demons a higher level than the max level the user has entered
  3. The user has specified a unique skill and the name of a demon that isn't the demon that learns the unique skill
  4. The user has specified a skill that conflicts with the inheritance type of the demon they entered
  5. The user has specified more skills than the demon they specified can inherit and learn
  6. The user has specified more skills than the demon ANY demon can inherit and learn
  7. The user has specified an elemental demon in the name field

validateInput() will check all 7 of these cases and throw an error if any of them are failed

isPossible()

This method is very similar to validateInput, but will not throw an error if the provided data yield an impossible fusion.

noDemon_getFusionChains()