Gaktan
Recruit
Creating and registering the CampaignBehavior
Before creating a quest, you need to create a new
(Some quests use a
This is not known yet if you need one CampaignBehavior per quest, but that's what TaleWorlds seems to be doing, so make sure you create one CampaignBehavior per quest.
First create a class called
In your submodule class, override the
Here is an example of a quest that get triggered when entering a settlement:
Note: In this example I decided to trigger the quest when the player enters a settlement, but in theory you could create and start a quest anywhere you'd like. It doesn't have to be in the CampainBehavior class.
Creating the quest itself
To create a class create a new inner inter class to
You will need to override a few abstract methods but they should be pretty easy to understand.
The way dialog work should need a whole tutorial of their own, I am not going to go into too much details. I added some comments in the code below to hopefully make it understandable.
In the following example, I created a simple dialog that can go two ways:
Journal logs
A journal log is an entry in the quest log that you see when opening the Quest Book. It can be used to display a text message, or a progression bar.
There are multiple types of journal logs available:
This screenshot also shows a couple more things:
Quest events
As explained earlier, triggering the quest is as easy as creating a new
You can pretty much start a new quest whenever you want, as long as the game was completely loaded first. (Exact starting point not known).
If you try to create a quest before that point, it will actually create the quest but it won't be visible. The quest logic will still work though.
You can complete the quest by calling either one of these functions:
This could be used to start additional quests, update the quest log and so on.
Before creating a quest, you need to create a new
TaleWorlds.CampaignSystem.CampaignBehavior
Class.(Some quests use a
TaleWorlds.CampaignSystem.CampaignBehavior.SaveableCampaignBehaviorTypeDefiner
Class as well, but not sure what this is actually used for). Both of these will help serialize your quests to be able to save/load them, this will be done completely automatically.This is not known yet if you need one CampaignBehavior per quest, but that's what TaleWorlds seems to be doing, so make sure you create one CampaignBehavior per quest.
First create a class called
MyCampainBehavior
that inherits from CampaignBehaviorBase
, and implement all the abstract functions.In your submodule class, override the
OnGameStart
function. This function is called when the game starts (from loading or after character creation), this is where we are going to register all your campaign behaviors:
C#:
protected override void OnGameStart(Game inGame, IGameStarter inGameStarter)
{
base.OnGameStart(inGame, inGameStarter);
// Make sure we only load the quest in StoryMode.
// This is not necessary, only if don't want your quest to start in a a game mode that's not the StoryMode (Like multiplayer)
if (!(inGame.GameType is StoryMode.CampaignStoryMode))
return;
CampaignGameStarter campaignGameStarter = (CampaignGameStarter) inGameStarter;
campaignGameStarter.AddBehavior(new MyCampainBehavior());
}
Here is an example of a quest that get triggered when entering a settlement:
Note: In this example I decided to trigger the quest when the player enters a settlement, but in theory you could create and start a quest anywhere you'd like. It doesn't have to be in the CampainBehavior class.
C#:
class MyCampainBehavior : CampaignBehaviorBase
{
public override void RegisterEvents()
{
CampaignEvents.SettlementEntered.AddNonSerializedListener(this, SettlementEnteredEvent);
}
// Event triggered when any party enters a settlement
private void SettlementEnteredEvent(MobileParty arg1, Settlement arg2, Hero arg3)
{
if (arg3 != Hero.MainHero)
return;
// Do not repeat this quest. (Currently don't know how to check completed quests)
if (Campaign.Current.QuestManager.Quests.Any(q => q.GetType() == typeof(MyCampainQuest)))
return;
Hero quest_giver = Hero.MainHero;
// Find nearest village around the player that is not the current settlement
IEnumerable<Settlement> settlements = Settlement.FindSettlementsAroundPosition(quest_giver.GetPosition().AsVec2, 200.0f)
.OrderBy(s => s.GetTrackDistanceToMainAgent())
.Where(s => s.IsVillage && s != arg2);
// Start the quest in that settlement
Settlement settlement = settlements.First();
if (settlement != null)
new MyCampainQuest(quest_giver, settlement).StartQuest();
}
public override void SyncData(IDataStore dataStore)
{
// Not sure what this does. Multiplayer maybe?
}
public class MyCampainQuestBehaviorTypeDefiner : CampaignBehaviorBase.SaveableCampaignBehaviorTypeDefiner
{
public MyCampainQuestBehaviorTypeDefiner()
// This number is the SaveID, supposed to be a unique identifier
: base(846_000_000)
{
}
protected override void DefineClassTypes()
{
// Your quest goes here, second argument is the SaveID
AddClassDefinition(typeof(MyCampainBehavior.MyCampainQuest), 1);
}
}
}
Creating the quest itself
To create a class create a new inner inter class to
MyCampainBehavior
called MyCampainQuest
that inherits from QuestBase
.You will need to override a few abstract methods but they should be pretty easy to understand.
SetDialogs()
is the function where you will setup all the quest's dialogs. This should be called twice. Once in the constructor, and once in InitializeQuestOnGameLoad
. This is not known why, but that's what TaleWorlds seems to be doing (need more info).The way dialog work should need a whole tutorial of their own, I am not going to go into too much details. I added some comments in the code below to hopefully make it understandable.
In the following example, I created a simple dialog that can go two ways:
- Positive: The quests ends successfully
- Negative: A new task is added (travel a distance of x units) before the quest can be completed
C#:
// Could also use StoryModeQuestBase, but it's a lot more restrictive
internal class MyCampainQuest : QuestBase
{
[SaveableField(1)]
private bool _metAntiImperialMentor;
[SaveableField(2)]
protected float _TotalDistanceTraveled;
// Save JournalLog so we can update it or just keep track of it
protected JournalLog _Task1;
protected readonly int _DistanceToTravel = 100;
protected Vec2 _PreviousPos;
private TextObject _StartQuestLog
{
get
{
TextObject parent = new TextObject("Find and meet {HERO.LINK} and tell him he is your dad. He is currently in {SETTLEMENT}.");
StringHelpers.SetCharacterProperties("HERO", StoryMode.StoryMode.Current.MainStoryLine.AntiImperialMentor.CharacterObject, null, parent);
parent.SetTextVariable("SETTLEMENT", StoryMode.StoryMode.Current.MainStoryLine.AntiImperialMentor.CurrentSettlement.EncyclopediaLinkWithName);
return parent;
}
}
private TextObject _EndQuestLog
{
get
{
TextObject parent = new TextObject("You talked with {HERO.LINK}.");
StringHelpers.SetCharacterProperties("HERO", StoryMode.StoryMode.Current.MainStoryLine.AntiImperialMentor.CharacterObject, null, parent);
return parent;
}
}
private TextObject _YouShouldRunLog
{
get
{
TextObject parent = new TextObject("Your \"dad\" wants you to run {DISTANCE} units.");
parent.SetTextVariable("DISTANCE", _DistanceToTravel);
return parent;
}
}
private TextObject _YouShouldRunText
{
get
{
TextObject parent = new TextObject("How dare you? How about you run {DISTANCE} units, just for fun.");
parent.SetTextVariable("DISTANCE", _DistanceToTravel);
return parent;
}
}
public override TextObject Title
{
get
{
TextObject parent = new TextObject("Meet with your \"dad\" {HERO.NAME}");
StringHelpers.SetCharacterProperties("HERO", StoryMode.StoryMode.Current.MainStoryLine.AntiImperialMentor.CharacterObject, (TextObject) null, parent);
return parent;
}
}
public override bool IsRemainingTimeHidden
{
get
{
return false;
}
}
public MyCampainQuest(Hero questGiver, Settlement settlement)
: base("my_campain_story_mode_quest", questGiver, duration: CampaignTime.DaysFromNow(20), rewardGold: -100 )
{
_metAntiImperialMentor = false;
SetDialogs();
HeroHelper.SpawnHeroForTheFirstTime(StoryMode.StoryMode.Current.MainStoryLine.AntiImperialMentor, settlement);
AddTrackedObject(settlement);
AddLog(_StartQuestLog);
AddTwoWayContinuousLog(new TextObject("This is a two way continuous Log. We are not going to use it."), new TextObject("Task 2"), 0, 10);
}
protected override void RegisterEvents()
{
CampaignEvents.TickEvent.AddNonSerializedListener(this, Tick);
}
protected void Tick(float dt)
{
if (_Task1 != null && !_Task1.HasBeenCompleted())
{
// Compute total distance traveled on the map.
Vec2 current_pos = Hero.MainHero.GetPosition().AsVec2;
_TotalDistanceTraveled += current_pos.Distance(_PreviousPos);
_Task1.UpdateCurrentProgress((int)Math.Floor(_TotalDistanceTraveled));
_PreviousPos = current_pos;
}
}
protected override void InitializeQuestOnGameLoad()
{
SetDialogs();
}
protected override void SetDialogs()
{
// Dialog to offer the quest?
OfferDialogFlow = DialogFlow.CreateDialogFlow(QuestManager.QuestOfferToken, 100)
.NpcLine(new TextObject("{=!}You shouldn't see this."))
.Condition(() => Hero.OneToOneConversationHero == QuestGiver)
.CloseDialog();
// Dialog to discuss the quest?
DiscussDialogFlow = DialogFlow.CreateDialogFlow(QuestManager.QuestDiscussToken, 100)
.NpcLine(new TextObject("{=!}You shouldn't see this."))
.Condition(() => Hero.OneToOneConversationHero == QuestGiver)
.CloseDialog();
// Dialog when starting a conversation with the AntiImperialMentor if we have NOT met him
DialogFlow unmet_dialog = DialogFlow.CreateDialogFlow(QuestManager.NpcLordStartToken, 110)
.NpcLine(new TextObject("So. Who are you, and what brings you to me?"))
// This whole dialog will only start if the following conditions are met
.Condition(() => Hero.OneToOneConversationHero != null && Hero.OneToOneConversationHero == StoryMode.StoryMode.Current.MainStoryLine.AntiImperialMentor && !_metAntiImperialMentor)
.PlayerLine(new TextObject("Hi dad, it's me, your boy."))
.NpcLine(new TextObject("Is that true? Well, that is interesting."))
// Player option: Starts with BeginPlayerOptions() and ends with EndPlayerOptions()
.BeginPlayerOptions()
// PlayerOption will trigger the following dialog flow up to the next CloseDialog() when selected
.PlayerOption(new TextObject("Yes, it's really me dad."), null)
.NpcLine(new TextObject("Good. I don't have anything to say."))
.Consequence(new ConversationSentence.OnConsequenceDelegate(GoodBoy))
.CloseDialog()
.PlayerOption(new TextObject("Actually I was just joking."), null)
.Consequence(new ConversationSentence.OnConsequenceDelegate(BadBoy))
.NpcLine(_YouShouldRunText)
.NpcLine(new TextObject("Now, GO!"))
.CloseDialog()
.EndPlayerOptions()
.CloseDialog();
// Dialog when starting a conversation with the AntiImperialMentor if we have met him
DialogFlow met_dialog = DialogFlow.CreateDialogFlow(QuestManager.NpcLordStartToken, 110)
.NpcLine(new TextObject("So, did you run like I told you to?"))
// This whole dialog will only start if the following conditions are met
.Condition(() => Hero.OneToOneConversationHero != null && Hero.OneToOneConversationHero == StoryMode.StoryMode.Current.MainStoryLine.AntiImperialMentor && _metAntiImperialMentor)
.BeginPlayerOptions()
.PlayerOption(new TextObject("Yes I did."), null)
.Condition(() => _Task1.HasBeenCompleted())
.NpcLine(new TextObject("Ok Great. Now leave"))
.Consequence(() => Campaign.Current.ConversationManager.ConversationEndOneShot += new Action(CompleteQuestWithSuccess))
.CloseDialog()
.PlayerOption(new TextObject("Actuall no, I didn't."), null)
.NpcLine(new TextObject("Don't come back until you did."))
.CloseDialog()
.EndPlayerOptions()
.CloseDialog();
Campaign.Current.ConversationManager.AddDialogFlow(unmet_dialog, this);
Campaign.Current.ConversationManager.AddDialogFlow(met_dialog, this);
}
private void GoodBoy()
{
AddLog(new TextObject("You were a good boy"));
Campaign.Current.ConversationManager.ConversationEndOneShot += new Action(CompleteQuestWithSuccess);
_metAntiImperialMentor = true;
}
private void BadBoy()
{
AddLog(new TextObject("You were a bad boy"));
_Task1 = AddDiscreteLog(_YouShouldRunLog, new TextObject("Units ran."), currentProgress: 0, targetProgress: _DistanceToTravel);
_PreviousPos = Hero.MainHero.GetPosition().AsVec2;
_TotalDistanceTraveled = 0.0f;
_metAntiImperialMentor = true;
}
// Called after the quest is finished
protected override void OnFinalize()
{
base.OnFinalize();
AddLog(_EndQuestLog);
}
}
Journal logs
A journal log is an entry in the quest log that you see when opening the Quest Book. It can be used to display a text message, or a progression bar.
There are multiple types of journal logs available:
- Blue: regular log, it just displays text.
AddLog(new TextObject("This is a regular Log"));
- Turquoise: two way continuous log.
AddTwoWayContinuousLog(task_text, new TextObject("Task 2"), 0, 10);
- Purple: discrete log, shows a progression bar,
AddDiscreteLog(_YouShouldRunLog, new TextObject("Units ran."), currentProgress: 0, targetProgress: _DistanceToTravel);
This screenshot also shows a couple more things:
- Orange: Quest title
- Pink: Time remaining, can be disabled when overriding
IsRemainingTimeHidden
- Green: Quest giver
Quest events
As explained earlier, triggering the quest is as easy as creating a new
MyCampainQuest
object and calling StartQuest()
.You can pretty much start a new quest whenever you want, as long as the game was completely loaded first. (Exact starting point not known).
If you try to create a quest before that point, it will actually create the quest but it won't be visible. The quest logic will still work though.
You can complete the quest by calling either one of these functions:
- CompleteQuestWithBetrayal
- CompleteQuestWithCancel
- CompleteQuestWithFail
- CompleteQuestWithTimeOut
- CompleteQuestWithSuccess (protected)
QuestBase
class:This could be used to start additional quests, update the quest log and so on.
- OnCanceled()
- OnFailed()
- OnBetrayal()
- OnCompleteWithSuccess()
- OnFinalize()
- OnStartQuest()
- OnTimedOut()
Last edited: