Quests/NPCS Info

Updated 10 months ago

Quests Quests are enumerated as EventManagement.SpecialEvent. This is critical because the story events are part of the map definition, and the map gets saved in the save file. So each quest gets a unique ID based on this enum.

Quest states are managed entirely by custom StoryEvents. There are a lot of them. Most quests have multiple story events associated with them. The game thinks that story events are things that get completed when they are explicitly set as completed, but there's actually a patch in EventManagement that changes this behavior. I've patched StoryEventsData.EventCompleted, and there's code in there to check if certain things have happened. Sometimes this is super necessary because the game isn't aware that the normal story won't actually progress in this mod. This patch forces some story events to always be true, and for other story events it runs a check. I also have a patch over StoryEventsData.SetEventCompleted so that some story events get saved to the run, and some get saved to your normal save file (for example, the story events around quests get saved to the run-based section of the save, but the story events like 'player has seen the credits' get saved to the normal part of the save).

Quests are advanced entirely based on engaging in dialogue with an NPC. EventManagement has a method called 'SetDialogueForSpecialEvent' which handles figuring out which dialogue lines the NPC should say. It does so by putting story pre-requisites and anti-pre-requisites on each line of dialogue.

Example:

if (se == SpecialEvent.ILoveBones) { node.SetDialogueRule("P03ILoveBones", "I LOVE BONES!!", se, antiPreRequisite:I_HAVE_THREE_BONES); node.SetDialogueRule("P03ILoveBonesSuccess", "I LOVE BONES!!!!", se, preRequisite:I_HAVE_THREE_BONES, completedCardReward:CustomCards.SKELETON_LORD, completeAfter:true); }

SetDialogueRule defines the ID for the line of dialogue that will be said, then it defines the text that will appear over the NPC's head, then it passes the specific event id, and then some other information.

In this example: the "I love bones" quest has two possible NPC dialogues. The first dialogue describes the quest to the player, and you see it has an anti-prerequisite, which means this dialogue cannot play if that story event is true. You can look up that story event and see that there's some code in the StoryEventsData patch I mentioned before that checks to see if you have three or more cards in your deck that are brittle. So if you do NOT have three or more brittle cards in your deck, this dialogue will play.

The second dialogue has the opposite requirement - you MUST have three bones in your deck. You can see that it defines that you get a custom card as your reward, and the quest "completes" after this dialogue is complete.

You can poke at the code a little more to get an idea of how each quest is coded, but it all basically works this way. You have to code the dialogue for the quest in this method, defining each line of dialogue that the NPC can say, under what conditions they say it, and what reward you get (if any) when they say it (and you pretty much should always complete the quest if you give them a reward).

Adding Quests to the Map Woof. This is part of the map generator spaghetti code and it's rough.

The GetSpecialEventForZone method handles getting quests added to the map. The purpose of this method is to give the map generator information about which quests it needs to add to the map. It returns a list of tuples, where the first item in the tuple is the ID of the quest, and the second is a method that returns True or False depending upon whether or not a particular room in the map is eligible to have that quest in it.

The way this method workds right now is that it creates a list of quests that can appear on any map. Then it removes quests that can't appear on the final map (i.e., quests that require you to do work on more than one map) and removes quests you've seen before. Then as long as there's at least one valid quest, it chooses one at random and then adds the following tuple to the return:

events.Add(new (randomEvent, bp => bp.color == 1 && !bp.isSecretRoom));

What this is saying is "my random event can go into any room as long as the room's 'color' is 1 and it is not a secret room." You can look at all the properties of HoloMapBlueprint to get all the possible properties of a room from the perspective of the map generator, but I'll explain these two properties now.

Color: the map is divided (broadly speaking) into four quadrants; the generator calls them "colors" (because in a very very early version they used to actually have different background colors). Part of what makes the map feel "good" is that I force things to appear only once per quadrant/color. I also force the random events to appear in the first quadrant/color; that's the one the player spawns in. This means you'll find the NPC very early on.

isSecretRoom: Pretty straightforward; if this is true, it means the arrow to enter this room is not visible on the map unless you hover over it. I make sure that the NPC does not spawn in a secret room.

If you look at the rest of the code in this method, you'll see how all the other quests are handled. If a quest has a specific follow-up (for example, if you make a donation in one map, you get a reward on the next map) it gets handled here. Some quests always happen on certain maps. Etc.

NPC Faces Hopefully you don't have to touch any of this code that generates random NPC faces. The only "gotcha" I can think about with this is that if you end up wanting to add more face components, the code expects there to be exactly the same number of backgrounds, hats, mouths, and eyes. You can just add one more hat - you have to keep the same number for all of them. You have to follow the naming convention you see in the assets directory, increasing the number by one each time, and you have to modify the NUMBER_OF_CHOICES constant in faces/P03ModularNPCFace.cs to match the number of choices for each layer of the face.