Skip to content

Conversations

Conversation flow is structured into Topics (similar to what the original scripts call Info).

Most of the time, a Topic is one of the selectable choices you can make when talking to an NPC, but they can also be automatically triggered (forced) or be part of sub-dialogue flow.

In general, a Topic consists of the following parts:

  • Name of the NPC the Topic belongs to
  • Conditions of when the Topic should appear
  • An Act function (coroutine) containing the actual sequence

Upon startup, the game searches for all classes inheriting from UConversationTopic and registers them to the NPCs they belong to.

At the lowest level, one of the simplest conversation topics you could make is this:

class UChoice_MyCustomChoice : UConversationTopic
{
// Text displayed in the selection UI (non localized for simplicity)
default Caption = FText::FromString("My Custom Choice");
default ForCharacter = n"OC_STT_DIEGO"; // Self
default WithCharacter = n"Hero"; // Other
// Coroutine containing the conversation sequence
UFUNCTION(BlueprintOverride)
void Act()
{
Other.AI.Say(FText::FromString("What is this?"));
Self.AI.Say(FText::FromString("This is my custom choice!"));
}
}

The game would automatically add this new Topic to Diego. Since it has no conditions, it will be permanently selectable.

For Gothic 1 Remake, we created classes to inherit from so that we don’t have to repeat e.g. the involved character names all the time. With those, the class could be changed to this:

class UChoice_MyCustomChoice : G1R::Conversation::UTopic_Hero__OC_STT_DIEGO
{
// Text displayed in the selection UI (non localized for simplicity)
default Caption = FText::FromString("My Custom Choice");
UFUNCTION(BlueprintOverride)
void Act()
{
Hero.AI.Say(FText::FromString("What is this?"));
Diego.AI.Say(FText::FromString("This is my custom choice!"));
}
}

There are 3 types of conditions in each Topic:

  1. Topic flow conditions: “Have I ever done this other topic before?” - fastest to evaluate, checked first
  2. Dynamic conditions: Some often recurring dynamic conditions like “Am I close to this location”
  3. Script conditions: IsVisible script function where you can put custom script code to return true/false - slowest, checked last

Adding some of each to the example above:

class UChoice_MyCustomChoice : G1R::Conversation::UTopic_Hero__OC_STT_DIEGO
{
// Text displayed in the selection UI (non localized for simplicity)
default Caption = FText::FromString("My Custom Choice");
// Topic flow condition: don't show this if we have already seen it ("non permanent" in OG terms)
default Rules.HideIfKnows(UChoice_MyCustomChoice);
// Topic flow condition: Show this only after the player has seen that other choice
default Rules.AllowIfCharacterHasKnowledgeOf(UChoice_SomeOtherChoice);
// Dynamic condition: (you could do this in the script conditions too, but that's more performance heavy on forced conversations)
default Rules.RequireCharacterCloseToWaypoint(n"Self", n"WP_EZ_STORY_GENERIC_DIEGO", 1500.0f);
// Dynamic condition: Require a specific voiceline to be heard (Prefer topic flow if possible)
default Rules.RequireCharacterHasListenedTo(n"HERO", LocText("INFO_RIORDIAN_MESSAGEWHY_14_06"));
// Script conditions: These are slowest to evaluate, but most flexible.
UFUNCTION(BlueprintOverride)
bool IsVisible() const
{
return G1R.Chapter > 3;
}
UFUNCTION(BlueprintOverride)
void Act()
{
// ...
}
}

To have the game look up a localized Text-ID, use the LocText(...) function (returns FText). Alternatively, you can assign a Text-ID directly to an FText, but that does not work in all contexts unfortunately.

Global story relevant variables (stored in savegame) and quests are attached to Story objects. For Gothic 1 Remake, this would be UStoryG1R.

If you want to create new variables for your mod, you may want to create your own custom Story object: Multiple stories can be active at the same time, but only one of them will be registered as “Main story”.

To create a new story, inherit from UStory:

class UStoryMyMod : UStory
{
const int32 XP_Finished_MyQuest = 50;
const int32 XP_Finished_MyOtherQuest = 150;
UPROPERTY(SaveGame)
int32 MyCustomValue = 0;
}

Each story has a static Get accessor function, so to access your global story object, just do this:

UStoryMyMod::Get().MyCustomValue = 123;

Each quest is defined as a class of type UQuest. Quests must belong to a story, which can be the Gothic 1 Remake main story or your custom made one.

You can either trigger the quests manually, or have them automatically check for start, success and failure conditions every couple ticks.

class UQuest_CreaturesGlossary_BiterGlossary_BiterEntry2 : UQuest
{
default BelongsToStory = UStoryMyMod;
AGothicCharacterState GetHero() const property { return GetCharacter(n"HERO"); }
UFUNCTION(BlueprintOverride)
bool ShouldSucceed() const
{
return UStoryMyMod::Get().MyCustomValue > 5;
}
UFUNCTION(BlueprintOverride)
void HandleQuestSucceeded()
{
Hero.GiveExperience(UStoryMyMod::Get().XP_Finished_MyQuest);
}
}